原文链接:https://developer.android.com/guide/components/broadcasts.html
广播
Android应用可以向Android系统和其他Android应用发送或从它们那接收广播消息,这类似于发布-订阅设计模式。当需要关注的事件发生时这些广播就会发送出去。例如Android系统在许多系统事件发生时会发送广播,比如当系统启动或设备开始充电时。应用也可以发送自定义广播,例如通知其他APP他们关注的某些事(例如,下载了一些新数据)。
应用可以通过注册接收特定的一些广播。当广播发送出去,系统会自动将广播路由(routes)到订阅了该特定类型广播的应用程序。
一般来说,广播可以用作应用程序和非常规用户流之间的消息传递系统。但是,你必须注意不要滥用这个响应广播的机会,在后台运行一些会导致系统变慢的任务,正如下面这个视频描述的(译者注:略)。
系统广播
系统在许多系统事件发生时会自动发送广播,比如,当系统打开或关闭飞行模式。系统广播会发送给所有订阅了该事件的应用程序。
广播消息本身封装在一个Intent对象中,它的action字符串区分发生的事件(例如android.intent.action.AIRPLANE_MODE
)。intent可能也包含其他封装在扩展字段(extra field)中的信息。例如,飞行模式的intent包含一个boolean的扩展,它表明了飞行模式是否打开。
更多关于如何解读intents和从intent中获取action字符串,参见Intents and Intent Filterslink.
对于一个完整的系统广播action列表,参见Android SDK里的BROADCAST_ACTIONS.TXT
文件。每个广播action有一个与之关联的常量字段。例如常量ACTION_AIRPLANE_MODE_CHANGED的值是android.intent.action.AIRPLANE_MODE
。每个广播action的文档在其关联的常量字段中是可用的。
系统广播的变化
Android7.0以上不再发送以下系统广播。这项优化影响所有应用程序,不只针对那些Android7.0.
面向Android7.0以上的应用程序,下面这条广播必须使用registerReceiver(BroadcastReceiver, IntentFilter)注册,在manifest声明无效。
从Android8.0(API 26)开始,系统对manifest声明的广播施加了其他限制。如果你的APP面向API26以上版本,对于大多数隐式广播(不是专门针对你的应用的广播),你不能使用manifest声明接收者。
接收广播
应用程序接收广播有两种途径:通过manifest声明接收者和context注册接收者。
Manifest声明的接收者
如果你在manifest中声明了一个广播接收者,系统会在该广播发送时启动你的应用(如果你的应用还没有运行).
注意:如果你的APP面向API26以上,你不能使用manifest为隐式广播(不是专门针对你的应用的广播)声明接收者,除了一些不受限制的隐式广播。在大多数情况下,你可以用计划任务代替。
按如下步骤在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>
- 定义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注册的接收者
按如下步骤使用context注册接收者:
-
创建BroadcastReceiver的一个实例。
BroadcastReceiver br = new MyBroadcastReceiver();
-
创建一个IntentFilter,然后通过调用registerReceiver(BroadcastReceiver, IntentFilter)注册接收者。
IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION); intentFilter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED); this.registerReceiver(br, filter);
注意:要注册本地广播,调用LocalBroadcastManager.registerReceiver(BroadcastReceiver, IntentFilter) 代替。
Context注册的接收者在他们注册用的context有效期间接受广播。例如,如果在Activity context中注册,Activity没有销毁时你可以接收广播。如果你用Application context注册,你在整个应用运行期间可以接收广播。
要停止接收广播,调用unregisterReceiver(android.content.BroadcastReceiver)。请确保在你不再需要它或者context不再有效的时候注销接收者。
注意你注册和注销接收者的地方,例如如果你使用activity的context在onCreate(Bundle)中注册接收者,你应该在onDestroy()中注销以防止将接收者泄漏在activity context之外。如果你在onResume()中注册接收者,你应该在onPause()中注销它以防止重复注册(如果你不想在paused时接收广播,这可以减少不必要的系统开销)。不要在onSaveInstanceState(Bundle)中注销,因为如果用户从历史栈中返回,该方法不会调用。
对进程状态的影响
广告接收者的状态(不管他有没有在运行)对它所在进程的状态有影响,这反过来又会影响它被系统杀死的可能性。例如,进程执行接收者时(即,正在运行onReceive()中的代码时),它被视为一个前台进程。系统保持进程运行,除非在极度的内存压力情形下。
但是一旦你的代码重onReceive()
返回,广播接收者就不再活跃。接收者的宿主进程变得和正在其中运行的其他app组件一样重要( The receiver's host process becomes only as important as the other app components that are running in it).如果那个进程只持有一个manifest声明的接收者(应用程序的常见情形,用户从未或最近未与之交互),那么当从onReceive()返回时,系统认为它的进程是一个低优先级的进程,并且可能会将其杀死,以便为其他更重要的进程提供资源。
由于这个原因,你不应该在广播接收者中开启长时间运行的后台线程。onReceive()
之后,系统可能在任何时候杀死进程回收内存,并且在这过程中,他会终止运行在该进程中的线程。为了避免这样,你应该调用goAsync()(如果你想在后台线程中多花点时间处理处理广播)或者使用JobScheduler从接收者中安排一个JobService,这样系统知道该进程在继续执行积极的工作。更多信息请查看Processes and Application LifeCycle.
下面这个片段展示了一个使用goAsync()的BroadcastReceiver.它表明在onReceive()
完成之后,它需要更多时间去完成工作。这在onReceive()
中你要完成的工作时间长到足以引发UI线程从框架中断开(miss a frame >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)方法发送广播一次给一个接收者。由于每个接收者按顺序执行,它可以将一个结果传递给下一位接收者,或者完全废弃这个广播,从而不会传给其他接收者。接收者的运行顺序可以通过对应的intent-filter的android:priority属性控制;相同优先级的接收者将以任意顺序运行。
- sendBroadcast(Intent)方法以无序的形式给所有接收者发送广播。这称为普通广播。这效率更高,但意味着接收者不能从其他接收者那读取结果,传递从广播接收的数据,或者废弃广播。
-
LocalBroadcastManager.sendBroadcast方法发送广播给与发送者同一APP中的接收者。如果你不需要跨越APP发送广播,可使用本地广播。该实现更加高效,并且你无需担心由于其他app可以接收或发送你的广播而引起的安全问题。
下面的代码片段描述了怎样通过创建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字符串必须提供app的java包名语法,并唯一标志广播事件。你可以使用putExtra(String, Bundle)给intent附加其他信息。你也可以通过在intent上调用setPackage(String)将广播限制在同一组织的app集合。
注意:虽然intents同时用于发送广播和使用startActivity(Intent)启动activity,但这些action完全无关。广播接收者无法看到或捕捉启动一个activity的intent;同样地,当你广播一个intent,你无法找到或者无法启动一个activity.
使用权限限制广播
权限允许你将广播限制在持有特定权限的app集合。你可以对广播的发送者和接收者实施限制。
使用权限发送
当你调用sendBroadcast(Intent,String) 或者 sendOrderedBroadcast(Intent, String, BroadcastReceiver, Handler, int, String, Bundle),你可以指定一个权限参数。只有使用标签在manifest中申请了该权限(如果是危险权限,还要经过允许)的接收者可以接收该广播,例如,下面的代码发送一条广播:
sendBroadcast(new Intent("com.example.NOTIFY"),Manifest.permission.SEND_SMS);
要接收该广播,接收方app必须像下面这样申请该权限:
<uses-permission android:name="android.permission.SEND_SMS"/>
你可以指定像SEND_SMS已经存在的系统权限或者使用<permission>自定义一个权限。有关权限和一般安全,请查看System Permissions.
注意:自定义权限在app安装时注册。定义自定义权限的app必须在使用它的app之前安装。
使用权限接收
如果你在注册广播接收者时指定了一个权限参数(registerReceiver(BroadcastReceiver, IntentFilter, String, Handler)或manifest里的<receiver>标签),那么只有在manifest中使用<uses-permission>标签请求了该权限(如果是危险权限还要经过允许)的广播才能发送intent到该接收者。
例如,假设你的接收者app有一个manifest声明的接收者如下所示:
<receiver android:name=".MyBroadcastReceiver" android:permission="android.permission.SEND_SMS">
<intent-filter>
<action android:name="android.intent.action.AIRPLANE_MODE"/>
</intent-filter>
</receiver>
或者你的接收者app有一个context注册的接收者如下所示:
IntentFilter filter = new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED);
registerReceiver(receiver, filter, Manifest.permission.SEND_SMS, null );
然后,为了能够发送广播给那些接收者,发送者app必须请求该权限,如下所示:
<uses-permission android:name="android.permission.SEND_SMS"/>
安全考虑和最佳实践
这里是一些安全考虑和发送和接收广播最佳实践:
- 如果你不需要发送广播到你的app以外的组件,那么你可以使用LocalBroadcastManager发送和接收本地广播,这在Support Library中也是可用的。LocalBroadcastManager效率更高(不需要跨进程通信)且让你不必思考有关其他app可能接受和发送你的广播的任何安全问题。在你的应用程序中,本地广播可用作通用的pub/sub事件总线而无需任何系统范围的广播。
- 如果许多app在它们的app中注册了相同广播的接收者,这会导致系统启动太多app,对设备性能和用户体验造成实质性影响。为了避免如此,优先使用context注册而不是manifest声明。某些条件下,Android系统本身会强制使用context注册接收者。例如,CONNECTIVITY_ACTION广播只传递给context注册的接收者。
- 不要使用隐式intent广播敏感信息。任意注册了接收该广播的app都能读到这些信息。有三个途径可以控制谁能接收你的广告:
- 发送广播时你可以指定一个权限
- Android4.0以上,发送广播时你可以使用setPackage(String)指定一个package.系统会将广播限制在符合包名的一类app.
- 你可以使用LocalBroadcastManager发送本地广播。
- 当你注册了一个接收者,任意app都可能会发送恶意的广播给你的app的接收者。有三个途径可以限制你的app接收的广播:
- 注册广播接收者时你可以指定权限
- 对于清单声明的接收者,你可以在manifest中设置android:exported属性为“false”。这样接收者不会接收该app意外来源的广播。
- 使用LocalBroadcastManager你可以限制自身只使用本地广播。
- 广播action的命名空间是全局的。确保action名和其他字符串写在一个你自己的命名空间,否则可能在无意中与其他app发生冲突。
- 因为接收者的onReceive(Context, Intent)方法运行在主线程中,它应该快速执行和返回。如果你需要执行耗时任务,注意多线程和启动后台服务,因为当
onReceive()
返回时,整个进程都可能被系统杀掉。更多信息,请查看Effect on process state.要执行耗时任务,我们建议:- 在你的接收者的
onReceive()
方法中调用goAsync(),并且将BroadcastReceiver.PendingResult传到后台线程。这可以让广播从onReceive()
返回之后仍然保持活跃。但是即使使用了该方法,系统仍然希望你能尽快完成广播(10秒钟以下)。它允许你将工作移交给另一个线程以防阻塞主线程。 - 使用JobScheduler安排任务。更多信息,请查看Intelligent Job Scheduling.
- 在你的接收者的
- 不要从广播接收者那启动activity,因为这样的用户体验很糟糕;如果接收者不止一个更是如此。相反,可以考虑显示一个通知。