Android基础:BroadcastReceiver

494 阅读14分钟

广播的类型

广播的类型主要分为:

  • 系统广播
  • 无序广播    不可被拦截
  • 有序广播    可被拦截 修改
  • 粘性广播     
  • 本地广播    只在app内传播

广播的使用场景

 a.同一app内有多个进程的不同组件之间的消息通信。

 b.不同app之间的组件之间消息的通信。

普通广播(intent得广播)

普通广播对于多个接收者来说是完全异步的,通常每个接收者都无需等待即可以接收到广播,接收者相互之间不会有影响。对于这种广播,接收者无法终止广播,即无法阻止其他接收者的 接收动作。

Intent intent = new Intent();
//对应BroadcastReceiver中intentFilter的action                
intent.setAction("MyBroadcastReceiver");
sendBroadcast(intent);//发送广播
  • 若被注册了的广播接收者中注册时intentFilter的action与上述匹配,则会接收此广播(即进行回调onReceive())。如下MyBroadcastReceiver则会接收上述广播

  • 若发送广播有相应权限,那么广播接收者也需要相应权限

系统广播

Android中内置了很多系统广播:只要涉及到手机的基本操作(开机、网络变化、插入耳机等),都可以通过发送系统广播来监听变化,通过发送对应的intent-filter(包括action)

系统操作

action

监听网络变化

android.net.conn.CONNECTIVITY_CHANGE

关闭或打开飞行模式

Intent.ACTION_AIRPLANE_MODE_CHANGED

充电时或电量发生变化

Intent.ACTION_BATTERY_CHANGED

电池电量低

Intent.ACTION_BATTERY_LOW

电池电量充足(即从电量低变化到饱满时会发出广播

Intent.ACTION_BATTERY_OKAY

系统启动完成后(仅广播一次)

Intent.ACTION_BOOT_COMPLETED

关闭或打开飞行模式

Intent.ACTION_AIRPLANE_MODE_CHANGED

按下照相时的拍照按键(硬件按键)时

Intent.ACTION_CAMERA_BUTTON

屏幕锁屏

Intent.ACTION_CLOSE_SYSTEM_DIALOGS

设备当前设置被改变时(界面语言、设备方向等)

Intent.ACTION_CONFIGURATION_CHANGED

插入耳机时

Intent.ACTION_HEADSET_PLUG

未正确移除SD卡但已取出来时(正确移除方法:设置--SD卡和设备内存--卸载SD卡)

Intent.ACTION_MEDIA_BAD_REMOVAL

插入外部储存装置(如SD卡)

Intent.ACTION_MEDIA_CHECKING

成功安装APK

Intent.ACTION_PACKAGE_ADDED

成功删除APK

Intent.ACTION_PACKAGE_REMOVED

重启设备

Intent.ACTION_REBOOT

屏幕被关闭

Intent.ACTION_SCREEN_OFF

屏幕被打开

Intent.ACTION_SCREEN_ON

关闭系统时

Intent.ACTION_SHUTDOWN

重启设备

Intent.ACTION_REBOOT

注:当使用系统广播时,只需要在注册广播接收者时定义相关的action即可,并不需要手动发送广播,当系统有相关操作时会自动进行系统广播
这里通过一个简单的耳机插入/拔掉时的监听来看下系统广播

第一步(注册广播)
IntentFilter intentFilter = new IntentFilter();
//只需用改变这里的action来区别不同的系统广播
intentFilter.addAction("android.intent.action.HEADSET_PLUG");
registerReceiver(broadcastReceiver, intentFilter);
第二步(接收广播)
BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            AudioManager locationManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
            headsetOn = locationManager.isWiredHeadsetOn();
            if (headsetOn){
                Toast.makeText(context, "插入耳机", Toast.LENGTH_SHORT).show();
            }else {
                Toast.makeText(context, "拔出耳机", Toast.LENGTH_SHORT).show();
            }
        }
    };
第三步(注销广播)
@Override
    protected void onDestroy() {
        super.onDestroy();
        if (broadcastReceiver != null) {
            unregisterReceiver(broadcastReceiver);
        }
    }

系统广播.gif

无序广播

无序广播即为我们平时经常使用的广播,其主要是通过public abstract void sendBroadcast (Intent intent)方法进行发送,并通过intent传递数据。代码示例如下:

Intent intent = new Intent();
 //定义广播事件类型
intent.setAction("Intercept_Stitch");
//Android8.0以上必须加这个,不然静态注册收不到广播
intent.setPackage(getPackageName());
// 发送广播
sendBroadcast(intent);//无序广播

无序广播会被注册了的相应的感兴趣(intent-filter匹配)接收,且顺序是无序的。如果发送广播时有相应的权限要求,BroadCastReceiver如果想要接收此广播,也需要有相应的权限。
无序广播不可以被拦截,不可以被终止,不可以被修改,无序广播任何接收者只要匹配条件都可以接收到,无优先级问题。

在AndroidManifest.xml中注册
 <receiver
      android:name=".MyBroadcastReceiver"
      android:enabled="true"
      android:exported="true">
      <intent-filter android:priority="500">
         <action android:name="Intercept_Stitch" />
      </intent-filter>
</receiver>
<receiver
      android:name=".MyBroadcastReceiver2"
      android:enabled="true"
      android:exported="true">
    <intent-filter android:priority="214748367">
          <action android:name="Intercept_Stitch" />
     </intent-filter>
</receiver>

优先级的值越大它的优先级就越高。
那么priority的最大值是多少呢?
网上或有些书说的是1000,但是都不正确,正确答案是2147483647这是我通过几个实验得出的数据。优先级的值比它大1都会报错。并且优先级1001确实比优先级1000更快接收到广播。
优先级的范围是:-2147483647到2147483647
刚好它的长度范围是Integer类型数据的最大值4294967296(2的32次方)。
所有我们要知道优先级1000并不是最高的优先级,不信的话你可以试试看效果。

第一个MyBroadcastReceiver类
public class MyBroadcastReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Log.e(TAG, "onReceive: 这是静态的广播接收者(优先级500)" );
}
第二个MyBroadcastReceiver2类
public class MyBroadcastReceiver2 extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Log.e("Receiver", "这是静态的广播接受者(优先级999)---》");
        //截断广播
        abortBroadcast();
        //无序广播是不允许截断的 不然会报错BroadcastReceiver trying to return result during a non-ordered broadcast
        //只有有序广播可以截断
}

1559030003(1).jpg

动态发送无序广播会在下方和动态发送有序广播在一起,其实主要的区别就是

  • sendOrderedBroadcast(intent, null);//发送有序广播
  • sendBroadcast(intent);//发送无序广播

有序广播

有序广播比较特殊,每次发送广播会先发送到优先者高的地方,然后再通过优先者高的往低的发送,优先者高的可以截断广播,那么之后的接收者就接收不到广播了,可以在广播注册时使用intent-filter里面的android: priority=”xxx”去解决或在java代码中用setPriority(xxx)来设置。

静态发送有序广播
//静态发送有序广播
Intent intent = new Intent();
 //定义广播事件类型
intent.setAction("Intercept_Stitch");
intent.setPackage(getPackageName());
// 发送广播
sendOrderedBroadcast(intent,null);//有序广播
  • 无截断

    无截断.jpg

  • 有截断

    有截断.jpg

由此可看出添加截断广播可将之后的广播截断

动态发送有序广播
package com.dongbo.model;

import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;

public class Main4Activity extends AppCompatActivity {

    private MyBroadcastReceiver receiver;
    private MyBroadcastReceiver2 receiver2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main4);
        //创建广播接收者对象
        receiver = new MyBroadcastReceiver();
        receiver2 = new MyBroadcastReceiver2();
        //注册广播接收者,需要一个意图对象,也需要action参数,这里是定义Action参数
        IntentFilter filter = new IntentFilter("Intercept_Stitch");
        IntentFilter filter2 = new IntentFilter("Intercept_Stitch");
        //动态设置广播的优先级
        filter.setPriority(999);
        filter2.setPriority(500);
        //注册广播,
        // 动态广播拦截?先注册者先收到消息???
        registerReceiver(receiver2, filter);
        registerReceiver(receiver, filter);
    }

    @Override
    protected void onDestroy() {
        //在适当的时候要解除广播接收者的绑定
        unregisterReceiver(receiver);
        unregisterReceiver(receiver2);
        super.onDestroy();
    }

    public void click(View view) {
        Intent intent = new Intent();
        intent.setAction("Intercept_Stitch");
        //发送有序广播
//        sendOrderedBroadcast(intent, null);
        //发送无序广播
        sendBroadcast(intent);
    }
}
//接收的地方还是和上面的接受一样 这里就不做粘贴了 

粘性广播

粘性消息在发送后就一直存在于系统的消息容器里面,等待对应的处理器去处理,如果暂时没有处理器处理这个消息则一直在消息容器里面处于等待状态,粘性广播的Receiver如果被销毁,那么下次重建时会自动接收到消息数据。

虽然在我们现在的开发中,很少有用到粘性广播,但是这里我们也可以理解一下来实现一下粘性广播

使用场景

发送一个粘性广播(StickyBroadcast),但是现在并没有一个广播接收者来接收该广播发出的数据. 而是在某个时间后,才注册一个receiver,但是也能收到之前StickyBroadcast发出的广播. 这点就和平常用的普通广播很不一样了.

1.发送一个粘性广播
2.5秒之后启动一个新的Activity
3.在新的Activity中注册receiver并且接收粘性广播发送的数据
4.注意这里的权限一定不能少
<uses-permission android:name="android.permission.BROADCAST_STICKY"/>


package com.dongbo.model;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
//第一个activity
public class Main5Activity extends AppCompatActivity {
   private Context mContext;
   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main5);
       mContext=this;
       sendStickyBroadcastTest();
       startAnotherActivity();
   }
   //发送粘性广播
   private void sendStickyBroadcastTest() {
       Intent intent=new Intent();
       intent.setAction("cc.vv");
       intent.putExtra("time", String.valueOf(System.currentTimeMillis()));
       Log.e("传输的time",String.valueOf(System.currentTimeMillis())+"");
       sendStickyBroadcast(intent);
   }

   // 启动其他Activity
   private void startAnotherActivity() {
       new Thread() {
           public void run() {
               try {
                   Thread.sleep(1000*5);
                   Intent intent=new Intent(mContext, Main6Activity.class);
                   mContext.startActivity(intent);
               } catch (Exception e) {
                   // TODO: handle exception
               }
           }
       }.start();

   }
   BroadcastReceiver mBroad = new BroadcastReceiver() {
       @Override
       public void onReceive(Context context, Intent intent) {
           Log.e("tent",intent.getAction()+"");
       }
   };

}


package com.dongbo.model;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;

public class Main6Activity extends AppCompatActivity {
    private BroadcastReceiver mBroadcastReceiver = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main6);
        registBroadcastReceiver();
    }

    //注册广播接收者
    private void registBroadcastReceiver() {
        mBroadcastReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                String time = intent.getStringExtra("time");
                Log.e("收到了StickyBroadcast发出的数据"+"-------"+"发送广播的时间",time);
            }
        };
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction("cc.vv");
        registerReceiver(mBroadcastReceiver, intentFilter);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mBroadcastReceiver != null) {
            unregisterReceiver(mBroadcastReceiver);
        }
    }
}

粘性广播.jpg

App应用内更新

1.App应用内广播可理解为一种局部广播,广播的发送者和接收者都同属于一个App。
2.相比于全局广播(普通广播),App应用内广播优势体现在:安全性高 & 效率高

  • 具体使用1 - 将全局广播设置成局部广播
    1.注册广播时将exported属性设置为false,使得非本App内部发出的此广播不被接收;
    2.在广播发送和接收时,增设相应权限permission,用于权限验证;
    3.发送广播时指定该广播接收器所在的包名,此广播将只会发送到此包中的App内与之相匹配的有效广播接收器中。

通过intent.setPackage(packageName)指定包名

  • 具体使用2 - 使用封装好的LocalBroadcastManager类
    使用方式上与全局广播几乎相同,只是注册/取消注册广播接收器和发送广播时将参数的context变成了LocalBroadcastManager的单一实例

注:对于LocalBroadcastManager方式发送的应用内广播,只能通过LocalBroadcastManager动态注册,不能静态注册

package com.dongbo.model;

import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.app.AppCompatActivity;
import android.view.View;

public class Main7Activity extends AppCompatActivity {

    private MyBroadcastReceiver mBroadcastReceiver;
    private LocalBroadcastManager localBroadcastManager;
    private String BROADCAST_ACTION = "android.net.conn.CONNECTIVITY_CHANGE";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main7);

        //注册应用内广播接收器
        //步骤1:实例化BroadcastReceiver子类 & IntentFilter mBroadcastReceiver
        mBroadcastReceiver = new MyBroadcastReceiver();
        IntentFilter intentFilter = new IntentFilter();

        //步骤2:实例化LocalBroadcastManager的实例
        localBroadcastManager = LocalBroadcastManager.getInstance(this);

        //步骤3:设置接收广播的类型
        intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");

        //步骤4:调用LocalBroadcastManager单一实例的registerReceiver()方法进行动态注册
        localBroadcastManager.registerReceiver(mBroadcastReceiver, intentFilter);







    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //取消注册应用内广播接收器
        localBroadcastManager.unregisterReceiver(mBroadcastReceiver);
    }

    public void click(View view) {
        //发送应用内广播
        Intent intent = new Intent();
        intent.setAction(BROADCAST_ACTION);
        localBroadcastManager.sendBroadcast(intent);
    }
}

两个App之间进行通信

现有2个App A发送广播 B接收广播

//发送广播
Intent intent = new Intent();  //Itent就是我们要发送的内容
intent.putExtra("data", "this is data from broadcast "+ Calendar.getInstance().get(Calendar.SECOND));
intent.setAction(BROADCAST_ACTION_DISC);   //设置你这个广播的action,只有和这个action一样的接受者才能接受者才能接收广播
sendBroadcast(intent,BROADCAST_PERMISSION_DISC);   //发送广播
//使用自定义权限
<uses-permission android:name="com.cn.customview.permissions.MY_BROADCAST" />

<permission
   android:name="com.cn.customview.permissions.MY_BROADCAST"
   android:protectionLevel="signature" >
</permission>
注意:android:protectionLevel=”signature” 如果是同一个应用可以这么声明,如果跨app发送广播(比如A app向B app发送广播),则需要去掉android:protectionLevel=”signature”。


//B  App进行接收
 BroadcastReceiver receiveBroadCast = new ReceiveBroadCast();
IntentFilter filter = new IntentFilter();
filter.addAction(BROADCAST_ACTION_DISC); // 只有持有相同的action的接受者才能接收此广播
registerReceiver(receiveBroadCast, filter,BROADCAST_PERMISSION_DISC,null);

public class ReceiveBroadCast extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
     String data = intent.intent.getStringExtra("data");
      Log.e("data",data+");

}

3.广播内部实现机制

  a.自定义广播接收者BroadcastReceiver,并且重写onReceiver()方法。

  b.通过Binder机制向AMS(Activity Manager Service)进行注册。

  c.广播发送者通过Binder机制向AMS发送广播。

  d.AMS查找符合条件(IntentFilter/Permission等)的BroadcastReceiver,将广播发送到相应的BroadcastReceiver(一般情况下是Activity)的消息队列中。

  e.消息循环执行拿到此广播,回调BroadcastReceiver中的onReceiver()方法。

4.本地广播内部实现机制及优势

  相比于系统广播而言,本地广播更加安全,更加高效,

  特点:

  a.使用它发送的广播将只在自身app内传播,因此你不必担心泄漏隐私的数据。

  b.其他app无法对你的app发送该广播,因此你的app根本不可能收到非自身app发送的该广播,因此你不必担心有安全漏洞可以利用。

  c.比系统广播更加高效。

  内部实现机制:

  LocalBroadcast高效的原因:因为它内部是通过Handler实现的,它的sendBroadcast()方法含义并非和系统的sendBroadcast()一样,它的sendBroadcast()方法其实就是通过Handler发送了一个Message而已。

  LocalBroadcast内部协作主要是靠两个Map集合:mReceivers和mActions,当然还有一个List集合mPendingBroadcasts,这个主要存储待接收的广播对象。

前台广播/后台广播

1. 区别

//ActivityManagerService.java
static final int BROADCAST_FG_TIMEOUT = 10 * 1000;
static final int BROADCAST_BG_TIMEOUT = 60 * 1000;
 
public ActivityManagerService(Context systemContext){
    //...
    mFgBroadcastQueue = new BroadcastQueue(this, 
                                    mHandler,
                                    "foreground",
                                    BROADCAST_FG_TIMEOUT,
                                    false);
    mBgBroadcastQueue = new BroadcastQueue(this,
                                    mHandler,
                                    "background",
                                    BROADCAST_BG_TIMEOUT,
                                    true);
    //...
}
 
//BroadcastQueue.java
BroadcastQueue(ActivityManagerService service,
            Handler handler,
            String name,//名称
            long timeoutPeriod,//超时时间
            boolean allowDelayBehindServices) {//是否允许等待上一个广播接收者所在的进程的Service结束流程
    mService = service;
    mHandler = new BroadcastHandler(handler.getLooper());
    mQueueName = name;
    mTimeoutPeriod = timeoutPeriod;
    mDelayBehindServices = allowDelayBehindServices;
}

可以看到前台广播与后台广播主要用三个方面的区别:名称、超时时间以及mDelayBehindServices的值。启动最主要的区别在于后面两个

  1. 超时时间 前台广播超时时间:10s 后台广播超时时间:60s 可以看到前/后台广播的超时时间差距非常大,一旦发生超时,就会进入broadcastTimeoutLocked方法,触发我们所熟知的ANR
  2. mDelayBehindServices 这个标志就比较有意思了,它对有序广播会有一定的影响。如果该变量为true,那么BroadcastQueue就可能陷入一种名为WAITING_SERIVCES的状态。相关代码在BroadcastQueue.finishReceiverLocked方法中(广播派发成功之后由接收者回调AMS时触发),

1.1 mDelayBehindServices字段

public boolean finishReceiverLocked(BroadcastRecord r, int resultCode,            String resultData, Bundle resultExtras, boolean resultAbort, boolean waitForServices) {    //...     if (waitForServices && r.curComponent != null && r.queue.mDelayBehindServices            && r.queue.mOrderedBroadcasts.size() > 0            && r.queue.mOrderedBroadcasts.get(0) == r) {        ActivityInfo nextReceiver;        if (r.nextReceiver < r.receivers.size()) {            Object obj = r.receivers.get(r.nextReceiver);            nextReceiver = (obj instanceof ActivityInfo) ? (ActivityInfo)obj : null;        } else {            nextReceiver = null;        }        // Don't do this if the next receive is in the same process as the current one.        // 当前接收者与下一个接收者必须不处于同一个进程        // 才会进入该if语句内        if (receiver == null || nextReceiver == null                || receiver.applicationInfo.uid != nextReceiver.applicationInfo.uid                || !receiver.processName.equals(nextReceiver.processName)) {            //...            //如果当前用户有正在启动的后台服务            //且待启动服务已经超过可一个阈值(hasBackgroundServices内部有做判断)            //则进入WAITING_SERVICES状态            if (mService.mServices.hasBackgroundServices(r.userId)) {                Slog.i(TAG, "Delay finish: " + r.curComponent.flattenToShortString());                r.state = BroadcastRecord.WAITING_SERVICES;                return false;            }        }    }    //...    return state == BroadcastRecord.APP_RECEIVE            || state == BroadcastRecord.CALL_DONE_RECEIVE;

可以看到,如果当前的广播接收者和下一个广播接收者不处于同一个进程,且当前有正在启动的后台服务(用户维度)那么BroadcastQueue进入WAITING_SERVICES状态。 该状态对processNextBroadcast内对有序广播的处理以及广播超时方法broadcastTimeoutLocked都有一定的影响

  1. processNextBroadcast 之前我们分析过,在派发有序广播之前,BroadcastQueue首先会通过一个while循环找到下一个广播接收者。这个循环内部有一段代码:

    if (r.state != BroadcastRecord.IDLE) { if (DEBUG_BROADCAST) Slog.d(TAG_BROADCAST, "processNextBroadcast(" + mQueueName + ") called when not idle (state=" + r.state + ")"); return;}

可见,当处于WAITING_SERVICES状态,有序广播的派发会被阻断。

  1. broadcastTimeoutLocked 广播超时的代码中有下面这个代码段:

    BroadcastRecord br = mOrderedBroadcasts.get(0);if (br.state == BroadcastRecord.WAITING_SERVICES) { //... br.curComponent = null; br.state = BroadcastRecord.IDLE; processNextBroadcast(false); return;}

可见,对于处于WAITING_SERVICES状态的广播超时,并不会触发ANR,而是默默地将状态重新设置为IDLE,然后主动调用processNextBroadcast方法处理下一个消息。

mDelayBehindServices字段体现了AMS的一个策略:如果当前用户正在启动的后台服务数量超过阈值,那么会暂停后台广播队列中的有序广播的派发。

2. 广播队列选择策略

针对不同的IntentAMS会选用不同的广播队列,相关的逻辑在broadcastQueueForIntent方法中

//ActivityManagerService.javaBroadcastQueue broadcastQueueForIntent(Intent intent) {    final boolean isFg = (intent.getFlags() & Intent.FLAG_RECEIVER_FOREGROUND) != 0;    //...    return (isFg) ? mFgBroadcastQueue : mBgBroadcastQueue;}

可以看到逻辑非常简单,就是判断了一下是否包含了FLAG_RECEIVER_FOREGROUND标志,如果是,则使用前台广播队列,否则使用后台广播队列。 参考源码中对于FLAG_RECEIVER_FOREGROUND的注解

/** * If set, when sending a broadcast the recipient is allowed to run at * foreground priority, with a shorter timeout interval.  During normal * broadcasts the receivers are not automatically hoisted out of the * background priority class. */public static final int FLAG_RECEIVER_FOREGROUND = 0x10000000;

可以得到两个关键点

  1. 接收者将被允许以前台优先级运行
  2. 更短的时间间隔(也即是前面讲到的超时时间)

Intent默认情况下是不带FLAG_RECEIVER_FOREGROUND标志的,因此通常我们的广播都是在后台广播队列中进行传播的。

3. 总结

通过上面对于源码的分析,我们知道了前台广播队列与后台广播队列的区别

  1. 前台广播队列的超时时间比后台广播队列短很多
  2. 后台广播队列在当前用户后台启动服务超过阈值时会暂停广播
  3. 前台广播允许接收者以前台优先级运行

以及如何在发送广播的时候指定广播传输的队列:通过设置FLAG_RECEIVER_FOREGROUND标志即可。