BroadCast(广播)

洪祺
2023-12-01

本文始发于github,由于文内部分链接使用的是相对路径,如果访问不到,请移步github项目内阅读即可正常访问。

原文(英文)地址


应用程序可以注册去接收特定的 broadcast,当一个 broadcast被发送,系统自动将该 broadcast路由到已订阅接收该特定类型 broadcast的应用程序。

一般来说,broadcast可以用作跨应用程序和正常用户流之外的消息传递系统。但是,您必须小心,不要滥用在后台响应broadcast和执行任务的机会,这可能导致系统运行变慢,如以下视频所述。

视频地址

关于系统broadcast

系统会在一些系统事件发生的时候发生broadcast,比如当系统在飞行模式和正常模式之间切换的时候。系统广播会发送给所有订阅了要接收对应事件的应用程序。

broadcast Message本身包装在一个Intent对象中,该对象的action属性标识发生的事件(例如android.intent.action.AIRPLAN_MODE)。Intent还可能包括捆绑到其extra字段中的附加信息。例如,飞行模式Intent包括一个布尔类型的extra值,用于指示飞行模式是否打开。

关于如何读取Intent以及如何从Intent中获取action字段值,请参阅Intent和Intent Filter.

关于完整的系统broadcast actions信息,请参阅Android SDK中的BROADCAST_ACTION.TXT文件。每一个broadcast action都有一个常量值与之对应,比如ACTION_AIRPLANE_MODE_CHANGED对应android.intent.action.AIRPLANE_MODEBROADCAST_ACTION.TXT文档中有每一个broadcast action对应的常量值的信息。

系统broadcast的更改

随着Android平台的发展,它会定期改变系统broadcast的行为方式。如果您的应用程序针对Android 7.0API级别24)或更高版本,或者安装在运行Android 7.0或更高版本的设备上,请记住以下更改。

Android 9

Android 9API级别28)开始,NETWORK_STATE_CHANGED_ACTION不再接收用户的位置或用户个人数据的信息。
此外,如果您的应用程序是安装Android 9或者更高版本的设备的上,从WI-FI发送的broadcast不再含有SSIDsBSSIDs、连接信息、扫描结果等信息。想得到这些信息,应该调用getConnectionInfo()方法。

Android 8.0

Android8.0(API 26)开始,系统对在manifest中声明的接受者添加了额外限制。
如果你的应用程序的目标设备版本为Android8.0或更高,您无法使用manifest为大多数隐式broadcastbroadcast不专门针对您的应用)声明receiver。当用户主动使用您的应用时,您仍然可以使用context注册的接收器(context-registered receiver)。

Android 7.0

Android 7.0API级别24)及更高版本不发送以下系统广播:

  • ACTION_NEW_PICTURE
  • ACTION_NEW_VIDEO

此外,针对Android 7.0及更高版本的应用必须使用[registerReceiver(BroadcastReceiver, IntentFilter)](https://developer.android.com/reference/android/content/Context.html#registerReceiver(android.content.BroadcastReceiver, android.content.IntentFilter))注册CONNECTIVITY_ACTION广播。在manifest中声明receiver不起作用。

接收broadcast

应用程序可以通过两种方式接收广播:在manifest中声明broadcast receiver(manifest-declared receivers)和使用context注册receiver( context-registered receivers)

在manifest中声明receiver

如果你在你的manifest中声明broadcast receiver,系统会在broadcast被发送后启动你的应用程序(如果发送broadcast的时候你的应用处于未启动状态)。

注意:如果您的应用程序的targetAPI级别26或更高,则您无法使用manifest来声明隐式broadcast(不特定针对你appbroadcast)的receiver,除了一些免于该限制(exempted from that restriction)的隐式broadcast,在大多数情况下,您可以使用scheduled jobs

为了在manifest中声明broadcast receiver,请按照以下步骤实现:

1:在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-filter指定receiver订阅的broadcast action

2:继承BroadcastReceiver并实现onReceive(Context,Intent)方法。以下示例中的BroadCastReceiver记录并显示broadcast的内容:

  • kotlin

  • private const val TAG = "MyBroadcastReceiver"
    
    class MyBroadcastReceiver : BroadcastReceiver() {
    
        override fun onReceive(context: Context, intent: Intent) {
            StringBuilder().apply {
                append("Action: ${intent.action}\n")
                append("URI: ${intent.toUri(Intent.URI_INTENT_SCHEME)}\n")
                toString().also { log ->
                    Log.d(TAG, log)
                    Toast.makeText(context, log, Toast.LENGTH_LONG).show()
                }
            }
        }
    }
    
  • java

  • 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();
            }
        }
    

系统的package manager在安装应用程序时会注册register。然后,register成为应用程序的单独入口点(separate entry point ),这意味着如果应用程序当前未运行,系统可以启动应用程序并发送broadcast

系统创建一个新的BroadcastReceiver组件对象来处理它接收的每个广播。此对象仅在调用onReceive(Context,Intent)期间有效。一旦您的代码从此方法返回,系统会认为该组件不再处于活动状态。

Context注册register

通过context注册receiver,大致应该按照以下步骤:

1:创建一个BroadcastReceiver实例

  • kotlin

  • val br: BroadcastReceiver = MyBroadcastReceiver()
    
  • java

  • BroadcastReceiver br = new MyBroadcastReceiver();
    

2:创建一个IntentFilter并通过调用[registerReceiver(BroadcastReceiver, IntentFilter)](https://developer.android.com/reference/android/content/Context.html#registerReceiver(android.content.BroadcastReceiver, android.content.IntentFilter))注册receiver

  • kotlin

  • val filter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION).apply {
        addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED)
    }
    registerReceiver(br, filter)
    
  • java

  • IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
        filter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED);
        this.registerReceiver(br, filter);
    

注意:如果要注册一个local broadcast,请调用[LocalBroadcastManager.registerReceiver(BroadcastReceiver, IntentFilter)](https://developer.android.com/reference/androidx/localbroadcastmanager/content/LocalBroadcastManager.html#registerReceiver(android.content.BroadcastReceiver, android.content.IntentFilter))。

只要注册的context有效,context注册的receiver就会接收broadcast。例如,如果您在Activity context中注册,则只要Activity未被销毁,您就会收到broadcast。如果您在application context中注册,则只要应用程序正在运行,您就会收到broadcast

3:要停止接收广播,请调用unregisterReceiver(android.content.BroadcastReceiver)。当您不再需要Regiscontext不再有效时,请务必取消注册的register

请注意注册和取消注册register的位置,例如,如果使用Activity contextonCreate(Bundle)中注册register,则应在onDestroy()中取消注册,以防止register泄漏到Activity context之外。如果在onResume()中注册register,则应在onPause()中注销它以防止多次注册(如果您不希望在Paused时接收广播,这可以减少不必要的系统开销)。不要在onSaveInstanceState(Bundle)中取消注册,因为如果用户在历史堆栈(history stack)中向后移动(moves back),则不会调用此方法。

对进程生命状态的影响

BroadcastReceiver的状态(无论是否正在运行)会影响其宿主进程的状态,从而影响该对应进程被系统杀死的可能性。例如,当进程执行receiver(即运行onReceive()方法中的代码)时,它被认为是前台进程,除极端内存压力外,系统会维持其运行。

但是,一旦您的代码从onReceive()返回,BroadcastReceiver就不再处于活动状态。Register的宿主进程会与系统中其他正在运行的应用进程一样重要。如果该进程仅承载manifest中声明的register(常见的比如用户从未或最近未与之交互过的应用程序),则在从onReceive()返回时,系统将其进程视为低优先级进程并且可能杀死它以使资源可用于其他更重要的进程。

因此,您不应该在broadcast receiver中启动长时间运行在后台的线程。在onReceive()返回结果之后,系统可以随时终止进程以回收内存,这样做会终止在该进程中运行的线程。要避免这种情况,您应该调用goAsync()(如果您希望broadcast的后台线程拥有更多处理任务的时间)或使用JobSchedulerreceiver中调度JobService,这样系统知道该进程正在继续执行有效工作。有关更多信息,请参阅进程和应用程序的生命周期

以下代码段展示了一个BroadcastReceiver,它使用goAsync()标记在onReceive()完成后需要更多时间才能完成(如果onReceive()中的任务耗时足够多,导致UI线程错过一个帧(> 16ms),使其更适合放在后台线程执行,这将特别有用)。

  • kotlin

  • private const val TAG = "MyBroadcastReceiver"
    
    class MyBroadcastReceiver : BroadcastReceiver() {
    
        override fun onReceive(context: Context, intent: Intent) {
            val pendingResult: PendingResult = goAsync()
            val asyncTask = Task(pendingResult, intent)
            asyncTask.execute()
        }
    
        private class Task(
                private val pendingResult: PendingResult,
                private val intent: Intent
        ) : AsyncTask<String, Int, String>() {
    
            override fun doInBackground(vararg params: String?): String {
                val sb = StringBuilder()
                sb.append("Action: ${intent.action}\n")
                sb.append("URI: ${intent.toUri(Intent.URI_INTENT_SCHEME)}\n")
                return toString().also { log ->
                    Log.d(TAG, log)
                }
            }
    
            override fun onPostExecute(result: String?) {
                super.onPostExecute(result)
                // Must call finish() so the BroadcastReceiver can be recycled.
                pendingResult.finish()
            }
        }
    }
    
  • java

    public class MyBroadcastReceiver extends BroadcastReceiver {
        private static final String TAG = "MyBroadcastReceiver";
    
        @Override
        public void onReceive(Context context, Intent intent) {
            final PendingResult pendingResult = goAsync();
            Task asyncTask = new Task(pendingResult, intent);
            asyncTask.execute();
        }
    
        private static class Task extends AsyncTask<String, Integer, String> {
    
            private final PendingResult pendingResult;
            private final Intent intent;
    
            private Task(PendingResult pendingResult, Intent intent) {
                this.pendingResult = pendingResult;
                this.intent = intent;
            }
    
            @Override
            protected String doInBackground(String... strings) {
                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);
                return log;
            }
    
            @Override
            protected void onPostExecute(String s) {
                super.onPostExecute(s);
                // Must call finish() so the BroadcastReceiver can be recycled.
                pendingResult.finish();
            }
        }
    }
    

发送Broadcast

Android为我们提供了3种发送broadcast的方法:

  • [sendOrderedBroadcast(Intent, String)](https://developer.android.com/reference/android/content/Context.html#sendOrderedBroadcast(android.content.Intent, java.lang.String))方法一次向一个receiver发送广播。当每个receiver依次执行时,它可以将结果传播到下一个receiver,或者它可以中止广播,这样该广播就不会传递给其他receiver。可以使用intent-filterandroid:priority属性来控制receiver的接收顺序,具有相同优先级的receiver将以任意顺序执行。
  • sendBroadcast(Intent)方法以未定义的顺序向所有receiver发送广播。这称为正常广播。通常情况下这种情况更有效,但意味着receiver无法从其他receiver中读取结果,receiver传播从广播接收的数据或者终止它。
  • LocalBroadcastManager.sendBroadcast方法将广播发送到与发送方位于同一应用程序中的receiver。如果您不需要跨应用程序发送广播,请使用本地广播。这种方式的实现效率更高(无需进程间通信),您无需担心由于其他应用程序接收或发送您的广播引发的任何安全问题。

以下代码演示了如何通过创建一个Intent以及使用sendBroadcast(Intent)发送一个广播:

  • kotlin

  • Intent().also { intent ->
        intent.setAction("com.example.broadcast.MY_NOTIFICATION")
        intent.putExtra("data", "Notice me senpai!")
        sendBroadcast(intent)
    }
    
  • java

  • Intent intent = new Intent();
    intent.setAction("com.example.broadcast.MY_NOTIFICATION");
    intent.putExtra("data","Notice me senpai!");
    sendBroadcast(intent);
    

broadcast发送的消息包含在Intent对象中,intentaction属性必须提供该appjava包名并唯一标识broadcast event。您可以使用putExtra(String,Bundle)将其他信息附加到intent。您还可以通过调用intentsetPackage(String)将广播限制为同一组织中的一组应用程序。

注意:尽管Intent可以被同时用于发送广播和启动Activity,但是这二者的action属性是完全无关的。广播接收器无法查看或捕获用于启动ActivityIntent。同样,您无法使用broadcastIntent找到或启动Activity

使用权限(permissions)限制broadcast

permissions允许您将广播限制在具有特定权限的应用程序集中。您可以对广播的发送者或接收者施加限制。

发送时施加权限

当你调用[sendBroadcast(Intent, String)](https://developer.android.com/reference/android/content/Context.html#sendBroadcast(android.content.Intent, java.lang.String))或者[sendOrderedBroadcast(Intent, String, BroadcastReceiver, Handler, int, String, Bundle)](https://developer.android.com/reference/android/content/Context.html#sendOrderedBroadcast(android.content.Intent, java.lang.String, android.content.BroadcastReceiver, android.os.Handler, int, java.lang.String, android.os.Bundle))时,你可以指定权限参数。只有那些已经在其manifest中指定权限tagreceiver(并且如果当前权限是危险权限,则需要手动授予权限)可以接收广播。例如,以下代码发送广播:

  • kotlin

  • sendBroadcast(Intent("com.example.NOTIFY"), Manifest.permission.SEND_SMS)
    
  • java

    sendBroadcast(new Intent("com.example.NOTIFY"),
                  Manifest.permission.SEND_SMS);
    

如果要接收该广播,则需要下面的权限:

<uses-permission android:name="android.permission.SEND_SMS"/>

您可以指定现有系统权限(如SEND_SMS)或使用<permission>元素自定义权限。有关一般权限和安全性的信息,请参阅System Permissions

注意:安装应用程序时会注册自定义权限,所以必须在使用自定义的权限之前安装定义自定义权限的应用程序。

接收时施加权限

如果在注册广播接收器时指定了权限参数(使用[registerReceiver(BroadcastReceiver, IntentFilter, String, Handler)](https://developer.android.com/reference/android/content/Context.html#registerReceiver(android.content.BroadcastReceiver, android.content.IntentFilter, java.lang.String, android.os.Handler))或manifest中的<receiver>标记),则只有在manifest中使用<uses-permission>请求权限(如果是危险权限,随后需要被手动授权)的broadcast才能向该receiver发送广播。

例如,假设您的接收程序的manifest中的receiver具有以下声明:

<receiver android:name=".MyBroadcastReceiver"
          android:permission="android.permission.SEND_SMS">
    <intent-filter>
        <action android:name="android.intent.action.AIRPLANE_MODE"/>
    </intent-filter>
</receiver>

或者接收程序中含有使用以下代码进行context注册的receiver

  • kotlin

  • var filter = IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED)
    registerReceiver(receiver, filter, Manifest.permission.SEND_SMS, null )
    
  • java

  • IntentFilter filter = new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED);
    registerReceiver(receiver, filter, Manifest.permission.SEND_SMS, null );
    

这样,如果想向上面的receiver发送broadcast,则必须向下面这样请求权限:

<uses-permission android:name="android.permission.SEND_SMS"/>

安全注意事项及最佳做法

以下是发送和接收broadcast的一些安全注意事项和最佳做法:

  • 如果您不需要向应用程序外部的组件发送广播,则使用支持库中提供的LocalBroadcastManager发送和接收本地广播。 LocalBroadcastManager效率更高(无需进程间通信),而且您可以不用考虑与其他应用程序接收或发送您的广播相关的任何安全问题。本地广播可以在您的应用中用作通用发布/订阅事件总线,而无需系统广播的任何开销。

  • 如果很多应用在其manifest文件中注册接收相同的广播,则可能导致系统启动大量应用,从而对设备性能和用户体验产生重大影响。为避免这种情况,请优先使用context注册而不是manifest声明的方式,Android系统的有些广播会强制使用context注册的接收器。例如,CONNECTIVITY_ACTION广播仅传递给上下文注册的接收器。

  • 不要使用隐式Intent广播敏感信息。因为任何注册接收广播的应用都可以读取该信息。有三种方法可以控制谁可以接收您的广播:

    • 您可以在发送广播时指定权限。
    • Android 4.0及更高版本中,您可以在发送广播时指定包含setPackage(String)的包。系统将广播限制为与包匹配的应用程序集。
    • 您可以使用LocalBroadcastManager发送本地广播。
  • 当您注册接收器时,任何应用都可以向您的应用接收器发送潜在的恶意广播。有三种方法可以限制应用收到的广播:

    • 您可以在注册广播接收器时指定权限。
    • 对于manifest中声明的接收器,您可以在manifest中将android:exported属性设置为“false”,即receiver不接收来自应用程序之外的来源的广播。
    • 您可以将自己限制为仅接收使用LocalBroadcastManager发送的本地广播。
  • 广播的action的命名空间是全局的。确保action名称和其他字符串都写在您拥有的命名空间中,否则您可能会无意中与其他应用程序发生冲突。

  • 因为接收者的onReceive(Context,Intent)方法在主线程上运行,所以它应该执行并快速返回。如果您需要执行长时间运行的工作,请谨慎启动线程或启动后台Service,因为系统可以在onReceive()返回后终止整个进程。有关更多信息,请参阅对进程生命状态的影响。如果要执行长时间运行的工作,我们建议:

    • 在接收者的onReceive()方法中调用goAsync()并将BroadcastReceiver.PendingResult传递给后台线程。这使得从onReceive()返回后broadcast依然可以保持活动状态。但是,即使采用这种方法,系统也希望您能够非常快速地完成广播中的任务(10秒以内)。它允许您将工作移动到另一个线程,以避免主线程发生阻塞。
    • 使用JobScheduler调度任务。有关更多信息,请参阅Intelligent Job Scheduling
  • 不要在广播接收器中启动Activity,因为用户体验很不稳定,特别是如果有多个receiver的时候,请考虑使用notification

 类似资料: