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 使用。









