广播概述

这是我对官方文档的一个渣翻译,兼我的学习笔记,原文在此。
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的应用。

接收广播

应用有两种方式接收广播:通过Manifest-declared receivers和context-registered receivers。

Manifest-declared receivers

如果你在你的manifest中声明了一个广播接收器,当广播被发送时,系统会启动你的应用(即使应用不在运行中)。

如果你的应用针对API level 26或更高版本,则不能在manifest中声明隐式广播的接收器(不仅仅是你的应用的广播),除了少数可以免除此限制的隐式广播。大多数情况下,你可以使用scheduled jobs代替。
按照以下步骤在manifest中声明广播接收器:

  1. 在应用的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。

  1. 创建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注册接收器:

  1. 创建BroadcastReceiver的实例。
BroadcastReceiver br = new MyBroadcastReceiver();
  1. 创建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)
Context-registered接收器只在注册的context有效时接收广播。例如,当你使用Activitycontext注册时,你仅在activity没有被销毁前接收广播。如果你使用Application context注册,那么你只能在应用运行时接收广播。

  1. 调用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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,179评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,229评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,032评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,533评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,531评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,539评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,916评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,813评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,568评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,654评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,354评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,937评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,918评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,152评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,852评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,378评论 2 342

推荐阅读更多精彩内容