Android Bound Service 是一個 client-server 的架構。它讓 Android 元件(clients)可以 bind Service(server)來傳送請求,甚至執行 interprocess communication(IPC)。本文章將介紹 Bound Service 基本概念。
Table of Contents
概覽
Android Service 元件可分為 Service 和 Bound Service。本文章將只介紹 Bound Service。關於 Service,請參照以下文章。
Client 呼叫 Context.bindService() 來 bind server。其中 clients 是 Android 元件,如 Activity,而 server 就是 Service。結束時,client 呼叫 Context.unbindService() 來 unbind server。當所有的 clients 都 unbind 時,系統會終止 service。
當 service 還沒有被啟動,而且一個 client 呼叫 Context.bindService() 時,系統會啟動 service,並且呼叫 Service.onCreate() 和 Service.onBind()。Service.onBind() 會回傳一個 IBinder。系統會 cache 這個 IBinder。之後,其他的 clients 呼叫 Context.bindService() 時,系統不會再啟動 service,也不會呼叫 Service.onBind(),而是將 cache 的 IBinder 傳給 clients。
Bound Service 有兩種:
- Local Bound Service:由同一個 app 的其他元件建立的 long-standing connection。
- Remote Bound Service:由其他 app 建立的 long-standing connection。
Local Bound Service
Local Bound Service 指的是 client 和 server 都在同一個 app 的同一個 process。這樣的情況和 Background Service 很像。不同的是 Local Bound Service 需要實作 IBinder 介面,而 client 可以呼叫 IBinder 的 public methods。
DownloadBoundService 有兩個 inner class,一個是 ServiceHandler,另一個是 DownloadBinder。ServiceHandler 繼承 Handler 來處理檔案下載,而 DownloadBinder 實作 Binder,並且提供一個 service
讓 clients 直接可以存取 DownloadBoundService 物件。Client 可以直接呼叫 startDownload()
來開始檔案下載工作。
DownloadBoundActivity 在 onStart()
中 bind service。當它要觸發檔案下載時,它直接呼叫 DownloadBoundService.startDownload()
即可。然後在 onStop()
中呼叫 unbindService()
。如果是 Background Service 的話,那就要呼叫 startCommand() 來觸發檔案下載。相較之下,就比較不那麼地方便。
package com.waynestalk.androidboundserviceexample import android.app.Service import android.content.Intent import android.os.* import android.util.Log class DownloadBoundService : Service() { companion object { const val RESULT_CODE_UPDATE_PROGRESS = 7 const val RESULT_PROGRESS = "progress" private const val TAG = "DownloadBoundService" } inner class ServiceHandler(looper: Looper) : Handler(looper) { override fun handleMessage(msg: Message) { Log.d(TAG, "handleMessage: msg.arg1=${msg.arg1} => ${Thread.currentThread()}") for (i in 1..10) { try { Thread.sleep(1000) val resultData = Bundle().apply { putInt(RESULT_PROGRESS, i * 10) } receiver?.send(RESULT_CODE_UPDATE_PROGRESS, resultData) } catch (e: InterruptedException) { e.printStackTrace() } } } } inner class DownloadBinder : Binder() { val service: DownloadBoundService get() = this@DownloadBoundService } private val binder = DownloadBinder() private var looper: Looper? = null private var serviceHandler: ServiceHandler? = null private var receiver: ResultReceiver? = null override fun onCreate() { Log.d(TAG, "onCreate => ${Thread.currentThread()}") val handlerThread = HandlerThread("DownloadService", Process.THREAD_PRIORITY_BACKGROUND) handlerThread.start() looper = handlerThread.looper serviceHandler = ServiceHandler(handlerThread.looper) } override fun onDestroy() { super.onDestroy() Log.d(TAG, "onDestroy => ${Thread.currentThread()}") } override fun onBind(intent: Intent): IBinder { Log.d(TAG, "onBind => ${Thread.currentThread()}") return binder } fun startDownload(receiver: ResultReceiver) { this.receiver = receiver serviceHandler?.obtainMessage()?.let { message -> message.arg1 = 0 serviceHandler?.sendMessage(message) } } }
package com.waynestalk.androidboundserviceexample import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.ServiceConnection import android.os.Bundle import android.os.Handler import android.os.IBinder import android.os.ResultReceiver import android.util.Log import androidx.appcompat.app.AppCompatActivity import com.waynestalk.androidboundserviceexample.databinding.DownloadBoundActivityBinding class DownloadBoundActivity : AppCompatActivity() { companion object { private const val TAG = "DownloadBoundActivity" } private var _binding: DownloadBoundActivityBinding? = null private val binding: DownloadBoundActivityBinding get() = _binding!! private var service: DownloadBoundService? = null private var isBound = false private val connection = object : ServiceConnection { override fun onServiceConnected(className: ComponentName, binder: IBinder) { Log.d(TAG, "onServiceConnected: className=${className} => ${Thread.currentThread()}") val serviceBinder = binder as DownloadBoundService.DownloadBinder service = serviceBinder.service isBound = true } override fun onServiceDisconnected(className: ComponentName) { service = null isBound = false } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) _binding = DownloadBoundActivityBinding.inflate(layoutInflater) setContentView(binding.root) binding.downloadWithBoundService.setOnClickListener { if (isBound) { service?.startDownload(UpdateReceiver(Handler(mainLooper))) } else { Log.d(TAG, "Service is not bound yet") } } } override fun onStart() { super.onStart() val intent = Intent(this, DownloadBoundService::class.java) bindService(intent, connection, Context.BIND_AUTO_CREATE) } override fun onStop() { super.onStop() if (isBound) { unbindService(connection) isBound = false } } inner class UpdateReceiver constructor(handler: Handler) : ResultReceiver(handler) { override fun onReceiveResult(resultCode: Int, resultData: Bundle) { super.onReceiveResult(resultCode, resultData) if (resultCode == DownloadBoundService.RESULT_CODE_UPDATE_PROGRESS) { val progress = resultData.getInt(DownloadBoundService.RESULT_PROGRESS) Log.d(TAG, "progress=" + progress + " => " + Thread.currentThread()) binding.progress.text = "$progress %" } } } }
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.waynestalk.androidboundserviceexample"> <application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.AndroidBoundServiceExample" tools:targetApi="31"> <activity android:name=".MainActivity" android:exported="true" android:label="@string/app_name" android:theme="@style/Theme.AndroidBoundServiceExample.NoActionBar"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <meta-data android:name="android.app.lib_name" android:value="" /> </activity> <activity android:name=".DownloadBoundActivity" /> <service android:name=".DownloadBoundService" /> </application> </manifest>
Remote Bound Service
Remote Bound Service 指的是 client 和 server 在不同的 process,甚至是不同的 app。這就是 interprocess communication(IPC)。
Remote Bound Service 提供兩種讓 client 和 server 溝通的方式,一種是使用 Messenger,另一種是使用 AIDL。在大部分的情況我們應該都會用 Messenger,因為實作起來簡單許多。如果使用 AIDL 的話,實作比較麻煩一點,而且我們還要處理 multithreading 的情況。所以本文章中只會介紹如何使用 Messenger。
在 DownloadRemoteBoundService 中,我們宣告一個 Messenger,並且在 onBind()
中回傳 messenger.binder
。之後 client 就會拿到這個 binder。
在 handleMessage()
中,每當收到一個 Message 時,如果 client 有設定 Message.replyTo 一個 Messenger 的話,service 可以透過它將結果回傳給 client。
在 DownloadRemoteBoundActivity 中,我們可以看到,當要向 service 送出請求時,它將參數放到一個 Message 中,並且設定 Message.replyTo 一個 Messenger。
最後,在 AndroidManifest.xml 中宣告 server,並且在屬性 android:process 中設定 process 的名稱。如果沒有設定的話,那 service 和 activity 就會在同一個 process 中執行。
package com.waynestalk.androidboundserviceexample import android.app.Service import android.content.Intent import android.os.* import android.util.Log class DownloadRemoteBoundService : Service() { companion object { const val CMD_UPDATE = 1 const val RES_UPDATE_PROGRESS = 2 const val RES_UPDATE_COMPLETE = 3 private const val TAG = "RemoteBoundService" } inner class IncomingHandler(looper: Looper) : Handler(looper) { override fun handleMessage(msg: Message) { Log.d( TAG, "handleMessage: msg.what=${msg.what} => pid=${Process.myPid()}, ${Thread.currentThread()}" ) when (msg.what) { CMD_UPDATE -> { for (i in 1..10) { try { Thread.sleep(1000) if (!send(msg.replyTo, RES_UPDATE_PROGRESS, i * 10)) return } catch (e: InterruptedException) { e.printStackTrace() send(msg.replyTo, RES_UPDATE_COMPLETE, 0) return } } send(msg.replyTo, RES_UPDATE_COMPLETE, 1) } else -> super.handleMessage(msg) } } private fun send(replyTo: Messenger, command: Int, result: Int): Boolean { return try { val resultMsg = Message.obtain(null, command, result, 0) replyTo.send(resultMsg) true } catch (e: RemoteException) { // The client is dead. e.printStackTrace() false } } } private var looper: Looper? = null private lateinit var messenger: Messenger override fun onCreate() { Log.d(TAG, "onCreate => pid=${Process.myPid()}, ${Thread.currentThread()}") val handlerThread = HandlerThread("DownloadRemoteService", Process.THREAD_PRIORITY_BACKGROUND) handlerThread.start() looper = handlerThread.looper messenger = Messenger(IncomingHandler(handlerThread.looper)) } override fun onDestroy() { Log.d(TAG, "onDestroy => pid=${Process.myPid()}, ${Thread.currentThread()}") } override fun onBind(intent: Intent): IBinder? { return messenger.binder } }
package com.waynestalk.androidboundserviceexample import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.ServiceConnection import android.os.* import android.util.Log import androidx.appcompat.app.AppCompatActivity import com.waynestalk.androidboundserviceexample.databinding.DownloadRemoteBoundActivityBinding class DownloadRemoteBoundActivity : AppCompatActivity() { companion object { private const val TAG = "RemoteBoundActivity" } inner class IncomingHandler(looper: Looper) : Handler(looper) { override fun handleMessage(msg: Message) { when (msg.what) { DownloadRemoteBoundService.RES_UPDATE_PROGRESS -> { val progress = msg.arg1 Log.d( TAG, "progress=$progress => pid=${Process.myPid()} ${Thread.currentThread()}" ) binding.progress.text = "$progress %" } DownloadRemoteBoundService.RES_UPDATE_COMPLETE -> { unbindService(connection) } else -> super.handleMessage(msg) } } } private var _binding: DownloadRemoteBoundActivityBinding? = null private val binding: DownloadRemoteBoundActivityBinding get() = _binding!! private var service: Messenger? = null private var isBound = false private var messenger: Messenger? = null private val connection = object : ServiceConnection { override fun onServiceConnected(className: ComponentName, binder: IBinder) { service = Messenger(binder) messenger = Messenger(IncomingHandler(mainLooper)) isBound = true } override fun onServiceDisconnected(className: ComponentName) { service = null messenger = null isBound = false } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) _binding = DownloadRemoteBoundActivityBinding.inflate(layoutInflater) setContentView(binding.root) binding.downloadWithRemoteBoundService.setOnClickListener { if (isBound) { val msg = Message.obtain(null, DownloadRemoteBoundService.CMD_UPDATE, 0, 0) msg.replyTo = messenger try { service?.send(msg) } catch (e: RemoteException) { // The service is crashed. e.printStackTrace() } } else { Log.d(TAG, "Service is not bound yet") } } } override fun onStart() { super.onStart() val intent = Intent(this, DownloadRemoteBoundService::class.java) bindService(intent, connection, Context.BIND_AUTO_CREATE) } override fun onStop() { super.onStop() if (isBound) { unbindService(connection) isBound = false } } }
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.waynestalk.androidboundserviceexample"> <application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.AndroidBoundServiceExample" tools:targetApi="31"> <activity android:name=".MainActivity" android:exported="true" android:label="@string/app_name" android:theme="@style/Theme.AndroidBoundServiceExample.NoActionBar"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <meta-data android:name="android.app.lib_name" android:value="" /> </activity> <activity android:name=".DownloadRemoteBoundActivity" /> <service android:name=".DownloadRemoteBoundService" android:exported="true" android:process=":downloadRemote"> <intent-filter> <action android:name="com.waynestalk.androidboundserviceexample.Download" /> </intent-filter> </service> </application> </manifest>
以上的範例中,client 和 server 是在同一個 app 但不同的 process 中執行。下面的範例中,我們會在另外一個 app 中對 DownloadRemoteBoundService 送出請求。
再回去看一下 AndroidManifest.xml 中宣告 service 的地方。我們還有設定屬性 android:exported 為 true 來公開給其他的 app 呼叫。並且宣告 intent-filter,這樣其他的 app 才能透過宣告的 action 來呼叫 service。
在以下範例中,我們建立一個 intent,並設定 service 的 action 和 package。然後呼叫 bindService() 並且傳入 intent 即可。
package com.waynestalk.androidboundserviceclientexample import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.ServiceConnection import android.os.* import android.util.Log import androidx.appcompat.app.AppCompatActivity import com.waynestalk.androidboundserviceclientexample.databinding.MainActivityBinding class MainActivity : AppCompatActivity() { companion object { const val CMD_UPDATE = 1 const val RES_UPDATE_PROGRESS = 2 const val RES_UPDATE_COMPLETE = 3 private const val TAG = "RemoteBoundActivity" } inner class IncomingHandler(looper: Looper) : Handler(looper) { override fun handleMessage(msg: Message) { when (msg.what) { RES_UPDATE_PROGRESS -> { val progress = msg.arg1 Log.d( TAG, "progress=$progress => pid=${Process.myPid()} ${Thread.currentThread()}" ) binding.progress.text = "$progress %" } RES_UPDATE_COMPLETE -> unbindService(connection) else -> super.handleMessage(msg) } } } private var _binding: MainActivityBinding? = null private val binding: MainActivityBinding get() = _binding!! private var service: Messenger? = null private var isBound = false private var messenger: Messenger? = null private val connection = object : ServiceConnection { override fun onServiceConnected(className: ComponentName, binder: IBinder) { service = Messenger(binder) messenger = Messenger(IncomingHandler(mainLooper)) isBound = true } override fun onServiceDisconnected(className: ComponentName?) { service = null messenger = null isBound = false } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) _binding = MainActivityBinding.inflate(layoutInflater) setContentView(binding.root) binding.downloadWithRemoteBoundService.setOnClickListener { if (isBound) { val msg = Message.obtain(null, CMD_UPDATE, 0, 0) msg.replyTo = messenger try { service?.send(msg) } catch (e: RemoteException) { // The service is crashed. e.printStackTrace() } } else { Log.d(TAG, "Service is not bound yet") } } } override fun onStart() { super.onStart() val intent = Intent("com.waynestalk.androidboundserviceexample.Download") intent.setPackage("com.waynestalk.androidboundserviceexample") bindService(intent, connection, Context.BIND_AUTO_CREATE) } override fun onStop() { super.onStop() if (isBound) { unbindService(connection) isBound = false } } }
結語
Bound Service 讓我們可以利用 IBinder 簡單地和 Service 溝通。此外,它還提供 IPC,這讓 app 可以提供一些功能給其他的 app 使用。