广播概述

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的系统广播不包含SSIDBSSID,连接信息或扫描结果。 要获取此信息,请调用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、在应用程序清单中指定<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过滤器指定接收者订阅的广播操作。
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<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-filterandroid: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);

要接收广播,接收应用必须请求权限,如下所示:

<uses-permission android:name="android.permission.SEND_SMS"/>

您可以指定现有系统权限(如SEND_SMS)或使用<permission>元素定义自定义权限。 有关一般权限和安全性的信息,请参阅系统权限。

注意:安装应用程序时会注册自定义权限。 必须在使用它的应用程序之前安装定义自定义权限的应用程序。

接收权限

如果在注册广播接收器时指定了权限参数(使用registerReceiver(BroadcastReceiver,IntentFilter,String,Handler)或清单中的<receiver>标记),则只有使用<uses-permission>请求权限的广播公司 在他们的清单中标记(并且如果它是危险的,则随后被授予许可)可以向接收者发送意图。

例如,假设您的接收应用程序具有清单声明的接收器,如下所示:

<receiver android:name=".MyBroadcastReceiver"
          android:permission="android.permission.SEND_SMS">
    <intent-filter>
        <action android:name="android.intent.action.AIRPLANE_MODE"/>
    </intent-filter>
</receiver>

或者您的接收应用程序有一个上下文注册的接收器,如下所示:

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

推荐阅读更多精彩内容