Android廣播機制簡析

1. 廣播機制簡介

Android 中的廣播主要是用來在不同的組件間傳遞消息用的。 app 能夠接收來自 Android 系統或者其他 app 所發送的廣播。也能像其發送系統提供的或者自定義的廣播。例如,Android系統啓動時就會發送一個開機廣播,如果想要在開機是處理一些邏輯,可以監聽該廣播。

應用場景如下:

  • 同一個app內部的同一個組件內的消息通信(單個或多個線程);
  • 同一個app內部的不同組件之間的消息通信(單個或多個進程)
  • 不同app之間的組件之間的消息通信
  • Android系統在特定情況下與App之間的消息通信

2. 廣播的接收

想要接收系統或者app發送的廣播,需要提前對廣播接收器(BroadcastReceiver)進行註冊。Android提供了兩種註冊的方式:靜態註冊和動態註冊

2.1 靜態註冊廣播接收器

靜態註冊是通過<receiver>標籤在 AndroidManifest.xml 進行聲明實現的。通過這種註冊方式,再接收到廣播時系統會啓動應用,即使這個應用沒有運行

通過以下步驟可以實現靜態註冊一個廣播接收器

  1. 在 AndroidManifest.xml 中定義一個 <receiver> 標籤:
    <receiver
        android:name=".CustomBroadcastReceiver"
        android:exported="true">
        
        <intent-filter>
            <action name="android.intent.action.BOOT_COMPLETED"/> 接收開機廣播
            <action name="com.zero.test.CUSTOM_ACTION"/> 自定義廣播
        </intent-filter>
    </receiver>
    
    
    //監聽開機廣播需要指定的權限,要加上下面這一句
    <uses-permission android:name="permission.RECEIVER_BOOT_COMPLETED"/>

<receiver>的常用屬性:

  • android:enable:是否啓用廣播接收器。
  • android:exported:設置此廣播接收器是否可以接收其他應用所發出的廣播。它的默認值取決於是否包含過濾器(<intent-filter>),如果沒有定義<intent-filter>標籤,則默認爲 false ,如果有則爲 true。
  • android:permission:外部的 app 只有聲明瞭該屬性所指定的權限,才能向這個廣播接收器發送消息。
  • android:process:指定該廣播接收器運行的進程。若沒有指定則默認運行在 app 的主進程或者 所指定的進程。如果該值以:開頭,則表示開啓一個子進程,這個廣播接收器就是運行在其中的。
  1. 接着創建一個繼承自 BroadcastReceiver 的子類,並重寫 onReceiver() 方法
    public class CustomBroadcastReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            //彈出一個提示
            Toast.makeText(context, "接收到" + intent.getAction() + "廣播", Toast.LENGTH_SHORT).show();
        }
        
    }

通過以上兩步,系統會在 app 安裝時就將廣播接收器註冊完成,相當於這個廣播是跟 app 分離的,即使 app 沒有運行,通過發送指定的廣播也可以啓動這個 app。

2.2 動態註冊廣播接收器

通過以下步驟可以註冊一個動態的廣播接收器

  1. 創建一個 BroadcastReceiver 或者其子類的實例,拿上面定義個 CustomBroadcastReceiver 爲例
    CustomBroadcastReceiver receiver = new CustomBroadcastReceiver();
  1. 創建一個過濾器(IntentFilter),然後註冊
    IntentFilter filter = new IntentFilter();
    filter.addAction("android.intent.action.BOOT_COMPLETED");
    filter.addAction("com.zero.test.CUSTOM_ACTION");
    
    this.registerReceiver(receiver, filter);

上面的 this 表示的是一個 Context 對象,可以理解爲組件運行的環境。這個 Context 決定了這個廣播接收器的生命週期。僅僅當註冊廣播接收器的 Context 對象有效時,該 BroadcastReceiver 纔是有效的。例如,使用 activity 的 context 對象來註冊,則當這個 Activity 運行時,這個廣播接收器才能接受到廣播。

  1. 動態註冊的廣播接收器必須在不需要時取消,否則會發生內存泄漏的情況
    unregisterReceiver(receiver);

一般會在 onCreate() 中註冊廣播接收器,在 onDestroy() 中取消,或者在 onResume() 中註冊,在 onPause() 中取消,要看具體的情況。

2.3 靜態廣播與動態廣播的區別

  1. 靜態廣播也叫常駐廣播,即使程序沒有運行,這個廣播也還是存在於 Android 系統之中的,脫離於應用的生命週期。而動態廣播則是跟隨註冊它的組件的生命週期,如果在 Activity 中註冊,那麼當 Activity 被銷燬時,這個廣播也就失效了。
  2. 靜態廣播既可以通過顯式 Intent 啓動(即通過在 Intent 中指定類名),又可以通過隱式 Intent 啓動。而動態廣播只能通過隱式廣播啓動。

2.4 廣播接收器的狀態對進程的影響

當正在運行 BroadcastReceiver 的 onReceive() 中的代碼時,此 BroadcastReceiver 所在的進程被認爲是前臺進程。一般情況下,系統不會將其殺死

當 onReceive() 方法執行結束,系統會判斷 BroadcastReceiver 是否還有其他的應用程序的組件在運行,如果沒有會被認爲是低優先級的進程,則系統會在內存不足時將其殺死以釋放內存。

因此不應該在 onReceive() 中開啓後臺線程,因爲當 onReceiver() 運行結束,當前進程可能會因爲優先級太低而被系統殺死,從而導致後臺線程也被幹掉,即便該線程還沒有執行完。

如果希望在 onReceive() 執行完之後可以有時間繼續執行未完成的線程,可以通過 goAsync() 方法或者在 onReceive() 方法中使用 JobSchedule 調度一個 JobService。不過這樣可能會導致主線程的卡頓

3. 發送廣播

Android 有三種發送廣播的方式

  • sendOrderedBroadcast(Intent, String)。用於發送有序廣播。通過該方法發送的廣播首先會被優先級最高的廣播接收器執行,然後在傳遞到下一個廣播。也可以通過 abortBroadcast() 來終止廣播的傳遞。廣播的優先級可以通過 標籤的 android:priority 屬性來指定。如果兩個廣播接收器有相同的優先級,對於不同類型的廣播,會優先執行動態註冊的廣播接收器。如果同爲動態的,則優先執行先註冊的。同爲靜態廣播則優先執行先掃描到的(類似於隨機)。
  • sendBroadcast(Intent intent)。向所有廣播接收器發送廣播,不帶順序。也叫作普通廣播。該方法發送的廣播不會被某一個廣播接收器所終止
  • LocalBroadcastManager.sendBroadcast(Intent)。該方法發送的廣播僅限於在同一個應用中進行消息傳遞,也叫作本地廣播。這種廣播效率要比其他兩種好,並且不需要擔心由廣播的發送和接收所帶來的的安全問題。如果 app 中不需要像外界發送廣播,建議使用本地廣播。

4. 廣播中的權限

4.1 在發送廣播時指定權限

可以通過在發送廣播的方法中指定權限,來確保只有的 AndroidManifest.xml 中聲明瞭該權限的廣播接收器才能夠接收到該廣播。

    sendBroadcast(new Intent("com.zero.test.CUSTOM_ACTION"), Manifest.permission.SEND_MSM);
    
    //接着你需要在廣播接收器所在 app 的 AndroidManifest.xml 中聲明該權限
    <uses-permission android:name="android.permission.SEND_MSM"/>

關於權限可以指定系統權限,也可以使用自定義的權限。但是自定義的權限是在應用安裝時被註冊的,因此在發送自定義權限的廣播之前要確保定義該權限的應用程序已經被安裝。

4.2 在接收廣播時指定權限

如果在註冊廣播接收器時指定了權限,則只有在 AndroidManifest.xml 中聲明瞭相應權限的 app 可以向這個廣播接收器發送廣播。

  1. 靜態註冊時指定權限
    <receiver
        android:name=".CustomBroadcastReceiver"
        android:exported="true"
        android:permission="android.permission.SEND_SMS">
        <intent-filter>
            <action android:name="com.zero.test.CUSTOM_ACTION"/>
        </intent-filter>  
    </receiver>
  1. 動態註冊廣播時指定權限
    CustomBroadcastReceiver receiver = new CustomBroadcastReceiver();

    IntentFilter filter = new IntentFilter("com.zero.test.CUSTOM_ACTION");
    registerReceiver(receiver, filter, Manifest.permission.SEND_MSM, null);
    

5. 廣播中的一些注意點

  • 如果不需要嚮應用程序以外的 app 或者組件發送廣播,建議使用 LocalBroadcastManager 。因爲使用 LocalBroadcastManager 的效率更高。能避免了由於向外部發布或者接收廣播所導致的安全問題。另外,使用 LocalBroadcastManager 能減少系統的開銷
  • 儘量動態註冊廣播接收器。因爲可能會存在多個不同的 app 註冊了相同的廣播的情況,此時可能會因爲啓動大量的 app 而對設備的性能造成重大影響。而且 Android 中有些廣播只會發送給動態註冊的廣播接收器,比如 CONNECTIVITY_ACTION。
  • 不要使用隱式的廣播傳遞敏感的信息,這樣可能會導致敏感信息的泄漏。可以通過指定權限、指定包名來控制誰可以接收該廣播,或者直接使用 LocalBroadcastManager 來發送本地廣播
  • 爲了防止受到別的 app 所發送的惡意廣播,可以在註冊廣播接收器是指定權限,或者將 標籤的 android:exported 屬性置爲false。來控制誰可以向你發送廣播。也可以使用 LocalBroadcastManager.registerReceiver(BroadcastReceiver receiver, IntentFilter filter)來將廣播接收器限制爲僅可接收本地廣播
  • BroadcastReceiver 的 onReceive() 必須在很短的時間內執行完,否則會觸發應用程序無響應。可以在 onReceive() 中方法中使用 goAsync() 或者 JobSchedule 調度一個作業來開啓線程。開啓線程需要謹慎的。也可以在 onReceive() 中開啓一個後臺服務
  • 儘量不要在 onReceive() 中開啓活動,想一想玩手機玩的好好地,突然給調到另一個界面,會不會很想卸載那個應用。可以採用顯示通知的方式。

6. Protected Broadcast

顧名思義,爲受保護的廣播Action。對應的標籤爲 <protected-broadcast> 。例如想將一個自定義的action設置爲受保護的廣播,在 AndroidManifest.xml 添加以下語句即可:

<protected-broadcast android:name="android.action.ACTION_PROTECTED" />

不過在普通的應用開發場景中用不到,因爲這種類型的廣播只供系統 app 使用,第三方的 app 聲明改標籤會被系統給忽略掉。

之前在工作中遇到過在 framework 層發送一個自定義的廣播,在廣播接收器裏面接收不到,查看日誌發現是報以下錯誤:
ActivityManager: Sending non-protected broadcast...
在網上搜了一下發現是因爲自定義的那個廣播沒有被聲明爲 <protected-broadcast> 的原因,然後在 Y:\T1\android\frameworks\base\core\res\AndroidManifest.xml 中將自定義的廣播聲明爲受保護的廣播即可。

然後找了一下源碼中那個地方會拋出上面那個錯誤,代碼在 ActivityManagerService.java 中,如下:

private void checkBroadcastFromSystem(Intent intent, ProcessRecord callerApp, String callerPackage, int callingUid, boolean isProtectedBroadcast, List receivers) {
    ....
     if (isProtectedBroadcast
                || Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(action)
                || Intent.ACTION_DISMISS_KEYBOARD_SHORTCUTS.equals(action)
                || Intent.ACTION_MEDIA_BUTTON.equals(action)
                || Intent.ACTION_MEDIA_SCANNER_SCAN_FILE.equals(action)
                || Intent.ACTION_SHOW_KEYBOARD_SHORTCUTS.equals(action)
                || Intent.ACTION_MASTER_CLEAR.equals(action)
                || Intent.ACTION_FACTORY_RESET.equals(action)
                || AppWidgetManager.ACTION_APPWIDGET_CONFIGURE.equals(action)
                || AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(action)
                || LocationManager.HIGH_POWER_REQUEST_CHANGE_ACTION.equals(action)
                || TelephonyIntents.ACTION_REQUEST_OMADM_CONFIGURATION_UPDATE.equals(action)
                || SuggestionSpan.ACTION_SUGGESTION_PICKED.equals(action)
                || AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION.equals(action)
                || AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION.equals(action)) {
            // Broadcast is either protected, or it's a public action that
            // we've relaxed, so it's fine for system internals to send.
            return;
        }
    ....
    
    if (callerApp != null) {
            Log.wtf(TAG, "Sending non-protected broadcast " + action + " from system " + callerApp.toShortString() + " pkg " + callerPackage, new Throwable());
    } else {
            Log.wtf(TAG, "Sending non-protected broadcast " + action + " from system uid " + UserHandle.formatUid(callingUid) + " pkg " + callerPackage, new Throwable());
    }
    
    .....
}

可以看出從系統發出的廣播會被檢查,如果該廣播沒有被聲明爲 protected-broadcast ,則會報錯。

與之相反,在普通的 app 中則只能發送不受保護的廣播,否則會拋一個異常,代碼也·在 ActivityManagerService.java 中:

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) {
    ...
    if (!isCallerSystem) {
        if (isProtectedBroadcast) {
            String msg = "Permission Denial: not allowed to send broadcast "
                    + action + " from pid="
                    + callingPid + ", uid=" + callingUid;
            Slog.w(TAG, msg);
            throw new SecurityException(msg);

        }else if(...){
            ...
        }
        ...
    }
    ...
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章