面倒なのでできれば相手をしたくないが

さて、前回は Service のプロセスがアプリと同一の場合、つまり通常の場合に Binder を用いて双方向でやり取りする方法に関して考察したが、今回は止むを得ない事情でプロセスを分けなければならなくなった場合に Messenger を用いてプロセス間通信を行う為の実装例に関して記載する。

Service 側の実装に関してコード例を示す:

companion object {

    /** クライアント側からの replyTo 受信. */
    val MESSAGE_RECEIVING_REPLY_TO = 1

    /** クライアントへのメッセージ送信. */
    val MESSAGE_SEND = 2
}

/** サービス側の処理を行うハンドラ. */
private class MyHandler : Handler() {

    /** クライアントの Messenger. */
    var clientMessenger: Messenger? = null

    override fun handleMessage(msg: Message?) {
        when (msg?.what) {
            MESSAGE_RECEIVING_REPLY_TO -> clientMessenger = msg.replyTo
            else -> super.handleMessage(msg)
        }
    }
}

/** ハンドラ. */
private val mHandler = MyHandler()

/** サービス側のメッセンジャ. */
private val mMessenger = Messenger(mHandler)

override fun onBind(intent: Intent?): IBinder = mMessenger.binder

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    // サービス実行から 5 秒後にコールバック実行
    Handler().postDelayed({
        val bundle = Bundle().apply { putString("message", "ほげふが") }
        mHandler.clientMessenger?.send(Message.obtain(null, MESSAGE_SEND, bundle))
    }, 5000)
    return Service.START_STICKY_COMPATIBILITY
}

こちらでもプロセスが同一の場合と同様に onBind() で IBinder を返却しているが、こちらは Messenger に対応する Binder である (後で Messenger に復元可能な Binder) ところが異なる。 mMessenger というフィールドを持っているが、これは Handler インスタンスを Messenger クラスでラップしたものである。 Handler クラスの方を拡張することによって相手から投げられたメッセージをどう処理するかを実装する。

上記の実装例では msg.replyTo をフィールドに持っているが、これは Service からみてどのクライアントの Messenger でメッセージを送信すれば良いのかを判別するためのものである。 つまり Service 側の任意のタイミングでクライアント側にメッセージを送信したい場合は以下の手順を踏む必要がある:

  1. Service の接続確立時にクライアント側から replyTo を教えるためのメッセージを Service 側の Messenger を使って送信
  2. Service 側の送信したいタイミングでフィールドに持っている replyTo を使用してクライアントに送信

上記コードでは onStartCommand() 内で 5 秒後に実際にクライアントに対してメッセージを送信している。

Activity / Fragment 側のコード

/** クライアント側の処理を行うハンドラ. */
private class MyHandler(val activity: MainActivity) : Handler() {
    override fun handleMessage(msg: Message?) {
        when (msg?.what) {
            MyService.MESSAGE_SEND -> activity.button.text = (msg.obj as Bundle).getString("message")
            else -> Log.w(javaClass.simpleName, "Unknown what: ${msg?.what}")
        }
    }
}

/** サービスとの bind 時の接続を行う. */
private val mConnection = object : ServiceConnection {
    override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
        mServiceMessenger = Messenger(binder)

        // サービス側に replyTo を介してクライアント側の Messenger を教える
        val message = Message.obtain(null, MyService.MESSAGE_RECEIVING_REPLY_TO)
        message.replyTo = mClientMessenger
        mServiceMessenger?.send(message)
    }

    override fun onServiceDisconnected(name: ComponentName?) {
        mServiceMessenger = null
    }
}

/** サービス側の Messenger. */
private var mServiceMessenger: Messenger? = null

/** クライアント側の Messenger. */
private val mClientMessenger = Messenger(MyHandler(this))

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    // サービスを bind する
    val intent = Intent(applicationContext, MyService::class.java)
    startService(intent)
    bindService(intent, mConnection, Context.BIND_AUTO_CREATE)
}

override fun onDestroy() {
    unbindService(mConnection)
    val intent = Intent(applicationContext, MyService::class.java)
    stopService(intent)
    super.onDestroy()
}

Service との接続が確立した時に渡ってきた IBinder を逆に Messenger のコンストラクタに渡すことで Service 側の Messenger を復元している。 それを使用することで Service 側にメッセージを送信することができる。 先ほど記載したように、メッセージの replyTo に Activity, Fragment 側の Messenger を詰めて送信しておく。

後は Service 側と同様にこちら側でも Handler クラスを拡張して Service から渡ってきたメッセージを処理する為のコードを書く。

まとめ

  1. Service に bindService() する
  2. Service とのコネクション確立時に Service の Messenger を使用して Service に対しクライアント側の Messenger を replyTo に詰めて送信することで教える
  3. Service 側で Handler の拡張クラスの実装によって渡ってきた replyTo を格納しておく
  4. replyTo を使用して任意のタイミングでクライアントに対しメッセージ送信
  5. クライアント側の Handler の拡張クラスでの実装で UI の表示処理などを行う

手順をしっかり理解しないと間違えそうな内容なので、やはり手軽ではないと思う。