Broadcasts 广播

原文链接: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中声明广播接收者:

  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>
    
  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)期间有效。一旦你的代码从该方法返回,系统就认为该组件不再活跃。

经Context注册的接收者

按如下步骤使用context注册接收者:

  1. 创建BroadcastReceiver的一个实例。

    BroadcastReceiver br = new MyBroadcastReceiver();
    
  2. 创建一个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注册,你在整个应用运行期间可以接收广播。

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

推荐阅读更多精彩内容