1 BroadcastReceiver简介
1.1 BroadcastReceiver概览
Android应用与Android系统和其他Android应用之间可以相互收发广播消息,这与发布-订阅设计模式相似。这些广播会在所关注的事件发生时发送。举例来说Android系统会在发生各种系统事件时发送广播,例如系统启动或设备开始充电时。再比如,应用可以发送自定义广播来通知其他应用它们可能感兴趣的事件(例如,一些新数据已下载)。
应用可以注册接收特定的广播。广播发出后,系统会自动将广播传送给同意接收这种广播的应用。
一般来说,广播可作为跨应用和普通用户流之外的消息传递系统。但是,不要滥用在后台响应广播和运行作业的机会,因为这会导致系统变慢。
1.2 关于系统广播
系统会在发生各种系统事件时自动发送广播,例如当系统进入和退出飞行模式时。系统广播会被发送给所有同意接收相关事件的应用。
广播消息本身会被封装在一个Intent对象中,该对象的操作字符串会标识所发生的事件(例如android.intent.action.AIRPLANE_MODE)。该intent可能还包含绑定到其extra字段中的附加信息。例如,飞行模式intent包含布尔值extra来指示是否已开启飞行模式。
有关系统广播操作的完整列表,请参阅Android SDK中的BROADCAST_ACTIONS.TXT文件。每个广播操作都有一个与之关联的常量字段。例如,常量ACTION_AIRPLANE_MODE_CHANGED的值为android.intent.action.AIRPLANE_MODE。每个广播操作的文档都可以在关联的常量字段中找到。
2 接收广播
应用可以通过两种方式接收广播:清单声明的接收器和上下文注册的接收器。
2.1 清单声明的接收器
如果在清单中声明广播接收器,系统会在广播发出后启动应用(如果应用尚未运行)。
【注意】如果应用以API级别26或更高级别的平台版本为目标,则不能使用清单为隐式广播(没有明确针对应用的广播)声明接收器,但一些不受此限制的隐式广播除外。在大多数情况下,可以使用调度作业来代替。
要在清单中声明广播接收器,请执行以下两个步骤:
- 在应用清单中指定<receiver>元素,Intent过滤器指定接收器所订阅的广播操作。
1 | <receiver android:name=".MyBroadcastReceiver" android:exported="true"> |
- 创建BroadcastReceiver子类并实现onReceive(Context, Intent)。以下示例中的广播接收器会记录并显示广播的内容。
1 | public class MyBroadcastReceiver extends BroadcastReceiver { |
系统软件包管理器会在应用安装时注册接收器。然后,该接收器会成为应用的一个独立入口点,这意味着如果应用当前未运行,系统可以启动应用并发送广播。
系统会创建新的BroadcastReceiver组件对象来处理它接收到的每个广播。此对象仅在调用onReceive(Context, Intent)期间有效。一旦从此方法返回代码,系统便会认为该组件不再活跃。
2.2 上下文注册的接收器
要使用上下文注册接收器,请执行以下三个步骤:
- 创建BroadcastReceiver的实例。
1 | BroadcastReceiver br = new MyBroadcastReceiver(); |
- 创建IntentFilter并调用registerReceiver(BroadcastReceiver, IntentFilter)来注册接收器。
1 | IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION); |
【注意】要注册本地广播,请调用LocalBroadcastManager.registerReceiver(BroadcastReceiver, IntentFilter)。
只要注册上下文有效,上下文注册的接收器就会接收广播。例如,如果在Activity上下文中注册,只要Activity没有被销毁,就会收到广播。如果在应用上下文中注册,只要应用在运行,就会收到广播。
- 要停止接收广播,请调用unregisterReceiver(android.content.BroadcastReceiver)。当不再需要接收器或上下文不再有效时,请务必注销接收器。
请注意注册和注销接收器的位置,比方说,如果使用Activity上下文在onCreate(Bundle)中注册接收器,则应在onDestroy()中注销,以防接收器从Activity上下文中泄露出去。如果在onResume()中注册接收器,则应在onPause()中注销,以防多次注册接收器(如果不想在暂停时接收广播,这样可以减少不必要的系统开销)。请勿在onSaveInstanceState(Bundle)中注销,因为如果用户在历史记录堆栈中后退,则不会调用此方法。
2.3 对进程状态的影响
BroadcastReceiver的状态(无论它是否在运行)会影响其所在进程的状态,而其所在进程的状态又会影响它被系统终结的可能性。例如,当进程执行接收器(即当前在运行其onReceive()方法中的代码)时,它被认为是前台进程。除非遇到极大的内存压力,否则系统会保持该进程运行。
但是,一旦从onReceive()返回代码,BroadcastReceiver就不再活跃。接收器的宿主进程变得与在其中运行的其他应用组件一样重要。如果该进程仅托管清单声明的接收器(这对于用户从未与之互动或最近没有与之互动的应用很常见),则从onReceive()返回时,系统会将其进程视为低优先级进程,并可能会将其终止,以便将资源提供给其他更重要的进程使用。
因此,不应从广播接收器启动长时间运行的后台线程。onReceive()完成后,系统可以随时终止进程来回收内存,在此过程中,也会终止进程中运行的派生线程。要避免这种情况,应该调用goAsync()(如果希望在后台线程中多花一点时间来处理广播)或者使用JobScheduler从接收器调度JobService,这样系统就会知道该进程将继续活跃地工作。
以下代码段展示了一个BroadcastReceiver,它使用goAsync()来标记它在onReceive()完成后需要更多时间才能完成。如果希望在onReceive()中完成的工作很长,足以导致界面线程丢帧 (>16ms),则这种做法非常有用,这使它尤其适用于后台线程。
1 | public class MyBroadcastReceiver extends BroadcastReceiver { |
3 发送广播
Android为应用提供三种方式来发送广播:
sendOrderedBroadcast(Intent, String)方法一次向一个接收器发送广播。当接收器逐个顺序执行时,接收器可以向下传递结果,也可以完全中止广播,使其不再传递给其他接收器。接收器的运行顺序可以通过匹配的intent-filter的android:priority属性来控制;具有相同优先级的接收器将按随机顺序运行。
sendBroadcast(Intent)方法会按随机的顺序向所有接收器发送广播。这称为常规广播。这种方法效率更高,但也意味着接收器无法从其他接收器读取结果,无法传递从广播中收到的数据,也无法中止广播。
LocalBroadcastManager.sendBroadcast方法会将广播发送给与发送器位于同一应用中的接收器。如果不需要跨应用发送广播,请使用本地广播。这种实现方法的效率更高(无需进行进程间通信),而且无需担心其他应用在收发广播时带来的任何安全问题。
以下代码段展示了如何通过创建Intent并调用sendBroadcast(Intent)来发送广播。
1 | Intent intent = new Intent(); |
广播消息封装在Intent对象中。Intent的操作字符串必须提供应用的Java软件包名称语法,并唯一标识广播事件。可以使用putExtra(String, Bundle)向intent附加其他信息。也可以对intent调用setPackage(String),将广播限定到同一组织中的一组应用。
【注意】虽然intent既用于发送广播,也用于通过startActivity(Intent)启动Activity,但这两种操作是完全无关的。广播接收器无法查看或捕获用于启动Activity的intent;同样,当广播intent时,也无法找到或启动Activity。
4 通过权限限制广播
可以通过权限将广播限定到拥有特定权限的一组应用。可以对广播的发送器或接收器施加限制。
4.1 带权限的发送
当调用sendBroadcast(Intent, String)或sendOrderedBroadcast(Intent, String, BroadcastReceiver, Handler, int, String, Bundle)时,可以指定权限参数。接收器若要接收此广播,则必须通过其清单中的标记请求该权限(如果存在危险,则会被授予该权限)。例如,以下代码会发送广播。
1 | sendBroadcast(new Intent("com.example.NOTIFY"), Manifest.permission.SEND_SMS); |
要接收此广播,接收方应用必须请求如下权限。
1 | <uses-permission android:name="android.permission.SEND_SMS"/> |
可以指定现有的系统权限(如SEND_SMS),也可以使用<permission>元素定义自定义权限。
【注意】自定义权限将在安装应用时注册。定义自定义权限的应用必须在使用自定义权限的应用之前安装。
4.2 带权限的接收
如果在注册广播接收器时指定了权限参数(通过registerReceiver(BroadcastReceiver, IntentFilter, String, Handler)或清单中的<receiver>标记指定),则广播方必须通过其清单中的<uses-permission>标记请求该权限(如果存在危险,则会被授予该权限),才能向该接收器发送Intent。
例如,假设接收方应用具有如下所示的清单声明的接收器。
1 | <receiver android:name=".MyBroadcastReceiver" |
或者接收方应用具有如下所示的上下文注册的接收器。
1 | IntentFilter filter = new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED); |
那么,发送方应用必须请求如下权限,才能向这些接收器发送广播。
1 | <uses-permission android:name="android.permission.SEND_SMS"/> |
5 安全注意事项和最佳做法
以下是有关收发广播的一些安全注意事项和最佳做法:
如果不需要向应用以外的组件发送广播,则可以使用支持库中提供的LocalBroadcastManager来收发本地广播。LocalBroadcastManager效率更高(无需进行进程间通信),并且无需考虑其他应用在收发广播时带来的任何安全问题。本地广播可在应用中作为通用的发布/订阅事件总线,而不会产生任何系统级广播开销。
如果有许多应用在其清单中注册接收相同的广播,可能会导致系统启动大量应用,从而对设备性能和用户体验造成严重影响。为避免发生这种情况,请优先使用上下文注册而不是清单声明。有时,Android系统本身会强制使用上下文注册的接收器。例如,CONNECTIVITY_ACTION广播只会传送给上下文注册的接收器。
请勿使用隐式intent广播敏感信息。任何注册接收广播的应用都可以读取这些信息。可以通过以下三种方式控制哪些应用可以接收广播:
可以在发送广播时指定权限。
在Android 4.0及更高版本中,可以在发送广播时使用setPackage(String)指定软件包。系统会将广播限定到与该软件包匹配的一组应用。
可以使用LocalBroadcastManager发送本地广播。
当注册接收器时,任何应用都可以向应用的接收器发送潜在的恶意广播。可以通过以下三种方式限制应用可以接收的广播:
可以在注册广播接收器时指定权限。
对于清单声明的接收器,可以在清单中将android:exported属性设置为“false”。这样一来,接收器就不会接收来自应用外部的广播。
可以使用LocalBroadcastManager限制应用只接收本地广播。
广播操作的命名空间是全局性的。请确保在自己的命名空间中编写操作名称和其他字符串,否则可能会无意中与其他应用发生冲突。
由于接收器的onReceive(Context, Intent)方法在主线程上运行,因此它会快速执行并返回。如果需要执行长时间运行的工作,请谨慎生成线程或启动后台服务,因为系统可能会在onReceive()返回后终止整个进程。要执行长时间运行的工作,建议:
在接收器的onReceive()方法中调用goAsync(),并将BroadcastReceiver.PendingResult传递给后台线程。这样,在从onReceive()返回后,广播仍可保持活跃状态。不过,即使采用这种方法,系统仍希望非常快速地完成广播(在10秒以内)。为避免影响主线程,它允许将工作移到另一个线程。
使用JobScheduler调度作业。
请勿从广播接收器启动Activity,否则会影响用户体验,尤其是有多个接收器时。相反,可以考虑显示通知。
BroadcastReceiver生命周期只有十秒左右,如果在onReceive()内做超过十秒内的事情,就会报ANR(Application No Response)程序无响应的错误信息,如果需要完成一项比较耗时的工作,应该通过发送Intent给Service, 由Service来完成。这里不能使用子线程来解决,因为BroadcastReceiver的生命周期很短,子线程可能还没有结束BroadcastReceiver就先结束了。BroadcastReceiver一旦结束,此时BroadcastReceiver的所在进程很容易在系统需要内存时被优先杀死,因为它属于空进程(没有任何活动组件的进程)。如果它的宿主进程被杀死,那么正在工作的子线程也会被杀死,所以采用子线程来解决是不可靠的。
动态注册广播接收器还有一个特点,就是当用来注册的Activity关掉后,广播也就失效了。静态注册无需担忧广播接收器是否被关闭,只要设备是开启状态,广播接收器也是打开着的。也就是说哪怕app本身未启动,该app订阅的广播在触发时也会对它起作用系统常见广播Intent,如开机启动、电池电量变化、时间改变等广播。
6 BroadcastReceiver示例
6.1 BroadcastReceiver静态注册示例
这个实例将解释如何创建广播接收器来拦截自定义意图。一旦你熟悉自定义意图,你可以为应用程序编程来拦截系统产生的意图。
步骤 | 描述 |
---|---|
1 | 使用Android Studio来创建Android应用程序并命名为broadcastreceiver,并放在com.ihui.broadcastreceiver包下。 |
2 | 修改主要活动文件MainActivity.java来添加sendBroadcast()方法。 |
3 | 在com.ihui.broadcastreceiver包下创建名为MyReceiver.java的Java文件来定义广播接收器。 |
4 | 应用程序可以处理一个或多个自定义或者系统的意图,没有任何限制。每个想拦截的意图都需要使用<receiver…/>标签在AndroidManifest.xml中注册。 |
5 | 修改res/layout/activity_main.xml文件中的默认内容来包含一个广播意图的按钮。 |
6 | 启动Android模拟器来运行应用程序,并验证应用程序所做改变的结果。 |
6.1.1 创建广播接收器
广播接收器需要实现为BroadcastReceiver类的子类,并重写onReceive()方法来接收以Intent对象为参数的消息,下面是src/com.ihui.broadcastreceiver/MyReceiver.java的内容。
1 | package com.ihui.broadcastreceiver; |
6.1.2 注册广播接收器
应用程序通过在AndroidManifest.xml中注册广播接收器来监听制定的广播意图。假设我们将要注册MyReceiver来监听系统产生的BCR事件。接下来修改AndroidManifest.xml文件。这里通过添加<receiver…/>标签来包含我们的广播接收器。
1 | <?xml version="1.0" encoding="utf-8"?> |
6.1.3 广播自定义意图
下面是修改的主要活动文件src/com.ihui.broadcastreceiver/MainActivity.java的内容。这个文件包含了每个基础的生命周期方法。我们添加了sendBroadcast()方法来广播自定义事件。
1 | package com.ihui.broadcastreceiver; |
6.1.4 设置布局文件
下面是res/layout/activity_main.xml文件的内容,只有一个包含广播自定义意图的按钮。
1 | <?xml version="1.0" encoding="utf-8"?> |
6.1.5 运行效果
Android Studio在AVD上安装应用程序并启动它。如果一切顺利,将在模拟器窗口上显示如图2所示。
现在点击”SEND”按钮来广播我们的自定义意图。这将广播我们的自定义意图BCR,在我们注册的广播接收器MyReceiver中拦截并执行我们实现的逻辑。模拟器的底部将出现Toast,如图3所示。
还可以尝试实现其他的广播接收器来拦截系统产生的意图,如系统启动,日期改变和低电量等。
【注意】注册广播有两种方式,6.1节描述的是静态注册(在XML文件中配置),另外还有动态注册。在Android 8.0及以后,Google对静态注册加了限制,发送广播时,需要指定接收的类。
6.2 BroadcastReceiver动态注册示例
与6.1节静态注册示例不同,动态注册无须在AndroidManifest.xml中配置<receiver>标签,但是其他代码非常类似。
6.2.1 创建广播接收器
1 | package com.ihui.monitoringnetwork; |
6.2.2 广播自定义意图
1 | package com.ihui.monitoringnetwork; |
6.2.3 其他配置
需要在AndroidManifest.xml文件中添加检测网络的权限,代码如下
1 | <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> |
activity_main.xml与6.1.4节的代码类似,只有一个按钮,这里就不放代码了。
6.2.4 运行效果
点击”CLICK”按钮来广播我们的自定义意图。这将广播我们的自定义意图android.net.conn.CONNECTIVITY_CHANGE,在我们注册的广播接收器NetworkChangeReceiver中拦截并执行我们实现的逻辑。模拟器的底部将出现Toast,如图4所示。
7 参考文献
- https://developer.android.google.cn/guide/components/broadcasts?hl=zh-cn
- https://developer.android.google.cn/guide/components/broadcast-exceptions?hl=zh-cn
- https://www.runoob.com/android/android-broadcast-receivers.html
- https://blog.csdn.net/freeking101/article/details/105377197
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达,可以邮件至 xingshuaikun@163.com。