Android Bound Service 教學

Photo by Hybrid Storytellers on Unsplash
Photo by Hybrid Storytellers on Unsplash
Android Bound Service 是一個 client-server 的架構。它讓 Android 元件(clients)可以 bind Service(server)來傳送請求,甚至執行 interprocess communication(IPC)。

Android Bound Service 是一個 client-server 的架構。它讓 Android 元件(clients)可以 bind Service(server)來傳送請求,甚至執行 interprocess communication(IPC)。本文章將介紹 Bound Service 基本概念。

完整程式碼可以在 下載。

概覽

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

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

You May Also Like
Photo by Hans-Jurgen Mager on Unsplash
Read More

Kotlin Coroutine 教學

Kotlin 的 coroutine 是用來取代 thread。它不會阻塞 thread,而且還可以被取消。Coroutine core 會幫你管理 thread 的數量,讓你不需要自行管理,這也可以避免不小心建立過多的 thread。
Read More