[안드로이드] 컴포넌트 - 서비스개요와 ForegroundService

hee09·2021년 11월 11일
0
post-thumbnail

참조
안드로이드 developer - Services overview
개발자분 블로그
틀린 부분을 댓글로 남겨주시면 수정하겠습니다..!!

서비스 작성 방법

서비스는 백그라운드 작업을 위한 컴포넌트이며 화면과 상관없이 장시간 동안 처리해야 하는 업무를 구현할 때 사용합니다. 이 때, 시간이 오래 걸리는 업무라도 화면이 스마트폰에 떠 있는 동안만 진행되어야 한다면, 서비스가 아닌 액티비티로 작성하는게 좋습니다. 결국, 서비스는 다른 앱이 사용자 화면을 점유하고 있더라도 앱의 프로세스가 살아서 계속 무언가 작업을 수행해야 할 때 사용하는 컴포넌트입니다.

서비스 컴포넌트를 이용하는 대표적인 예로는 음악 플레이어 앱과 채팅 앱등이 있습니다. 이 두 예시는 모두 화면을 안 보고 있거나 다른 앱을 사용하더라도 음악이 플레이되거나 채팅을 위한 데이터의 통신을 해야하므로 서비스로 구현해야 합니다. 이외에도 화면과는 전혀 상관 없지만, 앱이 백그라운드에서 무언가를 계속 감지하거나 수행해야 하는 업무가 있다면 서비스 컴포넌트로 구현합니다.


서비스 타입

서비스는 세 가지의 타입이 있습니다.

  1. Foreground
    foreground 서비스는 사용자가 인식할 수 있는 작업을 수행합니다. 예를 들면, 음악앱은 오디오를 재생하기 위하여 foreground 서비스를 이용합니다. foreground 서비스는 Notification을 보여주어야만 합니다. foreground 서비스는 사용자가 앱과 상호작용(사용하지) 않을 때도 계속해서 작동해야 합니다.

foreground 서비스를 사용한다고 하면 서비스가 작동하고 있음을 사용자가 알 수 있게 알림을 보여주어야만 하는데 이 알림은 서비스가 멈추거나 foreground에서 서비스가 제거되기전까지 해제할 수 없습니다.

참조
많은 경우에, WorkManager를 이용하는 것이 foreground service를 직접적으로 이용하는 것보다 더 좋습니다.

  1. Background
    background 서비스는 사용자가 직접 인식하지 못하는 작업을 수행합니다.

참조
API Level 26 이상에서, 시스템은 앱이 실행되고(foreground) 있지 않을 때 background 서비스를 실행하는 것에 제한이 있습니다. 예를 들어, background에서 위치 정보에 접근을 할 수 없습니다. 대신, WorkManager를 이용해 업무를 관리해야 합니다.

  1. Bound(묶다, 맺다)
    서비스는 앱의 컴포넌트가 bindService()를 호출한다면 이 컴포넌트에 바인딩됩니다. bound된 서비스는 클라이언트 - 서버 인터페이스를 제공합니다. 이 인터페이스는 요청을 보내고 결과를 받고 심지어 IPC와 함께 프로세스간의 통신도 가능합니다. bound된 서비스는 다른 앱의 컴포넌트가 이 서비스에 바인딩 되어 있는 동안에 실행됩니다. 여러 개의 컴포넌트가 동시에 서비스에 바인딩될 수 있는데, 이 컴포넌트들이 바인딩이 해제되면 서비스는 destroy 됩니다.

서비스 구현

서비스는 액태비티와 마찬가지로 생명주기 메서드를 가집니다.

  • onCreate() : 시스템이 최초로 서비스를 만들 때 수행합니다. 만약 서비스가 이미 작동중이라면 이 메서드는 호출되지 않습니다.

  • onStartCommand() : 액티비티와 같은 다른 컴포넌트에서 서비스가 시작되도록 startService()를 호출한다면 이 메서드가 호출됩니다. 만약 이것을 구현했다면 서비스가 완료될 때 stopSelf() 또는 stopService()를 호출하여 서비스를 중지하는 것은 개발자의 몫입니다. 만약 binding을 제공하길 원한다면 이 메서드를 구현할 필요가 없습니다. startService()를 통해 service가 시작된다면 stopSelf()를 호출하거나 다른 컴포넌트에서 stopService()를 호출하기 전까지 서비스는 계속해서 수행됩니다.
    만약 서비스가 startService()를 통해 실행중일 때, 종료하지 않고 다시 서비스를 시작한다면 onStartCommand()가 계속해서 호출됩니다.

  • onBind() : 다른 컴포넌트가 서비스와 함께 바인드되기를 원할 때 bindService()를 호출하면 이 메서드가 호출됩니다. 바인딩을 원한다면 IBinder를 구현하는 클래스를 리턴해야 합니다. 혹은 바인딩을 원하지 않더라도 구현해야하며 이때는 null을 리턴해주면 됩니다.

  • onDestroy() : 서비스가 더이상 사용되지 않거나 destroy되었을 때 이 메서드가 호출됩니다.


AndroidManifest.xml

서비스는 컴포넌트 클래스이므로 AndroidManifest.xml 파일에 등록해야 합니다.
태그는 <service>를 사용합니다.

<manifest ... >
  ...
  <application ... >
      <service android:name=".PlayService" />
      ...
  </application>
</manifest>

만약 서비스를 자신의 앱에서만 사용할 것이라면 android:exported 속성을 false로 주면 됩니다. 그러면 다른 앱에서 서비스를 실행하지 못합니다.

참조
보안을 위해서 서비스를 시작할 때 명시적 인텐트를 사용해야 합니다. 서비스를 시작하기 위해 암시적 인텐트를 사용하는 것은 보안에 위험합니다. 왜냐하면 어떤 인텐트에 서비스가 반응할지 모르고, 사용자는 어떤 서비스가 시작한지 모르기 때문입니다. Android 5.0(API Level 21) 이상에서는 bindService()와 함께 암시적 인텐트를 사용하면 예외를 던집니다.


서비스 시작하기

서비스는 Service라는 클래스를 상속받아 작성합니다. 중요한 것은 서비스를 상속받아 클래스를 작성할 때, 그 안에 새로운 스레드를 작성하는 것입니다. 서비스는 앱의 메인 스레드를 기본으로 사용하기에 앱의 성능에 영향을 줄 수 있기 때문입니다.

class HelloService : Service() {

    private var serviceLooper: Looper? = null
    private var serviceHandler: ServiceHandler? = null

    // Handler that receives messages from the thread
    private inner class ServiceHandler(looper: Looper) : Handler(looper) {

        override fun handleMessage(msg: Message) {
            // Normally we would do some work here, like download a file.
            // For our sample, we just sleep for 5 seconds.
            try {
                Thread.sleep(5000)
            } catch (e: InterruptedException) {
                // Restore interrupt status.
                Thread.currentThread().interrupt()
            }

            // Stop the service using the startId, so that we don't stop
            // the service in the middle of handling another job
            stopSelf(msg.arg1)
        }
    }

    override fun onCreate() {
        // Start up the thread running the service.  Note that we create a
        // separate thread because the service normally runs in the process's
        // main thread, which we don't want to block.  We also make it
        // background priority so CPU-intensive work will not disrupt our UI.
        HandlerThread("ServiceStartArguments", Process.THREAD_PRIORITY_BACKGROUND).apply {
            start()

            // Get the HandlerThread's Looper and use it for our Handler
            serviceLooper = looper
            serviceHandler = ServiceHandler(looper)
        }
    }

    override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
        Toast.makeText(this, "service starting", Toast.LENGTH_SHORT).show()

        // For each start request, send a message to start a job and deliver the
        // start ID so we know which request we're stopping when we finish the job
        serviceHandler?.obtainMessage()?.also { msg ->
            msg.arg1 = startId
            serviceHandler?.sendMessage(msg)
        }

        // If we get killed, after returning from here, restart
        return START_STICKY
    }

    override fun onBind(intent: Intent): IBinder? {
        // We don't provide binding, so return null
        return null
    }

    override fun onDestroy() {
        Toast.makeText(this, "service done", Toast.LENGTH_SHORT).show()
    }
}

위의 예제는 onStartCommand()에서 들어오는 모든 호출을 처리하고 백그라운드 스레드에서 실행되는 Handler에 작업을 게시합니다. onStartCommand()에서는 integer 값을 리턴해야만 합니다. 이 값은 시스템이 서비스를 중단하는 경우 시스템이 서비스를 어떻게 할지 방법을 나타냅니다.
(메모리 부족으로 서비스가 종료되는 경우등..)

  • START_NOT_STICKY
    시스템이 서비스를 중단하더라도 pending intent가 없는 경우 재생성되지 않습니다.

  • START_STICKY
    시스템이 서비스를 중단한다면 서비스를 재생성하지만 마지막 intent를 onStartCommand()의 인자로 다시 전달하지는 않습니다. 이는 살아는 있지만 별다른 동작이 없는 음악앱과 같은 곳에 적합합니다.

  • START_REDELIVER_INTENT
    시스템이 서비스를 중단한다면 서비스를 재생성하고 서비스에 전달되었던 마지막 인텐트를 onStartCommand()의 인자로 전달합니다. 이는 파일을 다운로드하는 것과 같이 즉시 재개해야 하는 작업을 수행하는 서비스에 적합합니다.


Foreground services

Foreground 서비스는 사용자가 알아차릴 수 있는 작업을 수행합니다.

Foreground 서비스는 status bar notification을 보여주기에 사용자들은 앱이 foreground에서 작업중이고 시스템의 리소스를 사용중인 것을 압니다. 알림은 서비스가 중단되거나 foreground로 부터 서비스가 제거되기 전까지 해제될(사라질) 수 없습니다.

foreground 서비스를 이용한 예제는 아래와 같이 있습니다.

  • foreground 서비스로 음악을 재생하고 있는 음악앱이 있습니다. 알림은 현재 재생되고 있는 노래를 보여줍니다.

  • foreground 서비스로 사용자가 달리는 것을 기록하는 피트니스 앱이 있습니다. 알림은 사용자가 현재 세션동안 이동한 거리를 보여줍니다.

권한 요청

Android 9(API Level 28) 이상부터는 foreground 서비스를 사용하기 위해서 FOREGROUND_SERVICE 권한이 필요합니다. normal permission으로 단지 선언하기만 하면 됩니다.

<manifest xmlns:android="http://schemas.android.com/apk/res/android" ...>

    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>

    <application ...>
        ...
    </application>
</manifest>

Foreground service 시작하기

foreground 서비스를 실행하기 위해서 intent를 생성 후 startForegroundService()를 호출합니다. API Level 26 이상에서는 앱이 포그라운드 서비스를 만들어야 하는 경우 startForegroundService()를 호출해야 합니다. 그리고 서비스가 만들어지고나서 서비스는 5초 이내에 startForeground()를 호출해야 합니다.

val intent = Intent(...) // Build the intent for the service
applicationContext.startForegroundService(intent)

그 후 대부분 service의 onStartCommand() 코드 안에서 service가 foreground에서 작동하도록 startForeground()를 호출합니다. 매개변수로는 알림을 식별할 수 있는 값과 알림 객체 자체를 넘기면 됩니다.

val pendingIntent: PendingIntent =
        Intent(this, ExampleActivity::class.java).let { notificationIntent ->
            PendingIntent.getActivity(this, 0, notificationIntent, 0)
        }

val notification: Notification = Notification.Builder(this, CHANNEL_DEFAULT_IMPORTANCE)
        .setContentTitle(getText(R.string.notification_title))
        .setContentText(getText(R.string.notification_message))
        .setSmallIcon(R.drawable.icon)
        .setContentIntent(pendingIntent)
        .setTicker(getText(R.string.ticker_text))
        .build()

// Notification ID cannot be 0.
startForeground(ONGOING_NOTIFICATION_ID, notification)

제한
Android 12 이상의 앱에서는 background있는 동안 foreground 서비스를 실행할 수 없습니다. 만약 background에 있는 동안 foreground 서비스를 실행한다면 특별한 경우를 제외하고 ForegroundServiceStartNotAllowedException 에러가 발생합니다.


Foreground 서비스 종료하기

foreground 서비스를 제거하기 위해서 stopForeground()를 호출하면 됩니다. 이 메서드는 boolean 값을 인자로 가지는데 이는 상태바에서 알림이 제거되었는지를 나타냅니다.

만약 서비스를 foreground에서 수행하는 동안 service를 종료한다면, 알림도 사라집니다.



ForegroundService 예제 코드

profile
되새기기 위해 기록

0개의 댓글