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 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或更高级别,则您不能使用清单来声明隐式广播的接收者(特定地不针对您的应用的广播),除了一些免于该限制的隐式广播。 在大多数情况下,您可以使用预定作业。
要在清单中声明广播接收器,请执行以下步骤:
1、在应用程序清单中指定
元素。
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) {
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();
}
}
系统软件包管理器在安装应用程序时注册接收器。 然后,接收器成为应用程序的单独入口点,这意味着如果应用程序当前未运行,系统可以启动应用程序并发送广播。
系统创建一个新的BroadcastReceiver
组件对象来处理它接收的每个广播。 此对象仅在调用onReceive(Context,Intent)
期间有效。 一旦您的代码从此方法返回,系统会认为该组件不再处于活动状态。
上下文注册的接收器
要使用上下文注册接收器,请执行以下步骤:
1、创建BroadcastReceiver的实例。
BroadcastReceiver br = new MyBroadcastReceiver();
2、创建一个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)
。
只要注册上下文有效,上下文注册的接收器就会接收广播。 例如,如果您在“活动”上下文中注册,则只要活动未被销毁,您就会收到广播。 如果您在应用程序上下文中注册,则只要应用程序正在运行,您就会收到广播。
3、要停止接收广播,请调用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(final Context context, final Intent intent) {
final PendingResult pendingResult = goAsync();
AsyncTask asyncTask = new AsyncTask() {
@Override
protected String doInBackground(String... params) {
StringBuilder sb = new StringBuilder();
sb.append("Action: " + intent.getAction() + "\n");
sb.append("URI: " + intent.toUri(Intent.URI_INTENT_SCHEME).toString() + "\n");
Log.d(TAG, log);
// Must call finish() so the BroadcastReceiver can be recycled.
pendingResult.finish();
return data;
}
};
asyncTask.execute();
}
}
三、发送广播
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);
要接收广播,接收应用必须请求权限,如下所示:
您可以指定现有系统权限(如SEND_SMS
)或使用
元素定义自定义权限。 有关一般权限和安全性的信息,请参阅系统权限。
注意:安装应用程序时会注册自定义权限。 必须在使用它的应用程序之前安装定义自定义权限的应用程序。
接收权限
如果在注册广播接收器时指定了权限参数(使用registerReceiver(BroadcastReceiver,IntentFilter,String,Handler)
或清单中的
标记),则只有使用
请求权限的广播公司 在他们的清单中标记(并且如果它是危险的,则随后被授予许可)可以向接收者发送意图。
例如,假设您的接收应用程序具有清单声明的接收器,如下所示:
或者您的接收应用程序有一个上下文注册的接收器,如下所示:
IntentFilter filter = new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED);
registerReceiver(receiver, filter, Manifest.permission.SEND_SMS, null );
然后,为了能够向这些接收者发送广播,发送应用必须请求权限,如下所示:
五、安全考虑因素和最佳实践
以下是发送和接收广播的一些安全注意事项和最佳做法:
- 如果您不需要向应用程序外部的组件发送广播,则使用支持库中提供的
LocalBroadcastManager
发送和接收本地广播。LocalBroadcastManager
效率更高(无需进程间通信),并允许您避免考虑与其他应用程序相关的任何安全问题,以便能够接收或发送您的广播。 本地广播可以在您的应用中用作通用发布/子事件总线,而无需系统广播的任何开销。 - 如果许多应用已注册在其清单中接收相同的广播,则可能导致系统启动大量应用,从而对设备性能和用户体验产生重大影响。 为避免这种情况,请优先使用上下文注册而不是清单声 有时,
Android
系统本身会强制使用上下文注册的接收器。 例如,CONNECTIVITY_ACTION
广播仅传递给上下文注册的接收器。 - 不要使用隐式意图广播敏感信息。 任何注册接收广播的应用都可以读取该信息。 有三种方法可以控制谁可以接收您的广播:
(1)您可以在发送广播时指定权限。
(2)在Android 4.0
及更高版本中,您可以在发送广播时指定包含setPackage(String)
的包。 系统将广播限制为与包匹配的应用程序集。
(3)您可以使用LocalBroadcastManager
发送本地广播。 - 当您注册接收器时,任何应用都可以向您的应用接收器发送潜在的恶意广播。 有三种方法可以限制应用收到的广播:
(1)您可以在注册广播接收器时指定权限。
(2)对于清单声明的接收器,您可以在清单中将android:exported
属性设置为“false”
。 接收方不接收来自应用程序之外的来源的广播。
(3)您可以将自己限制为仅使用LocalBroadcastManager
进行本地广播。 - 广播操作的命名空间是全局的。 确保操作名称和其他字符串都写在您拥有的命名空间中,否则您可能会无意中与其他应用程序发生冲突。
- 因为接收者的
onReceive(Context,Intent)
方法在主线程上运行,所以它应该快速执行并返回。 如果您需要执行长时间运行的工作,请小心生成线程或启动后台服务,因为系统可以在onReceive()
返回后终止整个进程。 有关更多信息,请参阅对进程状态的影响要执行长时间运行的工作,我们建议:
(1)在接收者的onReceive()
方法中调用goAsync()
并将BroadcastReceiver.PendingResult
传递给后台线程。 这使得从onReceive()
返回后广播保持活动状态。 但是,即使采用这种方法,系统也希望您能够非常快速地完成广播(10秒以内)。 它允许您将工作移动到另一个线程,以避免故障主线程。
(2)使用JobScheduler
安排作业。 有关更多信息,请参阅智能作业计划。 - 不要从广播接收器开始活动,因为用户体验很不稳定; 特别是如果有多个接收器。 相反,请考虑显示通知。