這是我對官方文檔的一個渣翻譯,兼我的學習筆記,原文在此。
Android app可以從Android系統(tǒng)和其他Android應(yīng)用發(fā)送或接收廣播信息,類似發(fā)布-關(guān)注設(shè)計模式。當關(guān)注的事件發(fā)生時就會發(fā)送廣播。舉個例子,當各種系統(tǒng)事件發(fā)生時發(fā)送廣播,比如系統(tǒng)啟動或設(shè)備開始充電。應(yīng)用也可以發(fā)送自定義廣播,例如,通知其他應(yīng)用他們可能關(guān)注的東西(比如一些新數(shù)據(jù)被下載了)。
應(yīng)用可以通過注冊來接收特定的廣播。當廣播被發(fā)送時,系統(tǒng)會自動將廣播路由到這些訂閱接收這類廣播的應(yīng)用。
通常來說,廣播可以用作跨應(yīng)用和正常用戶流之外的消息系統(tǒng)。但是,你要小心不要濫用在后臺響應(yīng)廣播和運行作業(yè)的機會,它會導(dǎo)致系統(tǒng)性能變慢。
關(guān)于系統(tǒng)廣播
各種系統(tǒng)事件發(fā)生時,系統(tǒng)會自動發(fā)送廣播,例如當系統(tǒng)切入或切出飛行模式。系統(tǒng)廣播會被發(fā)送到所有訂閱接收這個事件的應(yīng)用。
廣播信息自身被包裝在一個Intent對象中,這個對象的action字符標識了發(fā)生的事件(例如android.intent.action.AIRPLANE_MODE)。這個intent還可能包含了被捆綁了附加信息的其他域。例如,飛行模式intent包含了一個boolean類型的附加物來表示飛行模式是開還是關(guān)。
了解更多關(guān)于如何讀取intents和從intent中獲取action字符的信息,查看Intents and Intent Filters。
完整的系統(tǒng)廣播action列表,查看Android SDK中的BROADCAST_ACTIONS.TXT文件。每個廣播action關(guān)聯(lián)了一個一個常量域。例如,ACTION_AIRPLANE_MODE_CHANGED的常量值是android.intent.action.AIRPLANE_MODE。每個廣播action的文檔都可以在它關(guān)聯(lián)的常量域中獲得。
系統(tǒng)廣播的變化
Android 7.0 以及更高版本不再需要發(fā)送以下系統(tǒng)廣播。此優(yōu)化會影響所有應(yīng)用,而不僅僅是針對Android 7.0的應(yīng)用。
- ACTION_NEW_PICTURE
-
ACTION_NEW_VIDEO
針對Android 7.0 (API level 24) 以及更高版本的應(yīng)用必須要使用registerReceiver(BroadcastReceiver, IntentFilter)來注冊以下廣播。在manifest中聲明receiver不再起作用。 -
CONNECTIVITY_ACTION
從Android 8.0 (API level 26) 起,系統(tǒng)對manifest聲明的receiver增加了額外的限制。如果你的應(yīng)用針對API26或更高的版本,則不能使用manifest來聲明大多數(shù)隱式廣播的receiver(不僅僅是應(yīng)用的廣播)。
接收廣播
應(yīng)用有兩種方式接收廣播:通過Manifest-declared receivers和context-registered receivers。
Manifest-declared receivers
如果你在你的manifest中聲明了一個廣播接收器,當廣播被發(fā)送時,系統(tǒng)會啟動你的應(yīng)用(即使應(yīng)用不在運行中)。
如果你的應(yīng)用針對API level 26或更高版本,則不能在manifest中聲明隱式廣播的接收器(不僅僅是你的應(yīng)用的廣播),除了少數(shù)可以免除此限制的隱式廣播。大多數(shù)情況下,你可以使用scheduled jobs代替。
按照以下步驟在manifest中聲明廣播接收器:
- 在應(yīng)用的manifest中使用
<receiver>元素。
<receiver android:name=".MyBroadcastReceiver" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.INPUT_METHOD_CHANGED" />
</intent-filter>
</receiver>
intent filters指定了應(yīng)用要訂閱的廣播action。
- 創(chuàng)建
BroadcastReceiver的子類,并實現(xiàn)onReceive(Context, Intent)。在下面的例子中,廣播接收器輸出日志并顯示廣播內(nèi)容。
public class MyBroadcastReceiver extends BroadcastReceiver {
private static final String TAG = "MyBroadcastReceiver";
@Override
public void onReceive(Context context, Intent intent) {
StringBuilder sb = new StringBuilder();
sb.append("Action: " + intent.getAction() + "\n");
sb.append("URI: " + intent.toUri(Intent.URI_INTENT_SCHEME).toString() + "\n");
String log = sb.toString();
Log.d(TAG, log);
Toast.makeText(context, log, Toast.LENGTH_LONG).show();
}
}
當應(yīng)用安裝時,系統(tǒng)包管理器注冊了接收器。接收器就成為了應(yīng)用的獨立入口點,即使當前應(yīng)用尚未運行,系統(tǒng)也可以啟動應(yīng)用并且發(fā)送廣播。
如果接收到廣播,系統(tǒng)會創(chuàng)建一個新的BroadcastReceiver組件對象來處理每一個廣播。該對象僅在調(diào)用onReceive(Context, Intent)的期間中有效。一旦代碼從這個方法返回,系統(tǒng)就會認為這個組件不再處于活動狀態(tài)。
Context-registered receivers
根據(jù)以下步驟通過context注冊接收器:
- 創(chuàng)建
BroadcastReceiver的實例。
BroadcastReceiver br = new MyBroadcastReceiver();
- 創(chuàng)建
IntentFilter并且調(diào)用registerReceiver(BroadcastReceiver, IntentFilter)注冊接收器。
IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
filter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED);
this.registerReceiver(br, filter);
注意:要注冊本地廣播,調(diào)用
LocalBroadcastManager.registerReceiver(BroadcastReceiver, IntentFilter)。
Context-registered接收器只在注冊的context有效時接收廣播。例如,當你使用Activitycontext注冊時,你僅在activity沒有被銷毀前接收廣播。如果你使用Application context注冊,那么你只能在應(yīng)用運行時接收廣播。
- 調(diào)用
unregisterReceiver(android.content.BroadcastReceiver)停止接收廣播。要確認在你不再需要廣播時,或context不再存在時注銷接收器。
要注意你注冊和注銷接收器的位置。例如,當你在onCreate(Bundle)中使用activity context注冊接收器,那么你就要在onDestroy()中注銷,防止接收器從activity context中泄露。如果你在onResume()中注冊接收器,那么你就要在onPause()中注銷以防止多次注冊(如果你不想在暫停時接收廣播,并且它會減少不必要的系統(tǒng)開銷)。不要在onSaveInstanceState(Bundle)中注銷,因為如果用戶移回到歷史堆棧,它就不會被調(diào)用。
對進程狀態(tài)的影響
BroadcastReceiver(不論是否運行)影響了它包含的進程的狀態(tài),進而影響系統(tǒng)殺死它的可能性。例如,當一個進程執(zhí)行接收器(準確來說,是運行它的onReceive()方法中的代碼)時,它會被當做前臺進程。系統(tǒng)會一直保持進程除非內(nèi)存壓力過大。
然而,只要你的代碼從onReceive()中返回,BroadcastReceiver 將不再活動。接收器的主機進程的重要性變得和正在運行的其他應(yīng)用組件一樣。如果這個進程主機只是manifest-declared 接收器(應(yīng)用中用戶從未或最近沒有與其交互的普通組件),則從onReceive()返回時,系統(tǒng)會認為這個進程是一個優(yōu)先級很低的進程,并很有可能會殺死它,讓其他重要的進程獲得更多資源。
因為這個原因,你不能再廣播接收器中開啟長時間運行的后臺應(yīng)用。在onReceive()后,系統(tǒng)會在任意時間殺死這個進程來釋放內(nèi)存,并且,它會中止這個進程中的衍生
線程。為了避免這種情況發(fā)生,你應(yīng)該調(diào)用goAsync()(如果你想在后臺線程中多用一點時間處理廣播),或者在接收器中使用 JobScheduler來調(diào)度JobService,系統(tǒng)就會知道進程會繼續(xù)執(zhí)行有效的工作。要了解更多信息,查看Processes and Application Life Cycle。
以下片段顯示了一個BroadcastReceiver使用goAsync()來表示它在完成onReceive()后需要更多時間才能完成。當你想在你的onReceive()完成的工作用時很長,導(dǎo)致UI線程錯過一幀(大于16ms),使用后臺線程會更合適。
public class MyBroadcastReceiver extends BroadcastReceiver {
private static final String TAG = "MyBroadcastReceiver";
@Override
public void onReceive(final Context context, final Intent intent) {
final PendingResult pendingResult = goAsync();
AsyncTask<String, Integer, String> asyncTask = new AsyncTask<String, Integer, String>() {
@Override
protected String doInBackground(String... params) {
StringBuilder sb = new StringBuilder();
sb.append("Action: " + intent.getAction() + "\n");
sb.append("URI: " + intent.toUri(Intent.URI_INTENT_SCHEME).toString() + "\n");
Log.d(TAG, log);
// Must call finish() so the BroadcastReceiver can be recycled.
pendingResult.finish();
return data;
}
};
asyncTask.execute();
}
}
發(fā)送廣播
Android 為應(yīng)用發(fā)送廣播提供了三種方法。
-
sendOrderedBroadcast(Intent, String)方法在一個時間向一個接收器發(fā)送廣播。當每個接收器依次執(zhí)行時,它可以將結(jié)果發(fā)送到下一個接收器,它也可以完全中止廣播,使其不會被傳遞給其他廣播。 -
sendBroadcast(Intent)方法無序地將廣播發(fā)送給所有接收器。它被成為普通廣播。這種廣播更有效,但也意味著接收器不能讀取其他接收器的結(jié)果,發(fā)送從廣播接收到的數(shù)據(jù),或者終止廣播。 -
LocalBroadcastManager.sendBroadcast方法發(fā)送廣播給與發(fā)件人位于同一應(yīng)用中的接受者。如果你不需要跨應(yīng)用發(fā)送廣播,就使用本地廣播。它的實現(xiàn)最有效率(不需要進程間通信),其他應(yīng)用可以接收或發(fā)送你的廣播的相關(guān)安全問題也不需要擔心。
以下代碼片段演示了如何通過創(chuàng)建Intent并且調(diào)用sendBroadcast(Intent)來發(fā)送廣播。
Intent intent = new Intent();
intent.setAction("com.example.broadcast.MY_NOTIFICATION");
intent.putExtra("data","Notice me senpai!");
sendBroadcast(intent);
廣播信息被包裝在Intent對象中。intent的action string必須提供這個應(yīng)用的java包名句法并且唯一定義廣播事件名。你可以使用putExtra(String, Bundle)向intent中附加額外的信息。你還可以通過調(diào)用intent的setPackage(String)方法,將廣播限制在同一組織中的同一組應(yīng)用。
雖然intent既可以用來發(fā)送廣播,又可以用
startActivity(Intent)來啟動activity,但這兩種行為是完全無關(guān)的。廣播接收器無法看到或捕獲用來啟動activity的intent,同樣,當你廣播intent時,你也無法找到或啟動一個activity。
限制有權(quán)限的廣播
權(quán)限允許你對有特定權(quán)限的一系列應(yīng)用限制廣播。你可以對廣播的發(fā)送者或接收者實施限制。
權(quán)限發(fā)送
當你調(diào)用sendBroadcast(Intent, String)或sendOrderedBroadcast(Intent, String, BroadcastReceiver, Handler, int, String, Bundle)時,你可以制定一個權(quán)限參數(shù)。只有在manifest中滿足權(quán)限的接收器(如果是危險權(quán)限并在后來被授權(quán))才可以接收這個廣播。例如,下面的代碼發(fā)送了一個廣播:
sendBroadcast(new Intent("com.example.NOTIFY"),
Manifest.permission.SEND_SMS);
要接收這個廣播,應(yīng)用必須要如下請求權(quán)限:
<uses-permission android:name="android.permission.SEND_SMS"/>
你可以如SEND_SMS指定一個現(xiàn)成的系統(tǒng)權(quán)限,也可以使用<permission>元素定義自定義權(quán)限。了解一般權(quán)限和安全信息,查看System Permissions。
應(yīng)用安裝時會注冊自定義權(quán)限。在應(yīng)用使用前必須要定義自定義權(quán)限。
權(quán)限接收
如果你在注冊廣播接收器時指定了權(quán)限參數(shù)(不管是使用registerReceiver(BroadcastReceiver, IntentFilter, String, Handler)還是在manifest中聲明<receiver>),只有使用<uses-permission>在manifest中申請權(quán)限的廣播(如果是危險權(quán)限并在后來被授權(quán)可以發(fā)送一個intent到接收器中。
例如,確定你的接收應(yīng)用有如下的manifest聲明接收器:
<receiver android:name=".MyBroadcastReceiver"
android:permission="android.permission.SEND_SMS">
<intent-filter>
<action android:name="android.intent.action.AIRPLANE_MODE"/>
</intent-filter>
</receiver>
或者你的接收應(yīng)用有如下的context-registered接收器:
IntentFilter filter = new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED);
registerReceiver(receiver, filter, Manifest.permission.SEND_SMS, null );
然后,將廣播發(fā)送給這些接收器,發(fā)送應(yīng)用必須要如下申請權(quán)限:
<uses-permission android:name="android.permission.SEND_SMS"/>
安全考慮和最佳實踐
這里是一些發(fā)送與接收廣播的安全考慮與最佳實踐。
- 如果你不需要發(fā)送廣播到應(yīng)用外的組件,可以使用
LocalBroadcastManager發(fā)送、接收本地廣播,它可以從Support Library中獲得。LocalBroadcastManager更有效(不需要進程間通信),并且你可以不用擔心別的應(yīng)用接收或發(fā)送你的廣播的相關(guān)安全問題。本地廣播可以在你的應(yīng)用中作為通用的發(fā)布/訂閱的事件總線,而不需要系統(tǒng)廣播的任何開銷。 - 如果有很多應(yīng)用都在它們的manifest中注冊了要接收同一個廣播,會導(dǎo)致系統(tǒng)啟動過多應(yīng)用,對設(shè)備性能和用戶體驗都造成重大影響。為了避免這種情況,最好使用context注冊替代manifest聲明。有時Android系統(tǒng)本身會強制使用context-registered接收器。例如
CONNECTIVITY_ACTION廣播就只能使用context-registered發(fā)送給接收器。 - 不要使用隱式intent廣播敏感信息。任何注冊了要接收這個廣播的應(yīng)用都可以讀取這個信息。這里有三種方式可以限制接收你的廣播。
- 你可以在發(fā)送廣播時指定權(quán)限。
- 在Android 4.0或更高版本,你可以在發(fā)送廣播時使用
setPackage(String)指定一個包。系統(tǒng)將廣播限制為匹配了這個包的一系列應(yīng)用。 - 你可以使用
LocalBroadcastManager發(fā)送本地廣播。
- 當你注冊接收器時,任何應(yīng)用都可以發(fā)送潛在的惡意廣播到接收器中。這里有三種方法可以限制你的應(yīng)用接收的廣播:
- 你可以在注冊廣播接收器時指定權(quán)限。
- 對于manifest-declared接收器,你可以在manifest中設(shè)置android:exported屬性為false。接收器就不會接收應(yīng)用外部的廣播。
- 你可以通過
LocalBroadcastManager限制你的應(yīng)用。
- 廣播action的命名空間是全局的。確認action名和其他字符是依照你自己的命名空間寫的,否則你可能會無意中與其他應(yīng)用發(fā)生沖突。
- 因為接收器的
onReceive(Context, Intent)方法在主線程中運行,它應(yīng)該快速執(zhí)行并返回。如果你需要執(zhí)行長時間的工作,要注意派生線程或啟動后臺服務(wù),因為系統(tǒng)會在onReceive()返回后殺死整個進程。了解更多信息,查看Effect on process state來執(zhí)行長時間的工作,我們建議:- 在你接收器的
onReceive()方法中調(diào)用goAsync()并將BroadcastReceiver.PendingResult傳遞給后臺線程。它會保持廣播在onReceive()返回后繼續(xù)活動。但是,即使使用這種方法,系統(tǒng)也希望你盡快結(jié)束廣播(10秒內(nèi))。它還允許你將工作轉(zhuǎn)移到另一個線程以避免妨礙主線程。 - 使用
JobScheduler調(diào)度作業(yè)。了解更多信息,查看Intelligent Job Scheduling。
- 在你接收器的
- 不要從廣播接收器打開活動。因為這樣的用戶體驗很突兀。尤其是當這里有多個接收器時。作為替代,請考慮顯示一個notification。