プロセスが同一の場合の例

大抵の場合アプリケーション内に Service を置く場合は必要に迫られない限りアプリケーションと同一プロセスとすると思われる。 この場合 Fragment 側から Service を操作したい場合は bindService を使用し Binder 経由で操作を行うのが一般的だ。

あまり意味のない例で恐縮であるが、例えば以下のような「現在時刻のタイムスタンプを返すサービス」を実装したものとする:

/**
 * 現在時刻を返すサービス.
 */
class MyService : Service() {

    inner class MyBinder : Binder() {
        fun getTime() = Date().time
    }

    /** Binder インスタンス. */
    private val mBinder = MyBinder()

    override fun onBind(intent: Intent?): IBinder {
        Log.d(javaClass.simpleName, "onBind start.")
        return mBinder
    }

    override fun onUnbind(intent: Intent?): Boolean {
        Log.d(javaClass.simpleName, "onUnbind start.")
        return super.onUnbind(intent)
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        Log.d(javaClass.simpleName, "onStartCommand start.")

        // TODO 本当は何か UI に関係ない部分で待機するような処理が入るはず

        return super.onStartCommand(intent, flags, startId)
    }

    override fun onCreate() {
        super.onCreate()
        Log.d(javaClass.simpleName, "onCreate start.")
    }

    override fun onDestroy() {
        Log.d(javaClass.simpleName, "onDestroy start.")
        super.onDestroy()
    }
}

上記の場合 onBind() でサービス内で定義されている MyBinder を返却しているが、クライアント (Fragment) 側からこのインスタンスを使用してサービスの機能を利用する。 よって、例えば「ボタンを押したらサービスからデータを取ってきて画面に表示する」といった実装を行う場合は以下のようなコードになる:

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

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

/** Binder. */
private var mBinder: MyService.MyBinder? = null

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

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

    // ボタン押下でサービスからデータ取得し表示する
    button.setOnClickListener { text.text = mBinder?.getTime()?.toString() }
}

override fun onDestroy() {
    unbindService(mConnection)
    super.onDestroy()
}

この例だと bindService() しかしていないので Service 側としては onCreate()onBind() は呼ばれるが onStartCommand() はコールされないことに注意する。 onStartCommand() はあくまで startService() した時に呼ばれる処理となっている。

ServiceConnection.onServiceConnected() 内で Service 側で定義したクラスである MyService.MyBinder にキャストしているが、これが可能なためにこの MyService.MyBinder に対し自由にメソッドやフィールドを追加することにより Service に対して好きに処理を行うことができるようになる。

Service から任意のタイミングでクライアントに処理を行わせたい場合

上記の Binder の例はあくまでクライアント側から任意のタイミングで Service に対し処理を行わせたい場合に使用できる。 逆方向である Service からクライアントに対し処理を行わせる場合であるが、以下の 3 通りが考えられる:

今は EventBus などのライブラリを使用すると任意の位置からイベントを伝搬できるのでとても便利だ。 ここでは 1 番目の Binder を用いて実現する方法に関して書く。

例えばサービスが起動された 5 秒後にクライアント側で実装されたコールバックを実行する例を示す。 サービス側の実装として、まずクライアント側で実装すべきコールバックのインターフェースを開示する:

class MyService : Service() {

    /** サービスで 5 秒後に実行されるコールバック. */
    interface Callback {
        fun doIt()
    }

    inner class MyBinder : Binder() {
        fun getTime() = Date().time

        // クライアント側からコールバックを渡す為のメソッドを追加
        fun set(callback: Callback?) {
            mCallback = callback
        }
    }

    /** Binder インスタンス. */
    private val mBinder = MyBinder()

    /** クライアント側で実装されたコールバック */
    private var mCallback: Callback? = null

    override fun onBind(intent: Intent?): IBinder = mBinder

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        // サービス実行から 5 秒後にコールバック実行
        Handler().postDelayed({
            mCallback?.doIt()
        }, 5000)
        return Service.START_STICKY_COMPATIBILITY
    }
}

後は以下のようにクライアント側でコールバックの実装を行い、サービスのコネクションが張れたタイミングでコールバックを Binder を通してサービス側に渡してやれば良い:

private val mCallback = object : Callback {
    override fun doIt() {
        button.text = "サービス側から呼ばれた"
    }
}

/** サービスとの bind 時の接続を行う. */
private val mConnection = object : ServiceConnection {
    override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
        mBinder = binder as MyService.MyBinder
        mBinder?.set(mCallback)  // サービス側に定義したコールバックを渡す
    }

    override fun onServiceDisconnected(name: ComponentName?) {
        mBinder?.set(null)
        mBinder = null
    }
}

/** Binder. */
private var mBinder: MyService.MyBinder? = null

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)

    // ボタン押下でサービスからデータ取得し表示する
    button.setOnClickListener { text.text = mBinder?.getTime()?.toString() }
}

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

これで 5 秒後にボタンに「サービス側から呼ばれた」という文字列が表示される。

プロセスを分けるとどうなるか

さて Android の場合同一アプリ内であっても別のプロセスを使用した Service を定義することができる。 これは AndroidManitest.xml に以下のように定義を加えることにより実現できる:

<service
    android:name=".MyService"
    android:process=":remote"/>

android:process=":remote" の部分であるが、:remote の箇所は別に :hoge などの他の名前でも良い。 このようにして先ほどのコードを実行してみると ServiceConnection.onServiceConnected()MyService.MyBinder にキャストしている箇所で ClassCastException がスローされるようになる:

private val mConnection = object : ServiceConnection {
    override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
        mBinder = binder as MyService.MyBinder  // 駄目. binder は BinderProxy が渡ってくる!!
    }
}

デバッグ実行で見てみると分かるが Service 側で渡した Binder の参照がそのまま渡ってくるのではなく BinderProxy というプロキシクラスのインスタンスが渡ってくるようになる。 その為このようにキャスト前提の実装にしてしまうと失敗する。 また EventBus などのライブラリもプロセス間通信には使用することができない。

この場合は公式にある通り Messenger を使用するわけだが、長くなったのでそのコード例は次回とする。