这是我对官方文档的一个渣翻译,兼我的学习笔记,原文在此。
Android app可以从Android系统和其他Android应用发送或接收广播信息,类似发布-关注设计模式。当关注的事件发生时就会发送广播。举个例子,当各种系统事件发生时发送广播,比如系统启动或设备开始充电。应用也可以发送自定义广播,例如,通知其他应用他们可能关注的东西(比如一些新数据被下载了)。
应用可以通过注册来接收特定的广播。当广播被发送时,系统会自动将广播路由到这些订阅接收这类广播的应用。
通常来说,广播可以用作跨应用和正常用户流之外的消息系统。但是,你要小心不要滥用在后台响应广播和运行作业的机会,它会导致系统性能变慢。
关于系统广播
各种系统事件发生时,系统会自动发送广播,例如当系统切入或切出飞行模式。系统广播会被发送到所有订阅接收这个事件的应用。
广播信息自身被包装在一个Intent对象中,这个对象的action字符标识了发生的事件(例如android.intent.action.AIRPLANE_MODE
)。这个intent还可能包含了被捆绑了附加信息的其他域。例如,飞行模式intent包含了一个boolean类型的附加物来表示飞行模式是开还是关。
了解更多关于如何读取intents和从intent中获取action字符的信息,查看Intents and Intent Filters。
完整的系统广播action列表,查看Android SDK中的BROADCAST_ACTIONS.TXT文件。每个广播action关联了一个一个常量域。例如,ACTION_AIRPLANE_MODE_CHANGED的常量值是android.intent.action.AIRPLANE_MODE。每个广播action的文档都可以在它关联的常量域中获得。
系统广播的变化
Android 7.0 以及更高版本不再需要发送以下系统广播。此优化会影响所有应用,而不仅仅是针对Android 7.0的应用。
- ACTION_NEW_PICTURE
-
ACTION_NEW_VIDEO
针对Android 7.0 (API level 24) 以及更高版本的应用必须要使用registerReceiver(BroadcastReceiver, IntentFilter)
来注册以下广播。在manifest中声明receiver不再起作用。 -
CONNECTIVITY_ACTION
从Android 8.0 (API level 26) 起,系统对manifest声明的receiver增加了额外的限制。如果你的应用针对API26或更高的版本,则不能使用manifest来声明大多数隐式广播的receiver(不仅仅是应用的广播)。
接收广播
应用有两种方式接收广播:通过Manifest-declared receivers和context-registered receivers。
Manifest-declared receivers
如果你在你的manifest中声明了一个广播接收器,当广播被发送时,系统会启动你的应用(即使应用不在运行中)。
如果你的应用针对API level 26或更高版本,则不能在manifest中声明隐式广播的接收器(不仅仅是你的应用的广播),除了少数可以免除此限制的隐式广播。大多数情况下,你可以使用scheduled jobs代替。
按照以下步骤在manifest中声明广播接收器:
- 在应用的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 filters指定了应用要订阅的广播action。
- 创建
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)
的期间中有效。一旦代码从这个方法返回,系统就会认为这个组件不再处于活动状态。
Context-registered receivers
根据以下步骤通过context注册接收器:
- 创建
BroadcastReceiver
的实例。
BroadcastReceiver br = new MyBroadcastReceiver();
IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
filter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED);
this.registerReceiver(br, filter);
注意:要注册本地广播,调用
LocalBroadcastManager.registerReceiver(BroadcastReceiver, IntentFilter)
。
Context-registered接收器只在注册的context有效时接收广播。例如,当你使用Activity
context注册时,你仅在activity没有被销毁前接收广播。如果你使用Application context注册,那么你只能在应用运行时接收广播。
- 调用
unregisterReceiver(android.content.BroadcastReceiver)
停止接收广播。要确认在你不再需要广播时,或context不再存在时注销接收器。
要注意你注册和注销接收器的位置。例如,当你在onCreate(Bundle)
中使用activity context注册接收器,那么你就要在onDestroy()
中注销,防止接收器从activity context中泄露。如果你在onResume()
中注册接收器,那么你就要在onPause()
中注销以防止多次注册(如果你不想在暂停时接收广播,并且它会减少不必要的系统开销)。不要在onSaveInstanceState(Bundle)
中注销,因为如果用户移回到历史堆栈,它就不会被调用。
对进程状态的影响
BroadcastReceiver
(不论是否运行)影响了它包含的进程的状态,进而影响系统杀死它的可能性。例如,当一个进程执行接收器(准确来说,是运行它的onReceive()
方法中的代码)时,它会被当做前台进程。系统会一直保持进程除非内存压力过大。
然而,只要你的代码从onReceive()
中返回,BroadcastReceiver 将不再活动。接收器的主机进程的重要性变得和正在运行的其他应用组件一样。如果这个进程主机只是manifest-declared 接收器(应用中用户从未或最近没有与其交互的普通组件),则从onReceive()
返回时,系统会认为这个进程是一个优先级很低的进程,并很有可能会杀死它,让其他重要的进程获得更多资源。
因为这个原因,你不能再广播接收器中开启长时间运行的后台应用。在onReceive()后,系统会在任意时间杀死这个进程来释放内存,并且,它会中止这个进程中的衍生
线程。为了避免这种情况发生,你应该调用goAsync()
(如果你想在后台线程中多用一点时间处理广播),或者在接收器中使用 JobScheduler
来调度JobService
,系统就会知道进程会继续执行有效的工作。要了解更多信息,查看Processes and Application Life Cycle。
以下片段显示了一个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<String, Integer, String> asyncTask = new AsyncTask<String, Integer, String>() {
@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)
方法在一个时间向一个接收器发送广播。当每个接收器依次执行时,它可以将结果发送到下一个接收器,它也可以完全中止广播,使其不会被传递给其他广播。 -
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的action string必须提供这个应用的java包名句法并且唯一定义广播事件名。你可以使用putExtra(String, Bundle)
向intent中附加额外的信息。你还可以通过调用intent的setPackage(String)
方法,将广播限制在同一组织中的同一组应用。
虽然intent既可以用来发送广播,又可以用
startActivity(Intent)
来启动activity,但这两种行为是完全无关的。广播接收器无法看到或捕获用来启动activity的intent,同样,当你广播intent时,你也无法找到或启动一个activity。
限制有权限的广播
权限允许你对有特定权限的一系列应用限制广播。你可以对广播的发送者或接收者实施限制。
权限发送
当你调用sendBroadcast(Intent, String)
或sendOrderedBroadcast(Intent, String, BroadcastReceiver, Handler, int, String, Bundle)
时,你可以制定一个权限参数。只有在manifest中满足权限的接收器(如果是危险权限并在后来被授权)才可以接收这个广播。例如,下面的代码发送了一个广播:
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)
还是在manifest中声明<receiver>
),只有使用<uses-permission>
在manifest中申请权限的广播(如果是危险权限并在后来被授权可以发送一个intent到接收器中。
例如,确定你的接收应用有如下的manifest声明接收器:
<receiver android:name=".MyBroadcastReceiver"
android:permission="android.permission.SEND_SMS">
<intent-filter>
<action android:name="android.intent.action.AIRPLANE_MODE"/>
</intent-filter>
</receiver>
或者你的接收应用有如下的context-registered接收器:
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
发送、接收本地广播,它可以从Support Library中获得。LocalBroadcastManager
更有效(不需要进程间通信),并且你可以不用担心别的应用接收或发送你的广播的相关安全问题。本地广播可以在你的应用中作为通用的发布/订阅的事件总线,而不需要系统广播的任何开销。 - 如果有很多应用都在它们的manifest中注册了要接收同一个广播,会导致系统启动过多应用,对设备性能和用户体验都造成重大影响。为了避免这种情况,最好使用context注册替代manifest声明。有时Android系统本身会强制使用context-registered接收器。例如
CONNECTIVITY_ACTION
广播就只能使用context-registered发送给接收器。 - 不要使用隐式intent广播敏感信息。任何注册了要接收这个广播的应用都可以读取这个信息。这里有三种方式可以限制接收你的广播。
- 你可以在发送广播时指定权限。
- 在Android 4.0或更高版本,你可以在发送广播时使用
setPackage(String)
指定一个包。系统将广播限制为匹配了这个包的一系列应用。 - 你可以使用
LocalBroadcastManager
发送本地广播。
- 当你注册接收器时,任何应用都可以发送潜在的恶意广播到接收器中。这里有三种方法可以限制你的应用接收的广播:
- 你可以在注册广播接收器时指定权限。
- 对于manifest-declared接收器,你可以在manifest中设置android:exported属性为false。接收器就不会接收应用外部的广播。
- 你可以通过
LocalBroadcastManager
限制你的应用。
- 广播action的命名空间是全局的。确认action名和其他字符是依照你自己的命名空间写的,否则你可能会无意中与其他应用发生冲突。
- 因为接收器的
onReceive(Context, Intent)
方法在主线程中运行,它应该快速执行并返回。如果你需要执行长时间的工作,要注意派生线程或启动后台服务,因为系统会在onReceive()
返回后杀死整个进程。了解更多信息,查看Effect on process state来执行长时间的工作,我们建议:- 在你接收器的
onReceive()
方法中调用goAsync()
并将BroadcastReceiver.PendingResult
传递给后台线程。它会保持广播在onReceive()
返回后继续活动。但是,即使使用这种方法,系统也希望你尽快结束广播(10秒内)。它还允许你将工作转移到另一个线程以避免妨碍主线程。 - 使用
JobScheduler
调度作业。了解更多信息,查看Intelligent Job Scheduling。
- 在你接收器的
- 不要从广播接收器打开活动。因为这样的用户体验很突兀。尤其是当这里有多个接收器时。作为替代,请考虑显示一个notification。