Android

서비스 (2) : 바인딩된 서비스

까망사과 2022. 8. 28. 01:00

바인딩된 서비스

앱 컴포넌트가 bindService()를 호출하여 연결하는 서비스.

 

서비스에서 바인딩을 허용하려면 다음 두 가지가 필요하다.

  • IBinder
    클라이언트와 서비스가 상호작용할 때 사용하는 인터페이스.
  • ServiceConnection
    클라이언트와 서비스의 연결 상태를 모니터링한다.

 

클라이언트는 여러 개가 동시에 한 서비스에 바인딩될 수 있다. 서비스는 바인딩되어 있는 클라이언트가 하나라도 있는 이상 중지되지 않고, 모든 클라이언트가 바인딩을 해제해야 중지되고 소멸한다.

 

Binder 클래스 상속받기

서비스를 클라이언트와 같은 앱, 프로세스에서만 사용한다면 Binder 클래스를 상속받아 사용할 수 있다. 이는 IBinder 인터페이스를 구현하는 클래스로, 클라이언트와 서비스 사이를 연결하는 인터페이스 역할을 한다.

 

Binder를 사용하는 과정은 다음과 같다.

  1. ServiceConnection 인터페이스를 구현한다. 다음 두 콜백 메서드를 구현하는 것이 중요하다.
    • onServiceConnected(name: ComponentName!, service: IBinder!): Unit
      클라이언트와 서비스가 연결되었을 때 호출되는 콜백.

      파라미터 타입 설명
      name ComponentName! 연결된 서비스 클래스의 이름.
      service IBinder! 서비스의 onBind() 콜백에서 반환된 객체. 포함된 메서드 등을 사용하여 서비스와 상호작용할 수 있다.
    • onServiceDisconnected(name: ComponentName!): Unit
      클라이언트와 서비스의 연결이 예상치 못하게 해제되었을 때 호출되는 콜백. 일반적으로 서비스가 중지되거나 비정상 종료되었을 때 호출된다.

      파라미터 타입 설명
      name ComponentName! 연결이 해제된 서비스 클래스의 이름.
  2. 클라이언트에서 bindService()를 호출한다. 파라미터로 서비스를 시작하는 Intent와 위에서 구현한 ServiceConnection 객체를 전달해야 한다.
    • bindService(service: Intent!, conn: ServiceConnection, flags: Int): Boolean

      파라미터 타입 설명
      service Intent! 바인딩할 서비스를 실행하는 명시적 인텐트.
      conn ServiceConnection 서비스와의 연결 상태를 모니터링하는 객체.
      flags Int 바인딩 동작 옵션을 나타내는 플래그. 0 또는 Context.BIND_* 상수를 조합한 값을 사용할 수 있다.
      반환값 타입 설명
      Boolean 서비스가 존재하고 클라이언트가 바인딩될 때 요구되는 권한을 가지고 있으면 true를 반환한다. 반대로 서비스가 존재하지 않거나 클라이언트가 바인딩될 때 요구되는 권한을 가지고 있지 않으면 false를 반환한다.
  3. 서비스에서 다음 중 하나를 만족하는 Binder 인스턴스를 생성한다.
    • 클라이언트가 호출할 수 있는 공용(public) 메서드를 포함한다.
    • 현재 Service 인스턴스(클라이언트가 호출할 수 있는 공용 메서드를 포함)를 반환한다.
    • 서비스(클라이언트가 호출할 수 있는 공용 메서드를 포함)가 호스팅하는 다른 클래스의 인스턴스를 반환한다.
  4. 서비스의 onBind() 콜백 메서드에서 이 Binder 인스턴스가 반환된다.
    • onBind(intent: Intent!): IBinder?

      파라미터 타입 설명
      intent Intent! 클라이언트가 서비스에 바인딩하기 위해 bindService()에 전달하는 인텐트.
      반환값 타입 설명
      IBinder? 클라이언트와 서비스가 상호작용할 때 사용하는 인터페이스
  5. 반환된 BinderbindService()에 전달했던 ServiceConnection 객체의 onServiceConnection() 콜백에 전달된다. 클라이언트는 이 Binder에 포함된 메서드를 사용하여 서비스와 상호작용할 수 있다

 

다음은 Binder를 구현하여 클라이언트가 서비스의 메서드를 사용할 수 있게 하는 서비스 예시다.

class LocalService : Service() {
    // 클라이언트에 제공되는 Binder
    private val binder = LocalBinder()

    // 난수 생성기
    private val mGenerator = Random()

    // 클라이언트가 사용할 서비스의 메서드
    val randomNumber: Int
        get() = mGenerator.nextInt(100)

    // 클라이언트에 제공되는 Binder 클래스
    // 이 서비스는 항상 클라이언트와 같은 프로세스에서 실행되기 때문에 IPC를 고려하지 않아도 된다.
    inner class LocalBinder : Binder() {
        // 현재 LocalService 인스턴스를 반환하여 클라이언트가 서비스의 공용 메서드를 사용할 수 있다.
        fun getService(): LocalService = this@LocalService
    }

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

 

LocalService.getService()는 현재 LocalService 인스턴스를 반환한다. 이 반환된 인스턴스를 사용하여 클라이언트에서 LocalService의 메서드를 호출할 수 있다. 위 예시에서는 LocalService.getRandomNumber()를 호출할 수 있다.

 

아래는 버튼을 누를 때 LocalService.getRandomNumber()를 호출하는 액티비티 예시다.

class BindingActivity : Activity() {
    private lateinit var mService: LocalService
    private var mBound: Boolean = false

    // 서비스를 바인딩하는 데 사용하는 콜백을 정의한다. 이는 bindService()에 전달된다.
    private val connection = object : ServiceConnection {

        override fun onServiceConnected(className: ComponentName, service: IBinder) {
            // LocalService에 바인딩되었으므로 IBinder를 캐스팅하여 LocalService 인스턴스를 가져온다.
            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()
        // LocalService에 바인딩
        Intent(this, LocalService::class.java).also { intent ->
            bindService(intent, connection, Context.BIND_AUTO_CREATE)
        }
    }

    override fun onStop() {
        super.onStop()
        // LocalService에서 바인딩 해제
        unbindService(connection)
        mBound = false
    }

    // 버튼을 클릭할 때 호출됨 (레이아웃 파일에서 버튼의 android:onClick 속성 사용)
    fun onButtonClick(v: View) {
        if (mBound) {
            // LocalService의 메서드를 호출한다.
            // 이 메서드를 완료하는데 오래 걸린다면 ANR을 방지하기 위해 별도 스레드에서 호출해야 한다.
            val num: Int = mService.randomNumber
            Toast.makeText(this, "number: $num", Toast.LENGTH_SHORT).show()
        }
    }
}

 

Messenger 사용하기

서비스를 다른 프로세스에서 사용해야 할 경우 Messenger를 통해 상호작용 인터페이스를 제공할 수 있다.

 

Messenger를 사용하는 방법을 간략히 나타내면 다음과 같다.

  1. 서비스에서 클라이언트의 요청을 수신하는 Handler를 구현한다.
  2. 서비스에서 Handler를 사용하여 Messenger를 생성한다.
    • Messenger(target: Handler!)

      파라미터 타입 설명
      target Handler! 현재 Messenger가 전송하는 Message를 수신하는 Handler.
    • Messenger(target: IBinder!)

      파라미터 타입 설명
      target IBinder! 현재 Messenger가 상호작용해야 하는 IBinder.
  3. Messenger가 서비스의 onBind()에서 반환될 IBinder를 생성한다.
  4. 클라이언트가 IBinder를 사용하여 Messenger를 생성하고 이를 사용하여 서비스에 Message를 전송한다.
  5. 클라이언트가 전송한 Message를 서비스의 Handler가 수신한다.

이 방법은 바인더처럼 클라이언트가 서비스의 메서드를 호출할 수 있는 것이 아니라, 서비스에게 메시지(Message 객체)를 전달하는 방식이다.

 

아래는 Messenger를 사용하는 간단한 서비스 예시다.

// 서비스가 메시지를 표시하도록 하는 메시지 코드
private const val MSG_SAY_HELLO = 1

class MessengerService : Service() {
    // 클라이언트가 IncomingHandler에게 메시지를 전송하는데 사용하는 메신저
    private lateinit var mMessenger: Messenger

    // 클라이언트가 전송하는 메시지를 처리하는 Handler
    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)
            }
        }
    }

    // 클라이언트가 서비스에 바인딩될 때 메신저에 인터페이스를 반환하면 이를 사용하여 메시지를 전송한다.
    override fun onBind(intent: Intent): IBinder? {
        Toast.makeText(applicationContext, "binding", Toast.LENGTH_SHORT).show()
        mMessenger = Messenger(IncomingHandler(this))
        return mMessenger.binder
    }
}

 

HandlerhandleMessage()에서 Message를 수신하고 이의 what 멤버로 메시지의 내용을 식별한다.

 

클라이언트가 할 일은 전달받은 IBinder를 사용하여 Messenger를 생성하고 send()를 호출하여 메시지를 보내는 것뿐이다. 다음은 서비스에 바인딩되어 메시지를 보내는 액티비티 예시다.

class ActivityMessenger : Activity() {
    // 서비스와 통신하는 Messenger
    private var mService: Messenger? = null

    // 액티비티가 서비스에 바인딩되었는지 여부
    private var bound: Boolean = false

    // 서비스의 메인 인터페이스와 상호작용하는 클래스
    private val mConnection = object : ServiceConnection {

        // 클라이언트와 서비스가 연결되었을 때 호출된다.
        // 서비스의 onBind()에서 반환하는 IBinder가 전달된다.
        override fun onServiceConnected(className: ComponentName, service: IBinder) {
            // IBinder를 이용하여 서비스의 Messenger를 클라이언트로 불러온다.
            mService = Messenger(service)
            bound = true
        }

        // 클라이언트와 서비스가 완전히 연결 해제되었을 때 호출된다.
        override fun onServiceDisconnected(className: ComponentName) {
            mService = null
            bound = false
        }
    }

    fun sayHello(v: View) {
        if (!bound) return
        // 새 Message를 생성하여 서비스에 보낸다.
        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()
        // 서비스에 바인딩
        Intent(this, MessengerService::class.java).also { intent ->
            bindService(intent, mConnection, Context.BIND_AUTO_CREATE)
        }
    }

    override fun onStop() {
        super.onStop()
        // 바인딩되어 있다면 해제
        if (bound) {
            unbindService(mConnection)
            bound = false
        }
    }
}

 

바인딩된 서비스의 수명 주기

바인딩된 서비스를 사용할 때(바인딩만 허용한다고 가정) 호출되는 수명 주기 콜백 메서드는 다음과 같다.

  • onCreate(): Unit
    서비스가 생성될 때 호출된다. 초기 설정 작업을 여기서 수행할 수 있다.
  • onBind(intent: Intent!): IBinder?
    클라이언트가 바인딩될 때 호출된다. 클라이언트가 서비스와 상호작용할 때 사용하는 바인더를 반환한다.
  • onUnbind(intent: Intent!): Boolean
    클라이언트와 바인딩이 해제될 때 호출된다. 반환값은 onRebind()의 호출 여부를 의미한다.
  • onRebind(intent: Intent!): Unit
    onUnbind()가 호출된 후 새 클라이언트가 바인딩될 때 호출된다. onUnbind()true를 반환할 때만 호출된다. 이 메서드는 반환값이 없지만 클라이언트의 onServiceConnected() 콜백으로 IBinder가 전달된다.
  • onDestroy(): Unit
    서비스를 더 이상 사용하지 않아 소멸할 때 호출된다. 리소스 정리 작업을 여기서 수행할 수 있다.

 

바인딩된 서비스의 수명 주기는 다음 이미지에 설명된 것과 같이 동작한다.

 

이미지 출처 : https://developer.android.com/guide/components/bound-services#Lifecycle