Hanbit the Developer

Android Document | About Services 본문

Mobile/Android

Android Document | About Services

hanbikan 2023. 11. 20. 18:05

What is Services?

  • An application component that can perform long-running operations in the background.
  • 예시: 네트워크 트랜잭션 제어, 음악 재생, 파일 입출력 수행, content provider와 상호작용
  • No UI
  • 컴포넌트가 서비스에 바인드되어 상호작용할 수 있음
  • IPC 수행 가능
  • 메인쓰레드에서 작동함(ANR 주의할 것)

Types of Services

Foreground

  • 사용자가 알아차릴 수 있는 작업을 수행한다.
  • 예시: 음악 플레이어
  • Notification을 표시해야만 한다.(사용자가 알아차릴 수 있도록)
WorkManager API는 작업을 유연하게 스케줄링하고 작업을 foreground service로 돌릴 수 있습니다.

Background

  • 사용자가 직접 알아차릴 수 없는 작업을 수행한다.
  • 예시: 백그라운드에서 스토리지 압축

Bound

  • bindService()를 통해 컴포넌트가 서비스를 바인드할 때, 그 서비스가 ‘바운드되었다’고 말한다.
  • 바운드 서비스는 클라이언트-서버 인터페이스를 통해 컴포넌트가 서비스와 상호작용하고, 서비스로 요청을 보내고, 서비스로부터 결과를 받을 수 있다. IPC를 통해서도 처리할 수 있다.
  • 바운드 서비스는 다른 애플리케이션 컴포넌트가 여기에 바운드된 경우에만 동작한다. 여러 컴포넌트가 하나의 서비스에 바인드할 수 있으며 연결된 모두가 unbind하면 해당 서비스는 종료된다.

하나의 서비스는 started service(onStartCommand()를 구현한 경우)이면서 동시에 bound service(onBind()를 구현한 경우)일 수 있다.

어떤 종류의 서비스이든, 애플리케이션 컴포넌트는 Intent를 시작함으로써 서비스를 사용할 수 있다.(애플리케이션 컴포넌트가 액티비티를 시작할 때와 같음)

서비스를 매니페스트 파일에서 private하게 선언하여 다른 애플리케이션으로부터의 액세스를 방지할 수도 있다.

Choosing Between a service and a thread

서비스는 main thread에서 돌아가며, 원하는 경우 새 쓰레드를 생성해서 사용하면 된다.

예를 들어 앱 상호작용과 동시에 음악을 재생하고 싶다면, onCreate()에서 쓰레드를 생성하고 onStart()에서 음악을 재생하고, onStop에서 음악을 종료하면 된다. 코루틴을 사용하는 것도 권장된다.

The basics

onStartCommand()

다른 컴포넌트가 이 서비스 시작을 요청할 때, 시스템은 startService()를 호출함으로써 해당 메소드를 invoke한다. 해당 함수를 구현할 때, 작업을 마치면 stopSelf()나 stopService()를 호출하여 서비스를 중지해야 한다.

onBind()

다른 컴포넌트가 이 서비스와 바인드를 원할 때, 시스템이 bindService()를 호출함으로써 해당 메소드를 invoke한다. 해당 함수를 구현할 때, IBinder를 리턴함으로써 클라이언트가 해당 서비스와 상호작용할 수 있도록 해야한다. 반드시 해당 메소드를 구현해야 하며, 바인딩을 원치 않을 경우 null을 반환하면 된다.

onCreate()

서비스가 생성될 때 시스템에 의해 invoke되며, one-time setup procedures를 수행하면 된다.

onDestroy()

서비스가 더이상 사용되지 않으며 파괴되고 있을 때 invoke된다. 쓰레드, 리스너, 리시버 등의 자원을 clean up하는 작업을 두면 된다.

안드로이드 시스템은 메모리가 부족하고, 사용자가 포커스를 둔 액티비티의 시스템 자원을 회복해야 할 때만 서비스를 중단시킨다. 서비스가 장기간 돌아가는 경우, 시간이 지남에 따라 시스템이 해당 서비스를 killed 하기 쉬워지게 된다.

시스템이 서비스를 종료시킨 경우, 리소스가 복구되면 재시작된다.(onStartCommand()의 반환값에도 의존함)

Declearing a service in the manifest

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

이외에도 권한과 같은 프로퍼티를 <service> 요소 내에 포함시킬 수 있다.

어플을 배포한 뒤에 name을 바꾸면, 해당 서비스를 시작하거나 바인드하는 명시적 인텐트의 의존성 때문에 breaking code의 리스크가 있다.

보안을 위해, 서비스에 인텐트 필터를 선언하지 말고, 서비스를 명시적 인텐트로 시작하라.

android:exported=false를 통해 서비스를 앱에서만 사용할 수 있게 ensure할 수 있다.

android:description을 통해 사용자에게 해당 서비스를 설명함으로써 사용자가 임의로 서비스를 중단하지 않게 하라.

Creating a started service

startService()로 인텐트를 통해 서비스를 시작하면, onStartCommand()에서 해당 인텐트를 받게 된다.

안드로이드 프레임워크에서 제공하는 IntentService(Service의 서브클래스)는 worker thread를 사용하여 모든 시작 요청을 한 번에 하나씩 처리한다. Android 8 Oreo부터 Background execution limits를 도입하였기 때문에 뉴스 앱에는 권장되지 않는다. 게다가 Android 11부터 deprecated 되었기 때문에 대신 JobIntentService를 사용하라.

Extending the Service class

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()의 반환값(int)

  • START_NOT_STICKY: 시스템이 서비스를 kill하면, 서비스를 다시 생성하지 않는다.
  • START_STICKY: 시스템이 서비스를 kill하면, 서비스를 재생성하며 onStartCommand()에 null intent를 전달한다.(단, 서비스를 시작하는 PendingIntent——특정 이벤트가 발생할 때까지 실행을 대기하는 인텐트——가 있을 경우에는 예외이다.)
  • START_REDELIVER_INTENT: 시스템이 서비스를 kill하면, 서비스를 재생성하며 onStartCommand()에 가장 최근의 인텐트를 전달한다.

Starting a service

startService(Intent(this, HelloService::class.java))
startForegroundService(Intent(this, HelloService::class.java))

결과를 다시 이를 생성한 컴포넌트로 보내고 싶다면 broadcast에 대한 PendingIntent를 생성하여 전달해야 한다. 해당 서비스는 PendingIntent를 생성할 때 래퍼런스한 broadcast를 통해서 결과를 전달한다.

// 클라이언트 (예: Activity 또는 Fragment에서의 호출)
val serviceIntent = Intent(this, MyService::class.java)

// 브로드캐스트를 수신할 PendingIntent 생성
val pendingResult = PendingIntent.getBroadcast(
    this,
    0, // 요청 코드
    Intent(this, MyResultReceiver::class.java),
    PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)

// 서비스에 PendingIntent를 전달
serviceIntent.putExtra("resultPendingIntent", pendingResult)

// 서비스 시작
startService(serviceIntent)

시작 요청을 여러 번 보내면 onStartCommand()가 여러 번 호출된다. 하지만 중단할 때는 한 번의 호출(stopSelf() or stopService())만 요구된다.

Stopping a service

서비스가 동시에 여러 onStartCommand() 요청을 처리하는 경우, 한 요청의 처리가 끝났다고 해서 서비스를 중단해서는 안 된다. 이유는 새로운 시작 요청을 이미 받았을 수 있기 때문이며, 첫 번째 요청이 끝날 즈음에 서비스를 중단하면 두 번째 요청도 종료될 수 있다. 이 문제를 피하기 위해, stopSelf(int)를 사용하여 서비스 중단 요청이 항상 가장 최근의 시작 요청을 기반으로 하도록 할 수 있다. 즉, stopSelf(int)를 호출할 때, 해당 중단 요청과 일치하는 시작 요청의 ID(즉, onStartCommand()로 전달된 startId)를 전달한다. 그러면 서비스가 stopSelf(int)를 호출하기 전에 새로운 시작 요청을 받을 경우 ID가 일치하지 않아 서비스가 중단되지 않는다.

Creating a bound service

  • 바운드 서비스는, 컴포넌트와 서비스 사이에 인터렉션이 필요하거나, IPC를 통해 내 앱의 기능을 다른 애플리케이션에 노출시키고자 할 때 쓰인다.
  • bindService()를 통해 컴포넌트에서 바운드 서비스를 바인드할 수 있다.(startService()로 실행하는 것은 일반적으로 허용되지 않음)
  • 소통 매개체인 IBinder 인터페이스를 반환하는 onBind()를 구현해야 한다.
  • IBinder 인터페이스를 구현하여 커뮤니케이션 스펙을 정의하라.
  • 동시에 여러 클라이언트에서 바인드할 수 있다.
  • 바운드하는 컴포넌트가 없으면 자동으로 중단되므로 자체적으로 멈추지 말라.
  • 클라이언트는 unbindService()를 통해 바인드를 중단할 수 있다.

Sending notifications to the user

서비스가 실행 중일 때, snackbar나 status bar notification으로 이벤트를 알릴 수 있다.(status bar notification이 권장됨)

예를 들어, 파일을 다운로드하는 서비스가 다운로드를 마치면 알림을 띄우고 이걸 누르면 다운받은 파일로 이동할 수 있다.

Managing the lifecycle of a service

라이프싸이클이 액티비티보다는 간단하지만, 백그라운드에서 작업을 하기 때문에 더욱 신중해질 필요가 있다.

  • Started Service
    • startService()를 통해 생성된다.
    • stopSelf(), stopService()를 통해 중단된다. 이후 시스템이 이를 제거한다.
  • Bound Service
    • bindService()를 통해 생성된다.
    • unbindService()를 통해 연결이 닫힌다.
    • 연결된 모든 클라이언트가 unbind할 경우, 시스템이 이를 제거한다.

둘은 완전히 분리되지 않는다. 만약 한 서비스가 startService()로 시작되고 bindService()를 통해 바인드 되었다고 가정해보자. stopSelf()를 호출하더라도 모든 바인드가 끊긴 뒤에야 서비스가 중단된다.

Implementing the lifecycle callbacks

class ExampleService : Service() {
    private var startMode: Int = 0             // indicates how to behave if the service is killed
    private var binder: IBinder? = null        // interface for clients that bind
    private var allowRebind: Boolean = false   // indicates whether onRebind should be used

    override fun onCreate() {
        // The service is being created
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        // The service is starting, due to a call to startService()
        return startMode
    }

    override fun onBind(intent: Intent): IBinder? {
        // A client is binding to the service with bindService()
        return binder
    }

    override fun onUnbind(intent: Intent): Boolean {
        // All clients have unbound with unbindService()
        return allowRebind
    }

    override fun onRebind(intent: Intent) {
        // A client is binding to the service with bindService(),
        // after onUnbind() has already been called
    }

    override fun onDestroy() {
        // The service is no longer used and is being destroyed
    }
}
액티비티와는 달리, 각 메소드의 슈퍼클래스에서의 구현을 호출하지 않아도 된다.(@CallSuper가 없다.)

  • Entire Lifetime of Service
    • onCreate(): 초기 셋업
    • onDestroy(): 리소스 릴리즈
  • Active Lifetime of Service
    • onStartCommand() or onBind()
    • onUnbind()
서비스에는 stopSelf() 또는 stopService()에 상응하는, 액티비티의 onStop() 같은 콜백은 없다.

Summary

서비스는 배경에서 장시간 실행되는 애플리케이션 구성 요소입니다. 세 가지 유형의 서비스가 있습니다: 포어그라운드(사용자에게 알림을 통해 표시됨), 백그라운드(사용자에게 보이지 않음), 바운드(다른 컴포넌트와 바인딩되어 상호작용 가능). 서비스는 메인 스레드에서 실행되며, onStartCommand()와 onBind() 메소드로 시작되거나 바인딩됩니다. onCreate()와 onDestroy()는 서비스의 생성과 소멸을 처리합니다.

서비스는 스레드와 비교되며, 주로 백그라운드에서 작업을 수행하기 위해 사용됩니다. onStartCommand()는 다른 컴포넌트에서 서비스를 시작할 때 호출되며, onBind()는 서비스와 바인딩할 때 사용됩니다. 서비스는 매니페스트 파일에서 선언되며, 시작된 서비스는 startService()로 시작되고, 바운드 서비스는 bindService()로 시작됩니다. 서비스의 라이프사이클 관리는 중요하며, onCreate(), onStartCommand(), onBind(), onUnbind(), onDestroy() 등의 콜백을 통해 이루어집니다.

서비스는 사용자에게 상태 바 알림을 통해 통지할 수 있으며, 서비스의 라이프사이클은 onCreate()와 onDestroy()로 정의되며, 활성 상태는 onStartCommand()나 onBind()로 관리됩니다. 서비스는 stopSelf() 또는 stopService()를 통해 중단될 수 있습니다.

Example

Started Service에서 쓰레드를 생성하여 파일을 다운로드하고 완료되면 status bar notification으로 알리는 예시 코드입니다.

import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import java.io.BufferedInputStream
import java.io.FileOutputStream
import java.net.URL

class DownloadService : Service() {

    private val notificationChannelId = "DownloadServiceChannel"

    override fun onCreate() {
        super.onCreate()
        createNotificationChannel()
    }

    override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
        // Run the download in a new thread to avoid blocking the main thread
        Thread {
            downloadFile("<http://example.com/file.zip>", "downloaded_file.zip")
            showDownloadCompleteNotification()
            stopSelf()
        }.start()
        return START_NOT_STICKY
    }

    private fun downloadFile(urlString: String, outputFileName: String) {
        // ... Skipped ...
    }

    private fun showDownloadCompleteNotification() {
        val notificationBuilder = NotificationCompat.Builder(this, notificationChannelId)
            .setContentTitle("Download Complete")
            .setContentText("Your file has been downloaded.")
            .setSmallIcon(android.R.drawable.stat_sys_download_done)

        val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        notificationManager.notify(1, notificationBuilder.build())
    }

    private fun createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val name = "Download Service Channel"
            val descriptionText = "Notification Channel for Download Service"
            val importance = NotificationManager.IMPORTANCE_DEFAULT
            val channel = NotificationChannel(notificationChannelId, name, importance).apply {
                description = descriptionText
            }
            val notificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
            notificationManager.createNotificationChannel(channel)
        }
    }

    override fun onBind(intent: Intent): IBinder? {
        return null
    }
}

 

https://developer.android.com/guide/components/services

 

서비스 개요  |  Android 개발자  |  Android Developers

서비스 개요 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Service는 백그라운드에서 오래 실행되는 작업을 수행할 수 있는 애플리케이션 구성 요소이며 사

developer.android.com