Android Broadcast Receiver 元件讓 app 可以從 Android 系統或其他 apps 接收訊息,也可以傳送訊息給 app 自己的其他元件,或是其他 apps。它類似於 publish-subscribe 設計模式。本文章將介紹如何使用 Broadcast Receiver。
Table of Contents
概覽
當有系統事件發生時,Android 系統自動地廣播事件給所有監聽該事件的 apps。例如,當使用者開啟或關係飛航模式時,系統會廣播 ACTION_AIRPLANE_MODE_CHANGED 事件。
App 也可以廣播事件給所有監聽該事件的 apps。當然 App 自己也可以監聽自己廣播的事件。
接收 Broadcasts
App 有兩種方式可以接收 broadcasts,一種是註冊 manifest-declared receivers,另一種是註冊 context-declared receivers。
Manifest-Declared Receivers
Manifest-declared receivers 指的是在 AndroidManifest.xml 中註冊的 receivers。在 <receiver/> 中設定監聽事件的 class,並且在 <intent-filter/> 中指定要監聽的事件。如果事件來源是系統或是其他的 apps,還要設定 android:exported="true"
。
以下範例中,我們建立 LocaleChangedBroadcastReceiver 並繼承 BroadcastReceiver,然後 override onReceive() 即可。如果事件帶有額外資料的話,我們可以由 intent 中取得。
package com.waynestalk.broadcastreceiverexample import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.util.Log class LocaleChangedBroadcastReceiver : BroadcastReceiver() { companion object { private const val TAG = "LocaleChangedBroadcast" } override fun onReceive(context: Context, intent: Intent) { Log.d(TAG, "Received: locale changed => ${Thread.currentThread()}") } }
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <application android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.AndroidBroadcastReceiverExample" tools:targetApi="31"> <receiver android:name=".LocaleChangedBroadcastReceiver" android:exported="true"> <intent-filter> <action android:name="android.intent.action.LOCALE_CHANGED" /> </intent-filter> </receiver> </application> </manifest>
Android 提供很多系統事件,請參照 Intent constants。在 Android 8(API level 26)以上,app 不能透過 manifest-declared receivers 來接收 implicit broadcasts。Implicit broadcasts 是那些沒有指定你的 app 為目的地的 broadcasts。例如,系統事件就是 implicit broadcasts。所以你不能用 manifest-declared receivers 來接收系統事件,只有一些少數的 implicit broadcasts 除外。
當 app 從沒有被使用者執行或是被 force stop 的話,manifest-declared receivers 不會接收到事件。
如下面第一個圖,如果 app 正在執行,或是在背景,或是被使用者從背景移除,在 App info 中,Force stop 按鈕是可以按的。這表示 app 還是啟動的。這時 app 的 manifest-declared receivers 可以接收到事件。
如果使用者按下 Force stop 來停止 app,那 app 的 manifest-declared receivers 就不會再接收到事件了。當系統啟動後,如果使用者從未執行過 app,那 app 也是停止的。在某些廠牌的 Android 系統中,當使用者將 app 從背景移除時,系統會將 app force stop。
Context-Declared Receivers
Context-declared receivers 指的是在 Android 元件中,透過 Context.registerReceiver() 或是 ContextCompat.registerReceiver() 註冊的 receivers。
如下範例,我們在 MainActivity.onResume() 中註冊 AirplaneModeBroadcastReceiver。因為它監聽的是來自系統的事件,所以要設定 ContextCompat.RECEIVER_EXPORTED flag。最後,我們還要在 MainActivity.onPause() 中呼叫 Context.unregisterReceiver() 來取消註冊,以防止註冊多次。
package com.waynestalk.broadcastreceiverexample import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.widget.Toast class AirplaneModeBroadcastReceiver : BroadcastReceiver() { companion object { private const val TAG = "AirplaneModeBroadcast" } override fun onReceive(context: Context, intent: Intent) { if (intent.action != Intent.ACTION_AIRPLANE_MODE_CHANGED) return val isOn = intent.getBooleanExtra("state", false) val message = "Airplane mode is ${if (isOn) "on" else "off"}." Toast.makeText(context, message, Toast.LENGTH_LONG).show() } }
package com.waynestalk.broadcastreceiverexample import android.content.Intent import android.content.IntentFilter import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import com.waynestalk.broadcastreceiverexample.databinding.ActivityMainBinding class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private val broadcastReceiver = AirplaneModeBroadcastReceiver() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) } override fun onResume() { super.onResume() val filter = IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED) val flags = ContextCompat.RECEIVER_EXPORTED ContextCompat.registerReceiver(this, broadcastReceiver, filter, flags) } override fun onPause() { super.onPause() unregisterReceiver(broadcastReceiver) } }
Lifecycle
當系統傳送事件給 app 時,app 內監聽該事件的 BroadcastReceiver 就會被建立,並且系統會在該 process 中 main thread 中執行 BroadcastReceiver.onReceive()。當 onReceive() 執行完畢後,BroadcastReceiver 就不再是 active。系統會每次都會建立一個新的 BroadcastReceiver 來處理新的事件,所以當 onReceive() 執行完畢後,該 BroadcastReceiver 的生命週期算是結束了。
當 onReceive() 被執行時,其 hosting process 會被系統視為 foreground process。所以即使系統遭遇到 low memory 的情況時,也不會結束 app。但是當 onReceive() 結束後,如果 app 是 background process 的話,那系統可能會結束 process。
所以在 onReceive() 內,我們不應該建立 long running thread。因為當 onReceive() 一結束時,系統可能會結束 process,也會結束該 process 建立的 threads。
我們也不應該在 onReceive() 中執行 long running 的操作,因為 onReceive() 是在 main thread 中執行的。當使用者正在使用 app,而剛好 onReceive() 被執行來處理某個事件時,如果 onReceive() 花太多時間在 main thread 中處理事件的話,那對使用者體驗是不好的。
如果必須要在 onReceive() 處理 long running 的操作的話,可以用 JobScheduler 和 JobService,或是 BroadcastReceiver.goAsync()。goAsync() 告知系統需要更多的時間來處理事件(上限為10秒)。如下範例中的 onReceive(),它先呼叫 goAsync(),然後在 coroutine 中執行 long running 操作,這樣就不會 block main thread。最後要呼叫。 PendingResult.finish() 來告知系統處理結束了。
class MyBroadcastReceiver : BroadcastReceiver() { private val scope = CoroutineScope(SupervisorJob()) override fun onReceive(context: Context, intent: Intent) { val pendingResult: PendingResult = goAsync() scope.launch(Dispatchers.Default) { // ... // Must call finish() so the BroadcastReceiver can be recycled pending.finish() } } }
傳送 Broadcasts
App 可以呼叫 sendBroadcast(Intent, String) 廣播事件給自己和其他的 apps。
如下面的範例中,EchoBroadcastReceiver 監聽 com.waynestalk.echo
事件。我們在 AndroidManifest.xml 中註冊它。在 MainActivity 中,建立一個 intent,並設定要廣播 com.waynestalk.echo
事件。在 Android 8(API level 26)以上,app 不能透過 manifest-declared receivers 來接收 implicit broadcasts,所以我們在 Intent.package 設定接收者的 application package,也就是這個事件的目的地。因為是自己廣播給自己,所以就設定自己的 application package。
package com.waynestalk.broadcastreceiverexample import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.widget.Toast class EchoBroadcastReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { if (intent.action != "com.waynestalk.echo") return val message = intent.getStringExtra("message") Toast.makeText(context, "Received: $message", Toast.LENGTH_LONG).show() } }
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <application android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.AndroidBroadcastReceiverExample" tools:targetApi="31"> <receiver android:name=".EchoBroadcastReceiver" android:exported="true"> <intent-filter> <action android:name="com.waynestalk.echo" /> </intent-filter> </receiver> </application> </manifest>
package com.waynestalk.broadcastreceiverexample import android.content.Intent import android.content.IntentFilter import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import com.waynestalk.broadcastreceiverexample.databinding.ActivityMainBinding class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) binding.echoButton.setOnClickListener { val intent = Intent().apply { action = "com.waynestalk.echo" `package` = "com.waynestalk.broadcastreceiverexample" putExtra("message", "Hello Android") } sendBroadcast(intent) } } }
結語
系統事件讓 app 可以做出合適的反應,列如 app 可以監聽網路連線的改變。如果是斷線的話,app 可以顯示訊息請求使用者開啟網路連線。而且 app 不需要一直 pull 連線狀態,而是用監聽系統事件來得知網路狀態是否改變。