Android四大组件之Broadcast详解
简介
Android应用可以从Android系统和其他Android应用发送或接收广播消息,类似于发布 - 订阅设计模式。 当感兴趣的事件发生时,发送这些广播。 例如,Android系统在发生各种系统事件时发送广播,例如系统启动或设备开始充电时。 例如,应用程序还可以发送自定义广播,以通知其他应用程序他们可能感兴趣的内容(例如,已下载了一些新数据)。
应用可以注册以接收特定广播。 当发送广播时,系统自动将广播发送到已订阅接收该特定类型广播的应用。
一般而言,广播可以用作跨应用程序和普通用户流程之外的消息传递系统。 但是,您必须小心,不要滥用机会响应广播并在后台运行可能导致系统性能降低。
关于系统广播
当系统发生各种系统事件时,系统会自动发送广播,例如当系统进出飞机模式时。 系统广播将发送到订阅接收事件的所有应用程序。
广播消息本身包含在Intent对象中,该对象的动作字符串标识发生的事件(例如android.intent.action.AIRPLANE_MODE)。 意图还可以包括捆绑到其额外字段中的附加信息。 例如,飞行模式意图包括一个布尔额外值,用于指示飞行模式是否打开。
有关系统广播操作的完整列表,请参阅Android SDK中的BROADCAST_ACTIONS.TXT文件。 每个广播动作都有一个与之相关的常量字段。 例如,常量ACTION_AIRPLANE_MODE_CHANGED的值是android.intent.action.AIRPLANE_MODE。 每个广播操作的文档都在其关联的常量字段中提供。
Android系统发展导致广播的改变
随着Android平台的发展,它会定期更改系统广播的行为方式。 如果您的应用针对Android 7.0(API级别24)或更高版本,或者如果它安装在运行Android 7.0或更高版本的设备上,请记住以下更改。
-
Android 9
从Android 9(API级别28)开始,NETWORK_STATE_CHANGED_ACTION广播不会接收有关用户位置或个人身份识别数据的信息。此外,如果您的应用安装在运行Android 9或更高版本的设备上,则来自Wi-Fi的系统广播不包含SSID,BSSID,连接信息或扫描结果。 要获取此信息,请调用getConnectionInfo()。
-
Android 8.0
从Android 8.0(API级别26)开始,系统对清单声明的接收器施加了额外的限制。如果您的应用针对的是Android 8.0或更高版本,则无法使用清单为大多数隐式广播声明接收方(广告不会专门针对您的应用)。 当用户主动使用您的应用程序时,您仍然可以使用上下文注册的接收器。
-
Android 7.0
Android 7.0(API级别24)及更高版本不发送以下系统广播:- ACTION_NEW_PICTURE
- ACTION_NEW_VIDEO
此外,针对Android 7.0及更高版本的应用必须使用registerReceiver(BroadcastReceiver,IntentFilter)注册CONNECTIVITY_ACTION广播。 在清单中声明接收器不起作用。
接收广播
应用程序可以通过两种方式接收广播:通过清单声明的接收器和上下文注册的接收器。
清单注册接收
如果您在清单中声明了广播接收器,系统会在发送广播时启动您的应用(如果应用尚未运行)。
注意:如果您的应用程序的目标是API级别26或更高级别,则您不能使用清单来声明隐式广播的接收者(特定地不针对您的应用的广播),除了一些免于该限制的隐式广播。
要在清单中声明广播接收器,请执行以下步骤:
- 在应用程序清单中指定<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过滤器指定接收者订阅的广播操作。
2. 继承BroadcastReceiver并实现onReceive(Context,Intent)。 以下示例中的广播接收器记录并显示广播的内容:
public class MyBroadcastReceiver extends BroadcastReceiver {
private static final String TAG = "MyBroadcastReceiver";
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context,
"Action: " + intent.getAction() + "\n"+
"URI: " + intent.toUri(Intent.URI_INTENT_SCHEME).toString() + "\n"
, Toast.LENGTH_LONG).show();
}
}
系统软件包管理器在安装应用程序时注册接收器。 然后,接收器成为应用程序的单独入口点,这意味着如果应用程序当前未运行,系统可以启动应用程序并发送广播。
系统创建一个新的BroadcastReceiver组件对象来处理它接收的每个广播。 此对象仅在调用onReceive(Context,Intent)期间有效。 一旦您的代码从此方法返回,系统会认为该组件不再处于活动状态。
上下文注册的接收器
要使用上下文注册接收器,请执行以下步骤:
- 创建BroadcastReceiver的实例。
BroadcastReceiver br = new MyBroadcastReceiver();
- 创建一个IntentFilter并通过调用registerReceiver(BroadcastReceiver,IntentFilter)来注册接收器:
IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
filter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED);
this.registerReceiver(br, filter);
注意:要注册本地广播,请调用LocalBroadcastManager.registerReceiver(BroadcastReceiver,IntentFilter)。
只要注册上下文有效,上下文注册的接收器就会接收广播。 例如,如果您在“活动”上下文中注册,则只要活动未被销毁,您就会收到广播。 如果您在应用程序上下文中注册,则只要应用程序正在运行,您就会收到广播。
-
要停止接收广播,请调用unregisterReceiver(android.content.BroadcastReceiver)。 当您不再需要接收器或上下文不再有效时,请务必取消注册接收器。
请注意注册和取消注册接收器的位置,例如,如果使用活动的上下文在onCreate(Bundle)中注册接收器,则应在onDestroy()中取消注册,以防止接收器泄漏到活动上下文之外。 如果在onResume()中注册接收器,则应在onPause()中取消注册,以防止多次注册(如果您不想在暂停时接收广播,这可以减少不必要的系统开销)。 不要在onSaveInstanceState(Bundle)中取消注册,因为如果用户在历史堆栈中向后移动,则不会调用此方法。
进程内存过低对广播的影响
BroadcastReceiver的状态(无论是否正在运行)会影响其包含进程的状态,进而影响其被系统杀死的可能性。 例如,当进程执行接收器(即,当前在其onReceive()方法中运行代码)时,它被认为是前台进程。 除极端内存压力外,系统保持运行。
但是,一旦您的代码从onReceive()返回,BroadcastReceiver就不再处于活动状态。 接收方的主机进程与其中运行的其他应用程序组件一样重要。 如果该进程仅承载清单声明的接收者(用户从未或最近未与之交互的应用程序的常见情况),则在从onReceive()返回时,系统将其进程视为低优先级进程并且可能 杀死它以使资源可用于其他更重要的过程。
因此,您不应该从广播接收器开始长时间运行后台线程。 在onReceive()之后,系统可以随时终止进程以回收内存,并且这样做会终止在进程中运行的生成线程。 要避免这种情况,您应该调用goAsync()(如果您希望在后台线程中处理广播更多时间)或使用JobScheduler从接收器调度JobService,以便系统知道该进程继续执行活动 工作。 有关更多信息,请参阅进程和应用程序生命周期。
以下代码段显示了一个BroadcastReceiver,它使用goAsync()标记在onReceive()完成后需要更多时间才能完成。 如果要在onReceive()中完成的工作足够长,导致UI线程错过一个帧(> 16ms),使其更适合后台线程,这将特别有用。
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 {
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);
// 必须调用finish()才能回收BroadcastReceiver。
pendingResult.finish();
}
}
}
发送广播
Android为应用发送广播提供了三种方式:
-
sendOrderedBroadcast(Intent,String)方法一次向一个接收器发送广播。当每个接收器依次执行时,它可以将结果传播到下一个接收器,或者它可以完全中止广播,以便它不会传递给其他接收器。运行的订单接收器可以使用匹配的intent-filter的android:priority属性进行控制;具有相同优先级的接收器将以任意顺序运行。
-
sendBroadcast(Intent)方法以未定义的顺序向所有接收器发送广播。这称为正常广播。这更有效,但意味着接收器无法从其他接收器读取结果,传播从广播接收的数据或中止广播。
-
LocalBroadcastManager.sendBroadcast方法将广播发送到与发送方位于同一应用程序中的接收方。如果您不需要跨应用程序发送广播,请使用本地广播。实现效率更高(无需进程间通信),您无需担心与其他应用程序能够接收或发送广播相关的任何安全问题。
以下代码片段演示了如何通过创建Intent并调用sendBroadcast(Intent)来发送广播。
Intent intent = new Intent();
intent.setAction("com.example.broadcast.MY_NOTIFICATION");
intent.putExtra("data","Notice me senpai!");
sendBroadcast(intent);
广播消息包含在Intent对象中。 intent的操作字符串必须提供应用程序的Java包名称语法,并唯一标识广播事件。 您可以使用putExtra(String,Bundle)将其他信息附加到intent。 您还可以通过在intent上调用setPackage(String)将广播限制为同一组织中的一组应用程序。
注意:虽然意图用于发送广播和使用startActivity(Intent)启动活动,但这些操作完全不相关。 广播接收器无法查看或捕获用于启动活动的意图; 同样,当您广播意图时,您无法找到或开始活动。
限制具有权限的广播
权限允许您将广播限制为具有特定权限的应用程序集。 您可以对广播的发送者或接收者实施限制。
发送附带权限
当您调用sendBroadcast(Intent,String)或sendOrderedBroadcast(Intent,String,BroadcastReceiver,Handler,int,String,Bundle)时,您可以指定权限参数。 只有那些已经在其清单中请求带有标签的许可的接收者(并且如果它是危险的,则随后被授予许可)可以接收广播。 例如,以下代码发送广播:
sendBroadcast(new Intent("com.example.NOTIFY"),
Manifest.permission.SEND_SMS);
要接收广播,接收应用必须请求权限,如下所示:
<uses-permission android:name="android.permission.SEND_SMS"/>
您可以指定现有系统权限(如SEND_SMS)或使用<permission>元素定义自定义权限。 有关一般权限和安全性的信息,请参阅系统权限。
注意:安装应用程序时会注册自定义权限。 必须在使用它的应用程序之前安装定义自定义权限的应用程序。
接收权限
如果在注册广播接收器时指定了权限参数(使用registerReceiver(BroadcastReceiver,IntentFilter,String,Handler)或清单中的<receiver>标记),则只有使用<uses-permission>请求权限的广播公司 在他们的清单中标记(并且如果它是危险的,则随后被授予许可)可以向接收者发送意图。
例如,假设您的接收应用程序具有清单声明的接收器,如下所示:
<receiver android:name=".MyBroadcastReceiver"
android:permission="android.permission.SEND_SMS">
<intent-filter>
<action android:name="android.intent.action.AIRPLANE_MODE"/>
</intent-filter>
</receiver>
或者您的接收应用程序有一个上下文注册的接收器,如下所示:
IntentFilter filter = new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED);
registerReceiver(receiver, filter, Manifest.permission.SEND_SMS, null );
然后,为了能够向这些接收者发送广播,发送应用必须请求权限,如下所示:
<uses-permission android:name="android.permission.SEND_SMS"/>
安全注意事项和最佳做法:
-
如果您不需要向应用程序外部的组件发送广播,则使用支持库中提供的LocalBroadcastManager发送和接收本地广播。 LocalBroadcastManager效率更高(无需进程间通信),并允许您避免考虑与其他应用程序相关的任何安全问题,以便能够接收或发送您的广播。 本地广播可以在您的应用中用作通用发布/子事件总线,而无需系统广播的任何开销。
-
如果许多应用已注册在其清单中接收相同的广播,则可能导致系统启动大量应用,从而对设备性能和用户体验产生重大影响。 为避免这种情况,请优先使用上下文注册而不是清单声 有时,Android系统本身会强制使用上下文注册的接收器。 例如,CONNECTIVITY_ACTION广播仅传递给上下文注册的接收器。
- 您可以在发送广播时指定权限。
- 在Android 4.0及更高版本中,您可以在发送广播时指定包含setPackage(String)的包。 系统将广播限制为与包匹配的应用程序集。
- 您可以使用LocalBroadcastManager发送本地广播。
-
当您注册接收器时,任何应用都可以向您的应用接收器发送潜在的恶意广播。 有三种方法可以限制应用收到的广播:
- 您可以在注册广播接收器时指定权限。
- 对于清单声明的接收器,您可以在清单中将android:exported属性设置为“false”。 接收方不接收来自应用程序之外的来源的广播。
- 您可以将自己限制为仅使用LocalBroadcastManager.anifest进行本地广播。 接收方不接收来自应用程序之外的来源的广播。
-
广播操作的命名空间是全局的。 确保将动作名称和其他字符串写入您拥有的命名空间中,否则您可能会无意中与其他应用程序发生冲突。
-
因为接收者的onReceive(Context,Intent)方法在主线程上运行,所以它应该快速执行并返回。 如果您需要执行长时间运行的工作,请小心生成线程或启动后台服务,因为系统可以在onReceive()返回后终止整个进程。 有关更多信息,请参阅对进程状态的影响要执行长时间运行的工作,我们建议:
- 在接收者的onReceive()方法中调用goAsync()并将BroadcastReceiver.PendingResult传递给后台线程。 这使得从onReceive()返回后广播保持活动状态。 但是,即使采用这种方法,系统也希望您能够非常快速地完成广播(10秒以内)。 它允许您将工作移动到另一个线程以避免故障主线程。
- 使用JobScheduler安排作业。 有关更多信息,请参阅智能作业计划。
-
不要从广播接收器开始活动,因为用户体验很不稳定; 特别是如果有多个接收器。 相反,请考虑显示通知。
系统的隐式广播
作为Android 8.0(API级别26)后台执行限制的一部分,针对API级别26或更高级别的应用程序无法再在其清单中为隐式广播注册广播接收器。 但是,目前有几个广播免于这些限制。 无论应用程序所针对的API级别如何,应用程序都可以继续为以下广播注册监听器。
注意:即使这些隐式广播仍然在后台工作,您应该避免为它们注册侦听器。
-
ACTION_LOCKED_BOOT_COMPLETED, ACTION_BOOT_COMPLETED
免除,因为这些广播仅在首次启动时发送一次,并且许多应用需要接收此广播以安排作业,警报等。 -
ACTION_USER_INITIALIZE
“android.intent.action.USER_ADDED”, “android.intent.action.USER_REMOVED”
这些广播受特权权限保护,因此大多数普通应用程序无论如何都无法接收它们。“android.intent.action.TIME_SET”, ACTION_TIMEZONE_CHANGED, ACTION_NEXT_ALARM_CLOCK_CHANGED当时间,时区或警报发生变化时,时钟应用可能需要接收这些广播以更新警报。 -
ACTION_LOCALE_CHANGED
仅在区域设置更改时发送,这不常见。 应用可能需要在区域设置更改时更新其数据。 -
ACTION_USB_ACCESSORY_ATTACHED, ACTION_USB_ACCESSORY_DETACHED, ACTION_USB_DEVICE_ATTACHED, ACTION_USB_DEVICE_DETACHED
如果应用程序需要了解这些与USB相关的事件,目前还没有一个很好的替代方案来注册广播。 -
ACTION_CONNECTION_STATE_CHANGED, ACTION_CONNECTION_STATE_CHANGED, ACTION_ACL_CONNECTED, ACTION_ACL_DISCONNECTED
如果应用接收这些蓝牙事件的广播,则用户体验不太可能受到影响。 -
ACTION_CARRIER_CONFIG_CHANGED, TelephonyIntents.ACTION_*_SUBSCRIPTION_CHANGED, “TelephonyIntents.SECRET_CODE_ACTION”, ACTION_PHONE_STATE_CHANGED, ACTION_PHONE_ACCOUNT_REGISTERED, ACTION_PHONE_ACCOUNT_UNREGISTERED
OEM电话应用可能需要接收这些广播。 -
LOGIN_ACCOUNTS_CHANGED_ACTION
某些应用需要了解登录帐户的更改,以便他们可以为新帐户和已更改帐户设置计划操作。 -
ACTION_ACCOUNT_REMOVED
删除帐户后,可以看到帐户的应用会收到此广播。 如果这是应用程序需要执行的唯一帐户更改,则强烈建议应用程序使用此广播而不是已弃用的LOGIN_ACCOUNTS_CHANGED_ACTION。 -
ACTION_PACKAGE_DATA_CLEARED
仅在用户明确清除“设置”中的数据时发送,因此广播接收器不太可能显着影响用户体验。 -
ACTION_PACKAGE_FULLY_REMOVED
某些应用可能需要在删除其他包时更新其存储的数据; 对于这些应用程序,注册此广播没有其他好的选择。
注意:其他与包相关的广播(例如ACTION_PACKAGE_REPLACED)不受新限制的豁免。 这些广播很常见,对豁免它们有潜在的性能影响。
-
ACTION_NEW_OUTGOING_CALL
响应用户拨打电话而采取措施的应用需要接收此广播。 -
ACTION_DEVICE_OWNER_CHANGED
这种广播不经常发送; 一些应用需要接收它,以便他们知道设备的安全状态已经改变。 -
ACTION_MEDIA_MOUNTED,ACTION_MEDIA_CHECKING,ACTION_MEDIA_UNMOUNTED,ACTION_MEDIA_EJECT,ACTION_MEDIA_UNMOUNTABLE,ACTION_MEDIA_REMOVED,ACTION_MEDIA_BAD_REMOVAL
这些广播是由于用户与设备的物理交互(安装或删除存储卷)或作为启动初始化的一部分(因为可用卷已安装)而发送的,因此它们不常见,通常由用户控制。 -
SMS_RECEIVED_ACTION,WAP_PUSH_RECEIVED_ACTION
SMS收件人应用程序依赖这些广播。