[4대 컴포넌트] Service(2)

dwjeong·2023년 8월 30일
0

안드로이드

목록 보기
2/28
post-custom-banner

📖 서비스 타입

1. Foreground

사용자 눈에 띄는 작업 수행.

예시

음악플레이어 앱 - 현재 재생중인 노래 표시
피트니스 앱 - 달리기 이동거리 알림 표시

포그라운드 서비스는 알림을 표시하여 리소스를 소비하고 있음을 유저에게 알리며 서비스를 중지하거나 포그라운드에서 제거하지 않는 한 해제 불가. (안드로이드 13 이상부터 유저가 해제 가능)

WorkManager API를 사용하는 것이 좋음.

📝 사용자가 알림 해제

안드로이드 13 (API 레벨 33)부터 사용자가 스와이프 제스처를 통해 포그라운드 서비스와 연결된 알림 해제 가능.
사용자가 알림을 해제할 수 없도록 하려면 Notification.Builder를 사용하여 알림을 만들때
setOngoing() 메서드에 true 전달.

📝 알림을 즉시 표시하는 서비스

포그라운드 서비스에 아래 특성 중 하나 이상이 있는 경우 안드로이드 12 이상을 실행하는 기기에서도 서비스가 시작되는 즉시 관련 알림이 표시됨.

- 서비스가 액션 버튼이 포함된 알림과 연결되어 있음.
- 서비스의 포그라운드 서비스 타입이 mediaProjection, mediaPlayBack, phoneCall
- 서비스가 알림을 설정할때 setForegroundServiceBehavior()에 FOREGROUND_SERVICE_IMMEDIATE를 전달.

📝 Foreground 서비스 권한 요청

안드로이드 9 (API 레벨 28)이상을 대상으로 하고 포그라운드 서비스를 사용하는 앱일 경우
매니페스트 파일에 FOREGROUND_SERVICE 권한을 요청해야 함.

API 레벨 28 이상을 대상으로 하는 앱이 FOREGROUND_SERVICE 권한을 요청하지 않고 포그라운드 서비스를 만들려고 할 경우 SecurityException 발생

📝 포그라운드 서비스 시작

서비스 내부의 onStartCommand()에서 서비스를 포그라운드에서 실행하도록 요청

//status bar의 상태 표시줄 알림을 식별할 수 있는 양수인 정수와 알림 객체 필요

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

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)

📝 백그라운드 시작 제한

안드로이드 12 (API 레벨 31) 이상을 대상으로 하는 앱은 특별한 경우를 제외하고는 백그라운드에서 실행되는 동안 포그라운드 서비스를 시작할 수 없음.

백그라운드에서 실행되는 동안 포그라운드 서비스를 시작할 수 있는 경우

만약 한 앱이 다른 앱이 소유한 포그라운드 서비스를 시작하기 위해 Context.startForegroundService() 를 호출하는 경우 두 앱이 모두 안드로이드 12 이상을 대상으로 하는 경우에만 위의 제한이 적용됨.

📝 포그라운드에서 서비스 제거

stopForeground() 호출. 서비스가 포그라운드에서 실행되는 동안 서비스를 중지할 경우 해당 알림이 제거됨.


2. Background

사용자가 직접 인지하지 못하는 작업 수행.

예시

앱이 서비스를 사용하여 저장 공간을 압축하는 경우.
네트워크를 통해 데이터를 가져오는 경우

API 레벨 26 이상을 대상으로 하는 앱이 포그라운드에 있지 않을 때 시스템은 백그라운드 서비스 실행에 대해 제한한다. (예를 들어 대부분의 상황에서는 백그라운드에서 위치 정보에 액세스해서는 안됨.)


3. Bound

앱 컴포넌트가 bindService()를 호출하여 서비스에 바인딩.

다른 컴포넌트가 서비스와 상호 작용하고, 클라이언트 - 서버 인터페이스를 제공하여 프로세스 간 통신(IPC)을 가능하게 함.

바인딩된 서비스는 다른 앱 컴포넌트가 바인딩되어 있는 동안에만 실행되며,
여러 컴포넌트가 한번에 서비스에 바인딩될 수 있지만, 모든 컴포넌트가 바인딩 해제되면 서비스가 파괴됨.


📝 서비스에 바인딩

onBind() 콜백 메서드 구현.
onBind() 메서드는 클라이언트가 서비스와 상호 작용할 때 사용하는 인터페이스를 정의하는 IBinder 객체 반환.

일반적으로 onBind() 혹은 onStartCommand()중 하나를 구현.
두 가지 모두 구현하는 경우 - 음악 플레이어

사용자가 애플리케이션을 나가도 음악이 계속 재생되며, 사용자가 애플리케이션으로 돌아오면 
액티비티가 서비스에 바인딩하여 재생을 다시 제어할 수 있게 됨.
  • startService() - 호출 시 서비스를 무기한 실행할 수 있음.
  • bindService() - 호출 시 클라이언트가 서비스에 바인딩하도록 할 수 있음. 서비스와의 연결을 모니터링하는 ServiceConnection의 구현을 제공해야 함.
    클라이언트는 IBinder를 받기 위해 ServiceConnection 인스턴스 생성 후 이를 bindService()에 전달해야 함.

📝 IBinder 정의 방법

  1. Binder 클래스 확장
    로컬 애플리케이션에서만 서비스를 사용하고 여러 프로세스에서 동작할 필요가 없는 경우에
    클라이언트가 서비스의 공용 메서드에 직접 액세스할 수 있는 공유한 바인더 클래스 구현.

    Binder 클래스를 확장하고 onBind()에서 해당 인스턴스를 반환하여 인터페이스 생성.

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
    }
}
//onServiceConnected() 콜백을 사용하여 서비스에 바인딩

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)
            /** 명시적 인텐트를 사용하여 앱 보안을 유지해야 함.
           	 * 안드로이드 5 (API 레벨 21) 부터 암시적 인텐트로 bindService 호출 시 예외 발생 **/
        }
    }

    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()
        }
    }
}

추가 샘플 코드 1

추가 샘플 코드 2


  1. 메신저 사용
    서비스가 원격 프로세스와 통신해야하는 경우.
    여러 프로세스에서 작동하는 인터페이스가 필요한 경우.
    다양한 유형의 메시지 객체에 응답하는 핸들러 정의.
    핸들러와 클라이언트가 IBinder를 공유하여 클라이언트가 메시지 객체를 사용하여 서비스에 명령을 보냄. (IPC를 수행하는 가장 간단한 방법.)

/** 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)
            }
        }
    }

    /**
     * Messenger는 안드로이드 메신저 클래스
     */
    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
        }
    }
}

추가 샘플 코드 (서비스)

추가 샘플 코드 (클라이언트)

  1. AIDL 사용
    안드로이드 인터페이스 정의 언어.
    메신저는 단일 스레드에 모든 클라이언트 요청의 대기열을 생성하여 서비스가 요청을 한 번에 하나씩 받지만, AIDL을 이용할 경우 여러 요청을 동시에 처리할 수 있음.
    멀티스레딩이 가능해야 함.
    .aidl 파일을 생성하여 사용.


📝 바인딩 된 서비스 생명주기 관리

모든 클라이언트가 바인딩된 서비스를 해제하면 안드로이드 시스템이 서비스 삭제하며 따로 수명 주기를 관리하지 않아도 됨. (startService()를 사용하지 않았을 경우에만)

post-custom-banner

0개의 댓글