プロセスが同一の場合の例
大抵の場合アプリケーション内に 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 通りが考えられる:
Binder
を介してクライアント側からコールバックを渡す- EventBus や RxJava などのライブラリを使用して任意のイベントを飛ばす
BroadcastReceiver
をクライアント側に実装してService
からのsendBroadcast()
を受け取る
今は 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
を使用するわけだが、長くなったのでそのコード例は次回とする。