안드로이드 서비스는 Background Service
/Foreground Service
/Bind Service
로 3가지가 존재한다. 이번 포스팅엔 이 3가지 서비스들의 특징에 대해 서술하고 어떤 상황 때 위 서비스들을 선택하면 좋을 지 알려주고자 한다.
UI와는 완전 무관하게 백그라운드 작업을 위한 서비스이다. 이 서비스를 사용 시, 사용자의 인지 밖에서 백그라운드 작업을 실행시킬 수 있다. 하지만 여기서 필자는 아래와 같은 의문이 들었다.
Background Service
만으로 그 외 2가지 서비스들을 대체하여 쓰면 되는것 아닌가?
하지만 Android API 26버전부터 백그라운드 제한 정책으로 인해 Background Service
를 무한정으로 실행시킬 수 없다. 왜냐하면, 앱 내에서 무분별하게 실행중인 Background Service
들로 인해, 모바일 기기 의 메모리 사용량 증가로, 이로 인해 안드로이드 시스템은 앱을 무작위로 kill할 수 있기 때문이다. 이때 만약 kill당한 앱이 사용자가 심취해 듣고 있던 음악 앱일 경우, 사용자의 경험은 떨어질 것이다.
참고 : https://developer.android.com/about/versions/oreo/background?hl=ko#overview
이런 이유로, Android는 Background Service
를 실행시키는 데, 제약사항을 추가한다. 첫 번째로 Foreground Service
를 대신해 사용하라는 것. 두 번째로 JobScheduler
등과 같이 지속적 실행이 아닌 '예약'을 통해 간헐적인 백그라운드 작업을 실행시키는 것이다.
하지만 그렇다고 Background Service
가 무조건 나쁘다는 것은 아니. 적절히만 사용한다면 오히려 사용자 경험을 증가시키는데 기여할 수 있으며, 이런 예외 사항으로 4가지가 있다.
첫 번째로, FCM을 통해 사용하는 MyFirebaseMessageService
이다. 이를 통해 알림 메시지를 수신받았을 경우, 안드로이드 시스템은 Background Service
를 실행시킬 수 있는 약간의 유휴시간을 부여한다. 두 번째로, PendingIntent
가 NotificationManager
를 통해 실행되는 경우도 허용한다. 세 번째로, BroadcastReceiver
를 통해 MMS/SMS
메시지를 수신받았을 때도 허용하며, 마지막으로 VPNService
를 활용한 네트워크 처리를 할 때도 Background Service
사용이 허용된다. 즉, 안드로이드는 아래 4가지 사항일 때엔 Background Service
를 사용할 수 있다고 말하며 이를 활용하는 것은 사용자 경험을 해치지 않으며 오히려 증가시킬 수 있음을 인정하고 있는 셈이다.
참고 : https://developer.android.com/about/versions/oreo/background?hl=ko#services
아래는 필자가 진행하고 있는 프로젝트의 코드로, SMS/MMS메시지를 수신받아 Background Service
를 실행하고 있는 코드이다.
<service
android:name=".util.SMSHandlerService"
android:exported="false" />
<receiver
android:name=".util.MySMSReceiver"
android:enabled="true"
android:exported="true"
android:permission="android.permission.BROADCAST_SMS">
<intent-filter>
<action android:name="android.provider.Telephony.SMS_RECEIVED" />
</intent-filter>
</receiver>
우선 첫 번째로, 매니페스트에서 MySMSReceiver
를 등록했다. 이로 인해 기기 내 발생하는 SMS/MMS 브로드캐스트는 위 클래스로 라우팅되며 앱이 실행된다. 이렇게 되었을 경우, 안드로이드 시스템은 Background Service
실행을 위한 유휴시간을 제공한다. 이에 맞게, BroadcastReceiver
클래스의 onReceive
콜백으로 받는 메서드 내부에서 SMSHadlerService
를 Background Service
로 실행시켜주고 있다.
// MySMSReceiver 내부
override fun onReceive(context: Context, intent: Intent) {
CoroutineScope(Dispatchers.IO).launch {
try {
if (getPrefsBooleanUseCase(Pair(SP_MAIN_SMS_AGREE, false)).successOr(false)) {
context.startService(Intent(context, SMSHandlerService::class.java).apply {
putExtra(SMSHandlerService.USER_ID, userId)
putExtra(SMSHandlerService.SENDER, sender)
})
}
} finally {
cancel()
}
}
}
이 글을 읽으시는 분들도 위의 사례처럼 내가 구현할 Service가 위 4가지 사항에 부합한지 체크해보고, 만약 그렇다면 Background Service
를 사용할 수 있을 것이다.
만약, Background Service
를 AOS 14기기에서 실행시킨다면 어떤 결과가 발생할까? 크게 3가지로 분류해 서비스의 실행 여부를 체크할 수 있다.
[Service Test Case]
1. 화면을 나갔을 때, 실행되는가?
2. 홈 버튼을 눌러 앱을 나갔을 때, 실행되는가?
3. Task Stack에서 앱을 지웠을 때, 실행되는가?
간단하게 아래의 샘플코드를 작성해보았고 그에 대한 결과이다.
Manifest 내부 코드
<service
android:name=".MyService"
android:enabled="true"
android:exported="true"
/>
// Activity 에서 실행
Intent(this, MyService::class.java).apply {
startService(this)
}
// Service 내부 코드
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
CoroutineScope(Dispatchers.Default).launch {
repeat(100000000) {
delay(1000L)
println("serviceLog, [start], onStartCommand, count : $it, startId : $startId")
}
}
return START_STICKY
}
override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent)
println("serviceLog, onTaskRemoved")
}
override fun onDestroy() {
super.onDestroy()
println("serviceLog, onDestroy")
}
[Service Test Case]
1. 화면을 나갔을 때, 실행되는가? : True
2. 홈 버튼을 눌러 앱을 나갔을 때, 실행되는가? -> True
3. Task Stack에서 앱을 지웠을 때, 실행되는가? -> False
onTaskRemoved
와 onDestroy
생명주기 흐름도 함께 말하자면, 백그라운드 서비스가 지장이 없을 경우, 이 두가지 생명주기는 호출되지 않지만 그렇지 않을 경우는 호출되는걸 확인할 수 있다.
이 서비스는 Background Service
와 약간의 차이를 가진다. 첫 번째는 해당 서비스 실행을 위해 매니페스트 상 권한을 설정해줘야 한다는 점이다. 두 번째로, startForegroundService(...)
메서드 실행 후, onStartCommand()
내부에서 startForeground()
호출을 통해 해당 서비스를 Foreground Service
로 승격시키는 추가 작업이 포합된다는 것이다. 그 후, 5초 이내에 NotificationManager
를 활용하여 알림을 반드시 띄워줘야한다. (만약 그러지 않을 시, ANR에러가 발생하게 된다.)
[
Background Service
vsForeground Service
]
- Foreground Service의 매니페스트 권한 설정
- Foreground Service시작 메서드 상이
- Foreground Service 추가 승격 필요
- 승격 직후, 알림 추가 필요
위 Background Service
와 마찬가지로 아래 3가지 Test Case를 실행해봤을 때 결과이다.
Manifest 내부 코드
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<service
android:name=".MyService"
android:enabled="true"
android:exported="true"
android:foregroundServiceType="dataSync"
/>
// Activity 에서 실행
Intent(this, MyService::class.java).apply {
startForegroundService(this)
}
// Service 내부 코드
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val notification = Notification.Builder(this, "my_channel_id")
.setContentTitle("Foreground Service")
.setContentText("Service is running...")
.setSmallIcon(android.R.drawable.ic_dialog_info)
.build()
startForeground(1, notification)
CoroutineScope(Dispatchers.Default).launch {
repeat(100000000) {
delay(1000L)
println("serviceLog, [start], onStartCommand, count : $it, startId : $startId")
}
}
return START_STICKY
}
[Service Test Case]
1. 화면을 나갔을 때, 실행되는가? -> True
2. 홈 버튼을 눌러 앱을 나갔을 때, 실행되는가? -> True
3. Task Stack에서 앱을 지웠을 때, 실행되는가? -> True
권한을 부여받은 Foreground Service
는 앱을 Task Stack에서 지웠을 때도 실행이 잘된다. 하지만 차이가 있다면 디바이스 상단에 Notification이 계속 떠있다는 점이며, 이로 인해 사용자에게 현재 앱이 백그라운드 작업을 수행하고 있음을 알린다는 점이다.
하지만 해당 위 서비스 실행 시, 메모리 누수를 조심해야한다. 만약 아래와 같은 코드를 실행시켰을 때, 문제가 무엇일까?
button.setOnClickListener {
Intent(this, MyService::class.java).apply {
startForegroundService(this)
}
}
버튼 클릭으로 서비스가 실행되기도 하지만 버튼 한 번의 클릭으로 위 Intent
객체가 변수에 저장되지 않았음으로 추후 종료할 수 없다. 따라서 위 객체를 변수나 프로퍼티에 할당해두고 추후, stopService(Intent))
호출을 잊지 말아야 한다.
실행할 백그라운드 작업이 Activity
/Service
수명주기와 합이 맞아야할 때 사용한다. 좀 더 풀어서 말하자면, Background/Foreground Service
는 서비스 종료를 위해 stopService()
를 무조건 호출해야 하지만, Bind Service
는 bindService()
호출 뿐만 아니라 이를 호출한 컴포넌트 수명주기가 onDestroy
되었을 때에도 자동 종료될 수 있다는 의미다. 즉, Bind Service
는 Background/Foreground Service
보다, 자신을 호출한 컴포넌트와 밀접하게 연관되는데, 이는 통신 또한 가능하다. 이러한 방법엔 Binder
/Messenger
/AIDL
로 3가지가 존재한다.
차이에 대해 논의하기 전, 공통점을 먼저 간단히 짚고자 한다. Bind Service
는 통신을 위한 필요조건이 있는데, 그건은 Service.onBind()
가 IBind
타입의 객체를 반환하면 이를 클라이언트의 ServiceConnection.onServiceConnectected()
에서 IBinder
를 수신받아 통신을 진행한다는 점이다.
// 서비스에서 3가지 방식의 IBinder를 반환
override fun onBind(intent: Intent): IBinder {
// 1. IBinder 클래스를 직접 정의하여
// 2. Messenger클래스를 사용해 IBinder타입을 반환하거나
// 3. AIDL 정의 및 빌드 완료한 Stub파일의 IBinder타입을 반환하거나
return binder
}
// 클라이언트에서 IBinder타입 수신 및 타입 캐스팅하여 사용
private val connection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
val binder = service as LocalService.LocalBinder
mService = binder.getService()
}
override fun onServiceDisconnected(arg0: ComponentName) { }
}
클라이언트와 서비스 간 통신은 큰 범주로 2가지로 우선 나눌 수 있다.
첫 번째로 다룰 Bind Class
는 '하나의 앱 프로세스에서 클라이언트와 서비스 간 통신'수행 시 사용하며, Messenger Class
/AIDL
은 '여럿의 앱 프로세스에서 클라이언트와 서비스 간 통신'수행 시 사용한다.
만약 내가 구현해야 할 Bind Service
가 나의 앱 프로세스 내에서만 통신해야한다면? Bind Class
구현체를 사용함이 현명하다. 또한 이는 꽤 괜찮은 장점도 존재하는데, Bind Class
는 다른 2가지보다 구현도 용이할 뿐만 아니라, 직렬화/역직렬화 없이 참조 타입 객체를 전달할 수도 있다. 또한 추가적인 스레드를 사용하지 않아 동기화 이슈가 발생할 가능성도 없다. 아래는 안드로이드 공식 홈페이지에서 추출한 Binder Class
를 직접 구현한 사례이다.
class LocalService : Service() {
// Binder given to clients.
private val binder = LocalBinder()
// Random number generator.
private val mGenerator = Random()
/** Method for clients. */
val randomNumber: Int
get() = mGenerator.nextInt(100)
/**
* Class used for the client Binder. Because we know this service always
* runs in the same process as its clients, we don't need to deal with IPC.
*/
inner class LocalBinder : Binder() {
// Return this instance of LocalService so clients can call public methods.
fun getService(): LocalService = this@LocalService
}
override fun onBind(intent: Intent): IBinder {
return binder
}
}
class BindingActivity : Activity() {
private lateinit var mService: LocalService
private var mBound: Boolean = false
/** Defines callbacks for service binding, passed to bindService(). */
private val connection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
// We've bound to LocalService, cast the IBinder and get LocalService instance.
val binder = service as LocalService.LocalBinder
mService = binder.getService()
mBound = true
}
override fun onServiceDisconnected(arg0: ComponentName) {
mBound = false
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main)
}
override fun onStart() {
super.onStart()
// Bind to LocalService.
Intent(this, LocalService::class.java).also { intent ->
bindService(intent, connection, Context.BIND_AUTO_CREATE)
}
}
override fun onStop() {
super.onStop()
unbindService(connection)
mBound = false
}
/** Called when a button is clicked (the button in the layout file attaches to
* this method with the android:onClick attribute). */
fun onButtonClick(v: View) {
if (mBound) {
// Call a method from the LocalService.
// However, if this call is something that might hang, then put this request
// in a separate thread to avoid slowing down the activity performance.
val num: Int = mService.randomNumber
Toast.makeText(this, "number: $num", Toast.LENGTH_SHORT).show()
}
}
}
'여럿의 앱 프로세스에서 클라이언트와 서비스 간 통신'을 수행할 때, 첫 번째로 고려하면 좋은 솔루션이 Messenger Class
의 사용이다. Messenger Class
는 여럿 앱 프로세스가 보내는 요청을 단일 스레드 큐에 담고 이를 순차적 처리한다. 반면, AIDL
는 멀티스레딩을 고려해야할 경우 적합하다.
참고 : https://developer.android.com/develop/background-work/services/bound-services?hl=ko#Creating
아래는 Messenger Class
를 사용한 안드로이드 공식 홈페이지 샘플 코드이다.
/** Command to the service to display a message. */
private const val MSG_SAY_HELLO = 1
class MessengerService : Service() {
/**
* Target we publish for clients to send messages to IncomingHandler.
*/
private lateinit var mMessenger: Messenger
/**
* Handler of incoming messages from clients.
*/
internal class IncomingHandler(
context: Context,
private val applicationContext: Context = context.applicationContext
) : Handler() {
override fun handleMessage(msg: Message) {
when (msg.what) {
MSG_SAY_HELLO ->
Toast.makeText(applicationContext, "hello!", Toast.LENGTH_SHORT).show()
else -> super.handleMessage(msg)
}
}
}
/**
* When binding to the service, we return an interface to our messenger
* for sending messages to the service.
*/
override fun onBind(intent: Intent): IBinder? {
Toast.makeText(applicationContext, "binding", Toast.LENGTH_SHORT).show()
mMessenger = Messenger(IncomingHandler(this))
return mMessenger.binder
}
}
class ActivityMessenger : Activity() {
/** Messenger for communicating with the service. */
private var mService: Messenger? = null
/** Flag indicating whether we have called bind on the service. */
private var bound: Boolean = false
/**
* Class for interacting with the main interface of the service.
*/
private val mConnection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
// This is called when the connection with the service has been
// established, giving us the object we can use to
// interact with the service. We are communicating with the
// service using a Messenger, so here we get a client-side
// representation of that from the raw IBinder object.
mService = Messenger(service)
bound = true
}
override fun onServiceDisconnected(className: ComponentName) {
// This is called when the connection with the service has been
// unexpectedly disconnected—that is, its process crashed.
mService = null
bound = false
}
}
fun sayHello(v: View) {
if (!bound) return
// Create and send a message to the service, using a supported 'what' value.
val msg: Message = Message.obtain(null, MSG_SAY_HELLO, 0, 0)
try {
mService?.send(msg)
} catch (e: RemoteException) {
e.printStackTrace()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main)
}
override fun onStart() {
super.onStart()
// Bind to the service.
Intent(this, MessengerService::class.java).also { intent ->
bindService(intent, mConnection, Context.BIND_AUTO_CREATE)
}
}
override fun onStop() {
super.onStop()
// Unbind from the service.
if (bound) {
unbindService(mConnection)
bound = false
}
}
}
여럿 앱 프로세스들이 요청을 보낼 경우, 안드로이드 플랫폼은 자체적으로 관리하는 스레드 풀 내에서 해당 요청들에 적절한 스레드를 할당하고 해당 요청들이 병럴적으로 들어온다. 즉, 멀티스레드적으로 요청 처리 시, AIDL
방식을 고려한다. 하지만 이 방식은 Race Condition등의 문제를 추가적으로 고려해야하며, .aidl
파일에 인터페이스를 정의해야하기에 좀 더 수고가 드는 방식이기도 하다. 아래 또한 공식 홈페이지 코드이다.
참고 : https://developer.android.com/develop/background-work/services/aidl?hl=ko
class RemoteService : Service() {
override fun onCreate() {
super.onCreate()
}
override fun onBind(intent: Intent): IBinder {
// Return the interface.
return binder
}
private val binder = object : IRemoteService.Stub() {
override fun getPid(): Int {
return Process.myPid()
}
override fun basicTypes(
anInt: Int,
aLong: Long,
aBoolean: Boolean,
aFloat: Float,
aDouble: Double,
aString: String
) {
// Does nothing.
}
}
}
private const val BUMP_MSG = 1
class Binding : Activity() {
/** The primary interface you call on the service. */
private var mService: IRemoteService? = null
/** Another interface you use on the service. */
internal var secondaryService: ISecondary? = null
private lateinit var killButton: Button
private lateinit var callbackText: TextView
private lateinit var handler: InternalHandler
private var isBound: Boolean = false
/**
* Class for interacting with the main interface of the service.
*/
private val mConnection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
// This is called when the connection with the service is
// established, giving us the service object we can use to
// interact with the service. We are communicating with our
// service through an IDL interface, so get a client-side
// representation of that from the raw service object.
mService = IRemoteService.Stub.asInterface(service)
killButton.isEnabled = true
callbackText.text = "Attached."
// We want to monitor the service for as long as we are
// connected to it.
try {
mService?.registerCallback(mCallback)
} catch (e: RemoteException) {
// In this case, the service crashes before we can
// do anything with it. We can count on soon being
// disconnected (and then reconnected if it can be restarted)
// so there is no need to do anything here.
}
// As part of the sample, tell the user what happened.
Toast.makeText(
this@Binding,
R.string.remote_service_connected,
Toast.LENGTH_SHORT
).show()
}
override fun onServiceDisconnected(className: ComponentName) {
// This is called when the connection with the service is
// unexpectedly disconnected—that is, its process crashed.
mService = null
killButton.isEnabled = false
callbackText.text = "Disconnected."
// As part of the sample, tell the user what happened.
Toast.makeText(
this@Binding,
R.string.remote_service_disconnected,
Toast.LENGTH_SHORT
).show()
}
}
/**
* Class for interacting with the secondary interface of the service.
*/
private val secondaryConnection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
// Connecting to a secondary interface is the same as any
// other interface.
secondaryService = ISecondary.Stub.asInterface(service)
killButton.isEnabled = true
}
override fun onServiceDisconnected(className: ComponentName) {
secondaryService = null
killButton.isEnabled = false
}
}
private val mBindListener = View.OnClickListener {
// Establish a couple connections with the service, binding
// by interface names. This lets other applications be
// installed that replace the remote service by implementing
// the same interface.
val intent = Intent(this@Binding, RemoteService::class.java)
intent.action = IRemoteService::class.java.name
bindService(intent, mConnection, Context.BIND_AUTO_CREATE)
intent.action = ISecondary::class.java.name
bindService(intent, secondaryConnection, Context.BIND_AUTO_CREATE)
isBound = true
callbackText.text = "Binding."
}
private val unbindListener = View.OnClickListener {
if (isBound) {
// If we have received the service, and hence registered with
// it, then now is the time to unregister.
try {
mService?.unregisterCallback(mCallback)
} catch (e: RemoteException) {
// There is nothing special we need to do if the service
// crashes.
}
// Detach our existing connection.
unbindService(mConnection)
unbindService(secondaryConnection)
killButton.isEnabled = false
isBound = false
callbackText.text = "Unbinding."
}
}
private val killListener = View.OnClickListener {
// To kill the process hosting the service, we need to know its
// PID. Conveniently, the service has a call that returns
// that information.
try {
secondaryService?.pid?.also { pid ->
// Note that, though this API lets us request to
// kill any process based on its PID, the kernel
// still imposes standard restrictions on which PIDs you
// can actually kill. Typically this means only
// the process running your application and any additional
// processes created by that app, as shown here. Packages
// sharing a common UID are also able to kill each
// other's processes.
Process.killProcess(pid)
callbackText.text = "Killed service process."
}
} catch (ex: RemoteException) {
// Recover gracefully from the process hosting the
// server dying.
// For purposes of this sample, put up a notification.
Toast.makeText(this@Binding, R.string.remote_call_failed, Toast.LENGTH_SHORT).show()
}
}
// ----------------------------------------------------------------------
// Code showing how to deal with callbacks.
// ----------------------------------------------------------------------
/**
* This implementation is used to receive callbacks from the remote
* service.
*/
private val mCallback = object : IRemoteServiceCallback.Stub() {
/**
* This is called by the remote service regularly to tell us about
* new values. Note that IPC calls are dispatched through a thread
* pool running in each process, so the code executing here is
* NOT running in our main thread like most other things. So,
* to update the UI, we need to use a Handler to hop over there.
*/
override fun valueChanged(value: Int) {
handler.sendMessage(handler.obtainMessage(BUMP_MSG, value, 0))
}
}
/**
* Standard initialization of this activity. Set up the UI, then wait
* for the user to interact with it before doing anything.
*/
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.remote_service_binding)
// Watch for button taps.
var button: Button = findViewById(R.id.bind)
button.setOnClickListener(mBindListener)
button = findViewById(R.id.unbind)
button.setOnClickListener(unbindListener)
killButton = findViewById(R.id.kill)
killButton.setOnClickListener(killListener)
killButton.isEnabled = false
callbackText = findViewById(R.id.callback)
callbackText.text = "Not attached."
handler = InternalHandler(callbackText)
}
private class InternalHandler(
textView: TextView,
private val weakTextView: WeakReference<TextView> = WeakReference(textView)
) : Handler() {
override fun handleMessage(msg: Message) {
when (msg.what) {
BUMP_MSG -> weakTextView.get()?.text = "Received from service: ${msg.arg1}"
else -> super.handleMessage(msg)
}
}
}
}