Background Service : Android 6.0 (api level 23) 도입된 배터리 절약 기능 Doze 모드에 의해 시스템에 의해 Kill 당할 수 있다.
Foreground Service : 사용자가 인지할 수 있는 작업을 수행하며, 이 과정에서 반드시 알림을 통해 서비스가 실행 중임을 사용자에게 표시해야 합니다. 이러한 서비스는 사용자가 명시적으로 중지하지 않는 한 계속 실행되며, 중요한 작업이 중단되지 않도록 시스템에 의해 강제로 종료되지 않습니다.
Foreground Service는 상태 표시줄에 알림을 표시하며, 이 알림을 통해 사용자는 서비스가 활성화된 상태에서 시스템 리소스를 소비하고 있다는 것을 인지할 수 있습니다.
일반적인 Foreground Service 사용 사례로는 다음과 같은 예가 있습니다:
- 음악 플레이어 앱: 포그라운드 서비스에서 음악을 재생하며, 알림에는 현재 재생 중인 곡 정보가 표시됩니다.
- 피트니스 앱: 사용자의 운동을 기록하는 앱에서 포그라운드 서비스를 사용하여 사용자의 이동 기록을 저장하고, 권한 요청을 할 때 알림을 통해 진행 상황을 사용자에게 표시할 수 있습니다.
https://developer.android.com/develop/background-work/services/foreground-services?hl=ko
Foreground Service를 구현할 때, 간혹 android.app.ForegroundServiceStartNotAllowedException 오류가 발생할 수 있습니다.
Foreground Service는 startForegroundService()로 시작한 후, startForeground()를 호출해야 하며, startForegroundService() 호출 후 5초 이내에 startForeground()가 호출되지 않으면 시스템에 의해 서비스가 강제 종료되고 해당 오류가 발생합니다.
이 문제를 해결하기 위해 기존에 잘 사용하지 않던 bindService()를 사용하여 서비스를 바인딩하는 방식으로 구현했습니다.
bindService() 로 서비스 바인딩시 콜백 함수들의 실행 순서는 다음과 다음과 같다.
onCreate() -> onBind() -> onStartCommand()
1. 서비스의 생명주기
일반적으로 서비스 생성시 Service 를 상속받는 클래스를 만듭니다. 해당 서비스는 개발자가 직접 생명주기를 관리해야 합니다. 하지만 이방법은 액티비티와 서비스의 생명주기가 달라서 간혹 오류가 발생합니다.
class StepCounterService : Service()
변경
class StepCounterService : LifecycleService()
해당 문제 해결을 위해 LifecycleService() 사용하였습니다.
Android의 서비스 컴포넌트에 Lifecycle 기능을 추가한 클래스로, Android Jetpack의 Lifecycle-aware components(수명 주기 인식 컴포넌트)를 사용할 수 있게 해줍니다. 이 클래스는 Service의 하위 클래스이며, 서비스의 수명 주기를 Lifecycle 객체로 노출하여 옵저버들이 서비스의 수명 주기 변화를 인식할 수 있습니다.
- 수명 주기 관리: LifecycleService는 서비스의 수명 주기 상태를 Lifecycle 클래스를 통해 관리하며, 이를 통해 수명 주기를 인식하는 컴포넌트(예: LiveData, ViewModel)를 서비스 내에서 쉽게 사용할 수 있습니다.
- Observer 등록: LifecycleService를 통해 LifecycleObserver를 서비스에 등록하여 서비스의 생명 주기 상태 변화를 감지할 수 있습니다.
LifecycleService는 기본적으로 Service 클래스를 확장하므로, Service의 주요 메서드를 동일하게 사용할 수 있습니다. 추가적으로, getLifecycle() 메서드를 통해 Lifecycle 객체를 얻을 수 있습니다.
- getLifecycle(): 서비스의 Lifecycle 객체를 반환합니다. 이 객체를 통해 LifecycleObserver를 등록하거나 수명 주기 상태를 확인할 수 있습니다.
다음은 LifecycleService를 사용하는 간단한 예시입니다.
class StepCounterService : LifecycleService() {
private val observer = MyObserver()
override fun onCreate() {
super.onCreate()
// LifecycleObserver 등록
lifecycle.addObserver(observer)
}
override fun onDestroy() {
super.onDestroy()
// 서비스 종료 시 LifecycleObserver 해제
lifecycle.removeObserver(observer)
}
}
class StepObserver : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun onServiceStart() {
// 서비스가 시작될 때 수행할 작업
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun onServiceStop() {
// 서비스가 중지될 때 수행할 작업
}
}
LifecycleService의 주요 장점
- 서비스에서 수명 주기 인식 컴포넌트 사용 가능: LifecycleService는 LiveData, ViewModel, LifecycleObserver와 같은 컴포넌트를 서비스 내에서 사용할 수 있게 해줍니다.
- 수명 주기 관리의 간소화: 수명 주기를 별도로 추적하고 관리하지 않아도, LifecycleObserver를 통해 상태 변화에 따른 처리가 가능해집니다.
2. onBind() 구현
- 보통 onBind()를 구현하지 않는데 액티비티와 직접적으로 상호작용하지 않거나 제한적이기 때문에 굳이 구현할 필요가 없다.
- 하지만 5초 이내에 startForeground() 시작하기 위하여 구현하였고 Activity or Fragment or Receiver 에서 bindService()를 통해 서비스를 바인딩하고 IBinder 인터페이스를 통해 클라이언트와 서비스간에 상호작용을 하도록 하였다.
- Activity or Fragment or Receiver 직접적으로 서비스와 상호작용을 위해서는 onBind() 구현이 필수적이다.
- 클라이언트가 서비스의 메서드를 호출할 수 있도록 localBinder 반환해 준다.
override fun onBind(intent: Intent): IBinder {
super.onBind(intent)
StepServiceConnection.bindService()
startStepServiceBind()
}
private fun startStepServiceBind() {
val serviceIntent = Intent(this, StepCounterService::class.java)
ContextCompat.startForegroundService(this, serviceIntent)
}
override fun onUnbind(intent: Intent?): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
stopSelf()
}
stopForeground(true)
}
internal inner class LocalBinder: Binder() {
fun getService(): StepCounterService = this@StepCounterService
}
3. ServiceConnection 구현
object StepServiceConnection: ServiceConnection {
private var stepCounterService: StepCounterService? = null
private var isBound = false
private set
// 서비스를 바인드합니다.
fun bindService() {
isBound = true
}
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
val binder = service as StepCounterService.LocalBinder
this.stepCounterService = binder.getService()
bindService()
}
override fun onServiceDisconnected(name: ComponentName?) {
stepCounterService = null
isBound = false
}
}
4. onStartCommand() 구현
- onStartCommand() 서비스가 startService() or startForegrundService() 로 명시적으로 시작시 호출됨
- onBind() 에서 startForegroundService() 호출하기 때문에 onStartCommand()가 호출되고 startForeground() 통해 서비스가 5초안에 시작된다. 이렇게 ForegroundServiceStartNotAllowedException 를 방지하였다.
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ServiceCompat.startForeground(this, NOTIFICATION_CHANNEL_TYPE_STEP_COUNT, getNotification(this), FOREGROUND_SERVICE_TYPE_HEALTH)
} else {
ServiceCompat.startForeground(this, NOTIFICATION_CHANNEL_TYPE_STEP_COUNT, getNotification(this), 0)
}
} catch (e: Exception) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && e is ForegroundServiceStartNotAllowedException) {
e.printStackTrace()
}
}
return START_STICKY
}