广播接收器(BroadcastReceiver)

  • 现实中的广播:电台为了传达一些消息而发送广播,通过广播携带要传达的消息,群众只要买一个收音机,就可以收到广播了。

  • Android中的广播:Android系统在运行过程中,会发生很多事件,比如电量改变、耳机插入、收到短信、拨打电话、屏幕解锁、系统开机等,为了让App知道事件的发生,系统会发送该事件的广播,App只要注册一个BroadcastReceiver,就可以接收到对应的广播,以便做出响应。

在Android系统中,广播是进行进程间通信(IPC)的重要手段,所以Android系统为我们提供了BroadcastReceiver来接收程序(包括我们开发的程序和系统内建的程序)所发出的广播(实际上是Broadcast Intent)

本文所提到的广播主要是指全局广播,而对于进程内通信,建议使用局部广播 LocalBroadcastManger

各种OnXxxListener只是程序级别的监听器,这些监听器运行在指定程序所在进程中,当程序退出时,OnXxxListener监听器也随之关闭;而BroadcastReceiver属于系统级的监听器,在Android 4.0以前,对于静态注册的BroadcastReceiver,只要存在与之匹配的Intent被广播出来就会被触发它,即便它所在的应用程序还没有启动,系统也会启动这个应用程序

广播接收器的创建

1. 定义

创建一个类XxxReceiver继承于BroadcastReceiver,并重写onReceive()

public class XxxReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {

    }
}
2. 注册
  • 静态注册(常驻)

    静态注册就是在AndroidManifest中注册BroadcastReceiver,并指定它所接收的广播种类,如下面配置的XxxReceiver用来接收开机广播

    <receiver android:name=".XxxReceiver">
        <intent-filter>
            <action android:name="android.intent.action.BOOT_COMPLETED" />
        </intent-filter>
    </receiver>
    

使用静态注册的BroadcastReceiver,在Android 4.0以前,只要app被安装,它就会一直生效;而Android 4.0之后,在app安装后还需要先启动一次,它才会生效,并且如果用户在应用管理界面手动杀死了这个BroadcastReceiver所在进程,那么它将不在生效,直到用户下一次启动app,才会再次生效。但是如果是系统在内存不足时自动杀死了这个BroadcastReceiver所在进程,它仍然还是生效的。
BroadcastReceiver一旦生效,每次与之匹配的Intent被广播出来,系统就会创建对应的BroadcastReceiver实例,并自动触发它的onReceive()方法,onReceive()方法执行完后,BroadcastReceiver实例就会被销毁(生命周期结束)。

  • 动态注册(非常驻)

    动态注册是指在Java代码中注册BroadcastReceiver,并通过IntentFilter来指定它接收的广播种类。
    使用动态注册BroadcastReceiver,通常是在onResume()中使用registerReceiver(xxxReceiver, intentFilter)注册它,在onPause()使用unregisterReceiver(xxxReceiver)注销它,注销之后BroadcastReceiver立即失效,这样可以有效的节约系统消耗。下面代码动态注册的XxxReceiver用于接收屏幕开关广播:

    @Override
    protected void onResume() {
        super.onResume();
        receiver = new XxxReceiver(); 
        IntentFilter intentFilter = new IntentFilter();   // 用于指定接收广播的类型
        intentFilter.addAction(Intent.ACTION_SCREEN_ON);  // 屏幕点亮
        intentFilter.addAction(Intent.ACTION_SCREEN_OFF); // 屏幕熄灭
        registerReceiver(receiver, intentFilter);  // 注册广播接收器
    }
    
    @Override
    protected void onPause() {
        unregisterReceiver(receiver);  // 注销广播接收器
        super.onPause();
    }
    

    有些特殊的广播,必须使用动态注册的BroadcastReceiver来接收,比如:

    • 屏幕开关
    • 电量改变
    • 耳机插拔

    如果一个BroadcastReceiver用于更新UI,那么通常会使用动态注册。

接收系统发出的广播

IP拨号器

拨打电话时,系统会发出一个广播,广播中携带着用户所要拨打的号码。
我们可以创建一个BroadcastReceiver接收这个广播,然后取出广播中携带的号码进行修改(这里是加上线路号码),然后把修改后的号码放回广播。

在IP拨号器中定义广播接收器接收打电话广播

public class CallReceiver extends BroadcastReceiver {
    
    /**
     * 当广播接收器接收到广播时,此方法会调用
     * @param context
     * @param intent
     */
    @Override
    public void onReceive(Context context, Intent intent) {
        String number = getResultData();  // 拿到用户拨打的号码
        setResultData("17951" + number);  // 修改广播内的号码
    }
}

在清单文件中静态注册该receiver并定义它接收的广播类型

<receiver android:name=".CallReceiver">
    <intent-filter >
        <action android:name="android.intent.action.NEW_OUTGOING_CALL" />
    </intent-filter>
</receiver>

不要忘了申请接收打电话广播所需要的权限

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

短信拦截器

Android系统在收到短信时会发送一条广播,广播中携带着短信的源号码和内容。
我们可以写一个短信拦截器app,在程序中定义一个BroadcastReceiver,并使其优先级高于系统短信应用的BroadcastReceiver优先级,这样我们的app会先一步收到短信广播,然后拦截广播,使短信应用收不到短信广播,用户也就看不到被拦截的短信了。

在短信拦截器中定义广播接收器接收短信广播

public class SmsBlockReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        Bundle bundle = intent.getExtras(); // 从广播中取出短信
        Object[] objects = (Object[]) bundle.get("pdus");// 如果对方发来的短信内容过长,短信会被拆分成多条,所以这里用的是数组
        // PDU(Protocol Data Unit)即协议数据单元
        for (Object object : objects) { // 数组中的每一个元素,就是一条短信
            SmsMessage sms = SmsMessage.createFromPdu((byte[]) object); // 把数组中的元素转换成短信对象
            String number = sms.getOriginatingAddress();    // 获取对方(源)号码
            String content = sms.getMessageBody();          // 获取短信内容
            System.out.println(number + ":" + content);
            if ("13888888888".equals(number)) {
                abortBroadcast();   // 阻止其他广播接收器接受该广播,即拦截13888888888发来的短信
            }
        }
    }
}

然后在清单文件中配置BroadcastReceiver接收的广播类型,注意要设置优先级(设置为1000即高于系统应用的BroadcastReceiver优先级),保证我们app的BroadcastReceiver优先级高于短信应用的BroadcastReceiver优先级,才能实现短信拦截

<receiver android:name=".SmsBlockReceiver">
    <intent-filter android:priority="1000">
        <action android:name="android.provider.Telephony.SMS_RECEIVED" />
    </intent-filter>
</receiver>

接收短信广播同样也需要申请权限

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

SD卡状态侦听

首先定义广播接收器

public class SdcardReceiver extends BroadcastReceiver {
    
    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction(); // 取出Broadcast Intent中的action,判断收到的是哪一个广播
        if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) {
            Toast.makeText(context, "SD卡就绪", Toast.LENGTH_SHORT).show();
        } else if (action.equals(Intent.ACTION_MEDIA_UNMOUNTED)) {
            Toast.makeText(context, "SD卡被卸载", Toast.LENGTH_SHORT).show();
        } else if (action.equals(Intent.ACTION_MEDIA_REMOVED)) {
            Toast.makeText(context, "SD卡被拔出", Toast.LENGTH_SHORT).show();
        }
    }
}

然后在清单文件中注册这个广播接收器,并指定它所接收的广播类型。
一个广播接收器可以接收多种广播,只需要在intent-filter标签下定义多个action即可

<receiver android:name=".SdcardReceiver">
    <intent-filter>
        <action android:name="android.intent.action.MEDIA_MOUNTED"/>    <!-- SD卡就绪 -->
        <action android:name="android.intent.action.MEDIA_UNMOUNTED"/>  <!-- SD卡被卸载(系统设置中卸载) -->
        <action android:name="android.intent.action.MEDIA_REMOVED"/>    <!-- SD卡被拔出(物理插拔) -->
        <data android:scheme="file"/>   <!-- 广播中携带着以file为前缀的SD卡路径,添加这个data项才能匹配 -->
    </intent-filter>
</receiver>

勒索app

写一个勒索app(仅供学习),使其具有下面的功能:

  • Back键无效:重写onBackPressed()使其不调用finish().
  • Home键无效:通过监控Task栈,如果Task栈顶不是我们勒索app的Activity,就启动这个Activity.
  • 开机自启:在勒索app中定义接收开机广播的BroadcastReceiver,并在它的onReceive()中启动勒索app的Activity,这里主要介绍的就是这个功能的实现。

在勒索app中定义广播接收器接收开机广播

public class BootReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        // 当接收到开机广播,启动勒索软件MainActivity
        Intent intent1 = new Intent(context, MainActivity.class);
        // intent1.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(intent1);
    }
}

以上代码还不能启动MainActivity。使用标准启动模式启动Activity,Activity默认会进入启动它的组件所属的Task栈中,广播接收器(Activity Context之外)并没有Task栈,也就无法启动Activity,解决的方法就是要为待启动的Activity指定FLAG_ACTIVITY_NEW_TASK标记位,这样启动的时候就会为它创建一个新的Task栈

intent1.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

在清单文件中注册接收开机广播的广播接收器

<receiver android:name=".BootReceiver">
    <intent-filter >
        <action android:name="android.intent.action.BOOT_COMPLETED"/>
    </intent-filter>
</receiver>

申请权限

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

发送广播

广播是通过Intent发送的,给Intent设置一个action,然后调用下面三种方法即可发送我们自己的广播:

  1. sendBroadcast():最普通的发送intent的方式,是一种无序的广播机制,理论上,所有的接收器同时获得该intent的消息,接受器之间不存在先后顺序,不能截断/修改intent的数据。

  2. sendOrderedBroadcast():有序的发送广播的机制,所有接受器可以设置priority ,按照priority的大小顺序进行传递,上一个优先级的接受器可以截断和修改intent里面的数据。同时,也可以设置一个结果接收器(总是在最后一个接收到这个intent,用来实现一些特定的功能)。

  3. sendStickyBroadcast():是一种粘性广播。所谓的粘性是指,这个intent没有周期限制,一般广播只能将intent发送给当前已经注册了的BroadcastReceiver,一旦发送完毕就失去作用,而粘性广播没有这个限制,即便后来注册的BroadcastReceiver也可以收到这个广播。从android 5.0开始,出于安全性的考虑,官方已经正式废弃了粘性广播

public void sendMyBroadcast(View v) {
    Intent intent = new Intent();
    intent.setAction("a.b.c");  // a.b.c是我们自己的action
    sendBroadcast(intent);      // 发送普通广播
}

无序广播(普通广播)和有序广播

  • 对于无序广播(普通广播),所有与广播Intent匹配的BroadcastReceiver,都可以收到这条广播,并且不分先后顺序,视为同时收到,上面的sendBroadcast()发送的就是无序广播。这种广播的效率比较高,但缺点是接收器不能将处理结果传递给下一个接收器,并且无法在中途终止广播。

  • 对于有序广播,所有与广播Intent匹配的BroadcastReceiver,不一定都会收到这条广播,因为有序广播的接收是分先后顺序的,优先级高的先收到,优先级低的后收到,通过sendOrderedBroadcast()可以发送有序广播。

    • 对于静态注册的BroadcastReceiver,优先级声明在<intent-filter.../>标签内的android:priority属性中;对于动态注册的BroadcastReceiver,通过调用IntentFilter对象的setPriority()也可以设置优先级。优先级用一个整数来表示,值越大代表优先级越高,取值范围为-1000~1000
    • abortBroadCast():终止广播,类似拦截,只有有序广播可以被拦截(Andorid 4.4之后只有用户设置的默认短信应用调用这个方法可以拦截短信广播)
    • 优先接收到广播的接收器可以通过setResultXxx()修改广播内容,然后下一个接收器通过相应的getResultXxx()获取上一个接收器修改后的数据
    • 结果接收器resultReceiver:在通过sendOrderedBroadcast()发送有序广播时可以在第三个参数处指定这条有序广播的结果接收器,在这种情况下,当所有匹配的BroadcastReceiver都接收到该有序广播后,结果接收器才会收到,并且一定会收到该有序广播(即使使用abortBroadCast()拦截也会收到)。在前面IP拨号器的例子中,打电话应用中的BroadcastReceiver就是一个结果接收器,所以我们的IP拨号器中的BroadcastReceiver即使没设置优先级也会在打电话应用之前收到拨号广播,且打电话应用不能被拦截

BroadcastReceiver和Service

如果BroadcastReceiver的onReceive()方法不能在5s内执行完成,就会抛出ANR,所以不能在此方法中执行一些耗时的操作。

如果确实需要根据Broadcast来完成一项比较耗时的操作,则可以考虑在onReceive()中启动一个IntentService来完成该操作。

不应考虑在onReceive()中启动新线程去完成耗时操作,因为BroadcastReceiver本身的生命周期很短(静态注册下当onReceive()执行完生命周期即结束),很可能出现的情况是子线程还没有执行结束,BroadcastReceiver就已经被销毁了,此时应用进程可能由于不含有任何活动的应用组件而变为空进程,Android系统在内存紧张时会优先结束空进程,从而导致执行耗时操作的子线程不能执行完成。

抛出ANR的条件

对于Activity、BroadcastReceiver、Service这三大组件,如果在主线程(UI线程)中进行耗时操作,都有可能导致应用抛出ANR(Application Not Responding)异常,下面给出在这三大组件中抛出ANR的条件,源码基于Nougat - 7.1.1_r6

  1. 对于Activity,如果在主线程中执行耗时操作,并且从用户按键或触摸屏幕开始算起5s内耗时操作仍然未执行完,就会抛出ANR,这类ANR会有提示框弹出,用户可以选择force close或者继续等待。对应ActivityManagerService.java源码:

    // How long we wait until we timeout on key dispatching.
    static final int KEY_DISPATCHING_TIMEOUT = 5 * 1000;
    
    // How long we wait until we timeout on key dispatching during instrumentation.
    static final int INSTRUMENTATION_KEY_DISPATCHING_TIMEOUT = 60 * 1000;
    
  2. 对于BroadcastReceiver的onReceive(),如果在主线程中执行耗时操作,并且在60s内(默认后台队列广播)耗时操作仍然未执行完,就会抛出ANR,这类ANR没有提示框弹出。对应ActivityManagerService.java源码:

    // How long we allow a receiver to run before giving up on it.
    static final int BROADCAST_FG_TIMEOUT = 10 * 1000;
    static final int BROADCAST_BG_TIMEOUT = 60 * 1000;
    

    关于前台队列广播和后台队列广播的区别,详见Android广播机制——广播的发送以及说说Android的广播(4) - 前台广播为什么比后台广播快?

  3. 对于前台Service,如果在主线程中执行耗时操作,并且在20s内耗时操作仍然未执行完,就会抛出ANR,这类ANR同样没有提示框弹出。对应ActiveServices.java源码:

    // How long we wait for a service to finish executing.
    static final int SERVICE_TIMEOUT = 20 * 1000;
    
    // How long we wait for a service to finish executing.
    static final int SERVICE_BACKGROUND_TIMEOUT = SERVICE_TIMEOUT * 10;
    

关于ANR问题的详细分析,这里有一篇不错的文章,ANR问题分析指南

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

推荐阅读更多精彩内容