Android App可以接收來自系統和其他App的廣播消息,也可以向它們發送廣播消息,比較類似於“發佈-訂閱”的設計模式,本文主要介紹廣播的類型,如何註冊廣播,如何發送廣播以及使用廣播需要注意的一些事兒。
I. 廣播的分類
無序廣播
沒有順序的廣播,廣播的接收方沒有嚴格的順序可言,不可中斷。有序廣播
在註冊時可指定優先級,優先級高的廣播接收者優先收到廣播,優先級以一個整數來標識,數值越大優先級越高。可中斷,可再修飾。粘滯廣播
發出的廣播會滯留,註冊時間可晚於發送時間,其他功能與無序廣播相同。
II. 註冊廣播
編寫一個廣播接收者,通常是繼承BroadcastReceiver並重寫onReceive(Context,Intent)方法。
public class MyReceiver extends BroadcastReceiver {
private static final String TAG = "MyReceiver";
@Override
public void onReceive(Context context, Intent intent) {
// TODO: do somethings
// 這個方法運行在主線程下
String action = intent.getAction();
Log.d(TAG, "onReceive: " + action);
}
}
編寫完廣播接收者後,就需要進行註冊(訂閱),告訴系統這個廣播接收者對哪些廣播感興趣。註冊的方式有靜態註冊和動態註冊兩種:
- 在AndroidManifest.xml文件中聲明(靜態註冊)
<receiver android:name=".MyReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="jdqm.intent.action.TEST"/>
</intent-filter>
</receiver>
- 通過Java代碼註冊(動態註冊)
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction("android.intent.action.BOOT_COMPLETED");
intentFilter.addAction("jdqm.intent.action.TEST");
registerReceiver(myReceiver, intentFilter);
通過動態註冊的廣播接收者,在宿主(註冊時所使用的Context)的生命週期期間都是有效的。當然你也可以在適當的時間調用unregisterReceiver(BroadcastReceiver)來解除註冊,這個“適當”取決於具體的業務需求。例如使用Activity的Context在onCreate(Bundle) 中註冊的一個廣播接收者,可以在onDestory()方法回調時解除註冊來防止廣播接收者泄漏。原則:不重複註冊,不泄露。
以上註冊的廣播接收者對 android.intent.action.BOOT_COMPLETED 和 jdqm.intent.action.TEST 這兩種action的廣播感興趣,後者是自定義的廣播,前者是開機完成時由系統發出(通常自啓動的應用會註冊這個廣播),但註冊這個廣播須要以下權限:
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
III. 發送廣播
- 發送無序廣播
Intent intent = new Intent("jdqm.intent.action.TEST");
sendBroadcast(intent);
- 發送有序廣播
Intent intent = new Intent("jdqm.intent.action.TEST");
//第二個參數是權限
sendOrderedBroadcast(intent, null);
- 發送本地廣播
Intent intent = new Intent("jdqm.intent.action.TEST");
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
本地廣播只有本應用內通過LocalBroadcastManager.getInstance(this).registerReceiver方法註冊的廣播接收者能收到,具有更高的安全性,效率也更高(不用跨進程通信)。
- 發送粘滯廣播
Intent intent = new Intent("jdqm.intent.action.TEST");
sendStickyBroadcast(intent);
這種類型廣播在Android6.0中已經被標記被過時, 它有不安全(任何App都能訪問), 沒有保護 (任何App都能修改)等問題。另外發送這種廣播需要以下權限
<uses-permission android:name="android.permission.BROADCAST_STICKY"/>
IV. 接收順序
- 對於無序廣播,動態註冊的廣播接收者會先收到,可以從源碼中得到理論支撐。在BroadcastQueue類中有兩個集合
//存儲所有動態註冊的無序廣播接受者
final ArrayList<BroadcastRecord> mParallelBroadcasts = new ArrayList<>();
//存儲所有靜態註冊和動態註冊的有序廣播接收者
final ArrayList<BroadcastRecord> mOrderedBroadcasts = new ArrayList<>();
然後在其processNextBroadcast(boolean fromMsg)方法中,首先是處理了mParallelBroadcasts集合。
2. 對於有序廣播,優先級高的接受者先收到,如果優先級相同,順序就是不確定的。先收到的接收者可以調用abortBroadcast()來中斷此廣播,後續優先級較低的接受者將無法收到。除了中斷還可以調用setResultXxx()方法來往廣播添加數據,後續的接收者可以讀取這些數據。
IV. 安全性與實踐
- 如果你的廣播不需要發送給本應用以外的組件,使用LocalBroadcastManager來發送廣播,這樣安全性和效率都比較高
靜態註冊有可能造成大量的App啓動,這將會影響系統的性能,所以儘量使用動態註冊來替代靜態註冊。這一點Android系統就做出了很好的示範,比如 CONNECTIVITY_ACTION 這個廣播只發送給動態註冊的廣播接收者。
不在廣播的Intent中包含敏感的信息,因爲只要註冊了這個廣播就能讀取到這些信息。你可以通過以下3中方式來獲得一定的安全性。
- 通過使用權限來發送廣播,這樣只有聲明瞭該權限的應用才能收到廣播。但是你很難確保你的權限不被泄漏。
- Android4.0及以上版本,在發送廣播的時候可以通過setPackage來指定package(可以指定多個),這樣只有匹配的package能接受到。
- 使用LocalBroadcastManager來發送本地廣播。
當你註冊了一個廣播,意味着任何App都可以給你發送廣播,以下有三點可以限制接收者:
- 註冊的時候增加權限。
- 在AndroidManifest.xml註冊receivers時,將android:exported屬性設爲false。
- 使用LocalBroadcastManager來註冊。
action的命名空間是全局的,這意味着action有可能會與其他App衝突,所以最好是有一個自己的命名空間。
因爲廣播接收者是運行在主線程,它應該快速地被執行並且return,所以不要在onReveive方法中做比較耗時的操作。
- 不要在廣播接收者中啓動activitys,這違背了用戶的使用習慣,特別是不止一個接收者時。這種情況下可以考慮展示一個notification來替代。
V. 其他
系統廣播的action的完整列表在Android SDK下的 BROADCAST_ACTIONS.TXT,路徑爲: Android/sdk/platforms/android-26/data/BROADCAST_ACTIONS.TXT
Android3.1開始,系統的package manager將記錄處於停止狀態的應用。默認情況下,靜態註冊了廣播的處於“停止”狀態的應用,是不會被啓動的,即不會收到廣播。當然你也可以爲Intent指定flag來該變這個行爲:
- FLAG_INCLUDE_STOPPED_PACKAGES:包含處於“停止”狀態的應用;
- FLAG_EXCLUDE_STOPPED_PACKAGES:不包含處於“停止”狀態的應用;
如果Intent不包含(或都包含)這兩個flag,則表現形式是包含處於“停止”轉態的應用,但是系統默認添加了FLAG_EXCLUDE_STOPPED_PACKAGES這個flag,這一點在源碼中有所體現:
ActivityManagerService#broadcastIntentLocked
final int broadcastIntentLocked(ProcessRecord callerApp,
String callerPackage, Intent intent, String resolvedType,
IIntentReceiver resultTo, int resultCode, String resultData,
Bundle resultExtras, String[] requiredPermissions, int appOp, Bundle bOptions,
boolean ordered, boolean sticky, int callingPid, int callingUid, int userId) {
intent = new Intent(intent);
// By default broadcasts do not go to stopped apps.
intent.addFlags(Intent.FLAG_EXCLUDE_STOPPED_PACKAGES);
...
}
這就意味着如果你想啓動處於“停止”狀態的應用,必須添加FLAG_INCLUDE_STOPPED_PACKAGES這個flag。那麼一個應用在什麼情況下會處於停止狀態?①應用首次安裝並且沒有啓動過;②被人爲地強制停止。開機完成的廣播就是FLAG_EXCLUDE_STOPPED_PACKAGES這種類型的Intent,這意味着如果你的應用被停止了,開機自啓就會失效。下一篇文章將從源碼的角度來分析廣播的工作流程。