Android LocalBroadcastManager [transmisión local] análisis de código fuente

Visión de conjunto

  1. Introducción a LocalBroadcastManager
  2. Análisis de código fuente de LocalBroadcastManager
  3. LocalBroadcastManagerResumen

1. Introducción a LocalBroadcastManager

1. ¿Qué es LocalBroadcastManager?

En Android, Broadcastes una forma muy utilizada de transferir información entre aplicaciones.

BroadcastEs una comunicación entre procesos . Otras aplicaciones pueden monitorear los mensajes enviados. Al mismo tiempo, otras aplicaciones también pueden atacar su aplicación mediante el envío continuo de transmisiones. No se puede garantizar la seguridad de la aplicación.

Con el fin de resolver este problema, LocalBroadcastManagersurgió

LocalBroadcastManagerEs el kit de herramientas proporcionado en la biblioteca de Android XBroadcast y, en comparación con él, sus ventajas son:

1. Sin necesidad de comunicación entre procesos, mayor eficiencia

2. Difundir solo dentro de la aplicación, sin tener en cuenta ningún problema de seguridad que presenten otras aplicaciones al enviar y recibir mi transmisión.

En resumen, si solo desea comunicarse dentro de la aplicación, puede elegir la transmisión local como el bus de eventos de la aplicación.

2. Cómo usar LocalBroadcastManager

  • Creación del objeto LocalBroadcastManager

    LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance (contexto);

  • Registrar un receptor de transmisión

    LocalBroadcastManager.registerReceiver(broadcastReceiver, intentFilter);

  • enviar difusión

    LocalBroadcastManager.sendBroadcast (intención);

  • enviar transmisión simultánea

    LocalBroadcastManager.sendBroadcastSync ( intent ) ;

  • 取消注册广播接收器

    LocalBroadcastManager.unregisterReceiver( broadcastReceiver );

二、LocalBroadcastManager源码解析

LocalBroadcastManager是基于发布/订阅模式来设计的,LocalBroadcastManager本身是发布订阅中心,提供订阅、取消订阅、发布消息的功能,后续的文章中提到的订阅者即表示的是广播接收器消息事件指的是过滤器Action

在开始阅读LocalBroadcastManager源码之前,让我们先来认识一下LocalBroadcastManager的成员属性,理解各个成员属性的含义,对接下来源码的阅读会有非常大的帮助

1、LocalBroadcastManager成员属性

1.1 成员变量

类型 名称 说明
Context mAppContext Application的Context
HashMapBroadcastReceiver, ArrayList> mReceivers 广播接收者/订阅者集合
HashMap<String, ArrayList<ReceiverRecord>> mActions 订阅事件集合
ArrayList mPendingBroadcasts 待执行分发事件的集合
Handler mHandler 使用MainLoop创建的Handler
int MSG_EXEC_PENDING_BROADCASTS Message的标识

在以上的成员变量中,我们需要重点关注的是mReceiversmActions

  • mReceivers

上一节我们介绍了LocalBroadcastManager是基于发布/订阅模式设计的,事件发生时需要通知所有的订阅者,那么这里必然有一个保存所有订阅者的集合,在LocalBroadcastManager中这个集合就是mReceivers

到这里事情本该结束了,但当我们尝试发送一个事件时就会发现:一个订阅者可以订阅多个事件(Action),不同的订阅者也可以订阅同一个事件(Action)。

当一个事件发生后,每次都要先去遍历订阅者集合(mReceivers),再从每个订阅者订阅的事件集合(actions)中匹配是否订阅了正在发生的事件,伪代码:

    public void sendBroadcast(Intent intent){
       //遍历订阅者集合
        for(Receiver receiver :mReceivers){
           //遍历订阅者订阅的事件集合
            for(Action action : receiver.actions){
               if(intent.action == action){
                   //do something
                }
            }
        }
    }
复制代码

显然,这样做的时间效率并不高,为了解决这个问题,LocalBroadcastManager新增了一个事件集合:mActions

  • mActions

LocalBroadcastManager中,mActions表示的就是事件集合;其中,事件Action作为集合的key,对应的value是订阅这个事件订阅者们,这样,每次发送事件时,只需要去事件集合查找对应的订阅者们,通知它们即可,订阅者事件(Action)的关系如下图:

image_android_component_local_broadcast_manager_receiver_and_action_relation.jpg

1.2 内部类:ReceiverRecord

ReceiverRecordLocalBroadcastManager的内部类,存在的意义是包装广播接收器,给广播接收器增加一些属性

类型 名称 说明
IntentFilter filter 广播接收的过滤器
BroadcastReceiver receiver 广播接收者
boolean broadcasting 无意义标识
boolean dead 描述订阅者状态,这个广播接收器是不是解除注册了

1.3 内部类:BroadcastRecord

BroadcastRecord是待执行集合中的元素类型

类型 名称 说明
Intent intent 发送的广播
ArrayList receivers 已经匹配上的广播接收者集合

2、成员方法

2.1小节中我们把LocalBroadcastManager成员属性都介绍完了,这里再唠叨一下,重点关注mReceiversmActions这两个集合,广播的注册解除注册都是操作这俩集合

2.2小节中,我们将会介绍LocalBroadcastManager的各个成员方法功能以及如何实现的,LocalBroadcastManager方法数量不多,算上构造函数一共也才7个,接下来我们就先从构造函数开始讲起:

2.1 LocalBroadcastManager构造方法

private LocalBroadcastManager(Context context) {
    mAppContext = context;
    mHandler = new Handler(context.getMainLooper()) {

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_EXEC_PENDING_BROADCASTS:
                    executePendingBroadcasts();
                    break;
                default:
                    super.handleMessage(msg);
            }
        }
    };
}
复制代码

LocalBroadcastManager被设计成全局单例,所以它的构造函数private修饰的,在LocalBroadcastManager构造函数中一共做了两件事:

  1. 保存Context,注意这里保存的是Application的上下文,接下来的getInstance方法介绍里也会提到这一点
  2. 创建Handler

注意看创建Handler的时候使用的是MainLooper,也就是说将来通过这个Handler提交的消息都会添加到主消息队列,然后再由MainLooper分发给当前Handler,那么在executePendingBroadcasts中派发消息的时候,里面的代码都是运行在Main线程

Handler里面的逻辑也比较简单,接收到Message后调用executePendingBroadcasts()方法来分发消息,由于使用了MainLooper,所以在广播接收器BroadcastReceiveronReceive函数中,是可以进行UI操作的,比如这样:

new Thread(() -> {
    LocalBroadcastManager.getInstance(null).registerReceiver(new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            //执行UI操作
        }
    },new IntentFilter());
}).start();
复制代码

示例代码中在子线程中注册了一个广播接收器,在里面做执行UI的操作是完全没问题的,因为onReceive()方法最终运行在Main线程

2.2 getInstance():获取实例

public static LocalBroadcastManager getInstance(@NonNull Context context) {
    synchronized (mLock) {
        if (mInstance == null) {
           //使用Application的上下文创建实例
            mInstance = new LocalBroadcastManager(context.getApplicationContext());
        }
        return mInstance;
    }
}
复制代码

getInstance()方法只做一件事:检查mInstance实例是否为空,为空的话创建一个新的对象赋值给mInstance

这里调用context的getApplicationContext()方法,获取的是Application的上下文,这样就不用担心传入生命周期短的组件造成内存泄漏的问题了

我们细看这段代码会发现,LocalBroadcastManager只要初始化过一次之后,再次传进来的context其实是没有用到的

这时候你可能会想:既然用不到,那我是不是可以在应用创建之初调用LocalBroadcastManager.getInstance()方法初始化实例,之后在其他的地方使用时不传context呢?

答案是当然可以一旦创建LocalBroadcastManager实例后,我们可以在任意线程调用getInstance()且不传入context来获取实例发送广播啥的

如果你使用Kotlin语言开发,因为context被@NonNull注解修饰,直接传null的话编译器会不通过~

使用Java语言开发的同学可以试一试

2.3 registerReceiver():注册广播

public void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
    synchronized (mReceivers) {
        //point 1
        ReceiverRecord entry = new ReceiverRecord(filter, receiver);
        //1. 获取订阅者的过滤器集合,目的是给这个订阅者新增一个过滤器
        ArrayList<ReceiverRecord> filters = mReceivers.get(receiver);
        if (filters == null) {
            filters = new ArrayList<>(1);
            mReceivers.put(receiver, filters);
        }
        //point 2
        filters.add(entry);//point 2 这里需要注意,因为entry是重新创建的,所以多次调用时,哪怕传入的接收器和过滤器是相同的,在发送广播时也会回调接收器多次
        //解析过滤器要监听的事件(Action)
        for (int i = 0; i < filter.countActions(); i++) {
            String action = filter.getAction(i);
            //2. 获取事件(Action)绑定的订阅者集合,目的是给这个事件增加一个订阅者
            ArrayList<ReceiverRecord> entries = mActions.get(action);
            if (entries == null) {
                entries = new ArrayList<>(1);
                mActions.put(action, entries);
            }
            entries.add(entry);
        }
    }
}
复制代码

注册广播的关键步骤注释都已经标好了,总结一下在registerReceiver()方法中一共完成了两件事:

  1. 向订阅者集合添加一条数据,增加一个订阅者
  2. 遍历订阅者要订阅的事件集合,把当前的订阅者绑定到订阅的事件上去

这里的订阅者指的是广播接收器(BroadcastReceiver),事件集合指的是广播接收器的过滤器(IntentFilter)包含的actions

在日常开发中调用registerReceiver()方法注册广播时,有一点需要注意,我们回头看注释标注point 1point 2的地方会发现,不管传入的广播接收器过滤器是否已经存在订阅者集合中,在point 1的位置总会重新创建ReceiverRecord来描述广播接收器过滤器

什么意思呢

当你在注册广播时小手一抖,不小心按了Ctrl+D复制了一行

    private BroadcastReceiver receiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            //do something * 手抖次数
        }
    };
    
    private IntentFilter filter = new IntentFilter();
    
    public void test(){
        LocalBroadcastManager.getInstance(this).registerReceiver(receiver,filter);
        LocalBroadcastManager.getInstance(this).registerReceiver(receiver,filter);//不小心手抖出来的
    }
复制代码

那么当事件发生时,广播接收器会收到多个回调,手抖多少次就会收到多少次~

2.4 unregisterReceiver():解除注册

public void unregisterReceiver(@NonNull BroadcastReceiver receiver) {
    synchronized (mReceivers) {
        //1. 获取订阅者绑定的过滤器集合,并将当前订阅者从订阅者集合中删除
        final ArrayList<ReceiverRecord> filters = mReceivers.remove(receiver);
        if (filters == null) {
            return;
        }
        for (int i = filters.size() - 1; i >= 0; i--) {
            final ReceiverRecord filter = filters.get(i);
            filter.dead = true;//宣告当前广播接收器死亡
            //2. 遍历当前订阅者订阅的事件集合,再将它从每个事件绑定的订阅者集合中删除
            for (int j = 0; j < filter.filter.countActions(); j++) {
                final String action = filter.filter.getAction(j);
                //找到这个事件绑定的所有订阅者
                final ArrayList<ReceiverRecord> receivers = mActions.get(action);
                if (receivers != null) {
                    //这里的倒序遍历应该是Google的小优化,通常最后注册的广播都会被优先删除,因为当前Activity被kill解除注册后就返回上一级页面了
                    for (int k = receivers.size() - 1; k >= 0; k--) {
                        final ReceiverRecord rec = receivers.get(k);
                        if (rec.receiver == receiver) {
                            rec.dead = true;//我觉得这一步多余了,因为这个订阅者在方法入口时就已经宣告死亡了,这里直接将订阅者删除就行了
                            receivers.remove(k);
                        }
                    }
                    //安全检查,删除掉当前订阅者后,这个事件没有订阅者愿意监听了,那么从事件集合中删除
                    if (receivers.size() <= 0) {
                        mActions.remove(action);
                    }
                }
            }
        }
    }
}
复制代码

unregisterReceiver()解除注册广播的方法中,同样也是做了两件事:

  1. 把订阅者从订阅者集合中删除
  2. 遍历全局的事件集合(mActions),把订阅者从绑定的事件集合中删除

这里和广播的注册流程是一样的,无非一个是增,一个是删,没什么需要特别注意的地方

2.5 sendBroadcast():发送广播

public boolean sendBroadcast(@NonNull Intent intent) {
    synchronized (mReceivers) {
        //匹配规则详情
        final String action = intent.getAction();
        final String type = intent.resolveTypeIfNeeded(
                mAppContext.getContentResolver());
        final Uri data = intent.getData();
        final String scheme = intent.getScheme();
        final Set<String> categories = intent.getCategories();

        //获取当前事件的所有订阅者们
        ArrayList<ReceiverRecord> entries = mActions.get(intent.getAction());
        if (entries != null) {
            //receivers集合里面保存的是,一圈遍历匹配下来,符合规则的订阅者们
            ArrayList<ReceiverRecord> receivers = null;
            for (int i = 0; i < entries.size(); i++) {
                ReceiverRecord receiver = entries.get(i);
                //point 1
                if (receiver.broadcasting) {
                    continue;
                }
                //规则匹配过程
                int match = receiver.filter.match(action, type, scheme, data,
                        categories, "LocalBroadcastManager");
                if (match >= 0) {
                    if (receivers == null) {
                        receivers = new ArrayList<>();
                    }
                    receivers.add(receiver);
                    //point 2
                    receiver.broadcasting = true;
                }
            }

            if (receivers != null) {
                //point 3
                for (int i = 0; i < receivers.size(); i++) {
                    receivers.get(i).broadcasting = false;
                }
                //将订阅者们添加到待执行的任务集合中
                mPendingBroadcasts.add(new BroadcastRecord(intent, receivers));

                //point 4 防止重复发消息,代价是要遍历消息队列里所有消息 ummmm..
                if (!mHandler.hasMessages(MSG_EXEC_PENDING_BROADCASTS)) {
                    mHandler.sendEmptyMessage(MSG_EXEC_PENDING_BROADCASTS);
                }
                return true;
            }
        }
    }
    return false;
}
复制代码

发送广播的方法中就只做了一件事:从订阅事件集合(mActions)中找到符合intent条件的订阅者们,并将它们放入待执行集合(mPendingBroadcasts)

注意哦,虽然方法名称叫做发送广播,但是其实里面并没有发送的动作,只是找出符合发送规则的订阅者们丢进待执行集合,等待Handler来执行

这里有两个槽点要说一下:

一是point 1/2/3标注的broadcasting变量,压根就没用到,不知道存在的意义是什么

二是point 4标注的防止重复发消息的设计,咱创建一个bool类型的变量来标识不行嘛,为啥要想不开去遍历消息队列一个个检查

2.6 sendBroadcastSync():发送同步广播

//从方法名称就可以看出这是一个同步方法,调用者会阻塞到消息分发给所有订阅者才会返回
public void sendBroadcastSync(@NonNull Intent intent) {
    if (sendBroadcast(intent)) {
        executePendingBroadcasts();
    }
}
复制代码

sendBroadcastSync()方法代码量不多,一共做了两件事:

  1. 调用sendBroadcast()方法,将符合条件的订阅者放入待执行集合(mPendingBroadcasts),等待执行
  2. 调用executePendingBroadcasts()方法,立刻执行消息分发

这里需要重点关注的点时:由于sendBroadcastSync()方法内部调用了executePendingBroadcasts()立刻执行了消息分发,所以,各个广播接收器的onReceive()函数运行的线程,取决于广播发送者所在的线程

什么意思呢

我们知道,当调用sendBroadcast()方法发送广播时,executePendingBroadcasts()方法最终是由Handler来调用的,也就是说不管广播接收器是在哪个线程注册的,都会切换到Main线程来执行onReceive()方法中的代码

而当广播的发送者可以直接调用executePendingBroadcasts()分发消息时,性质就不一样了!!!

public void test(){
    //子线程发送同步广播
    new Thread(() -> {
        LocalBroadcastManager.getInstance(null).sendBroadcastSync(new Intent("action"));
    }).start();
}
复制代码

如上,当发送者处于子线程调用发送同步广播方法时,广播接收器的onReceive()方法也运行在这个子线程

这时候,如果在onReceive()方法中执行操作UI的代码,那你将会收到异常:Only the original thread that created a view hierarchy can touch its views.

2.7 executePendingBroadcasts():派发广播

void executePendingBroadcasts() {
    while (true) {
        //保存待分发的订阅者集合
        final BroadcastRecord[] brs;
        synchronized (mReceivers) {
            final int N = mPendingBroadcasts.size();
            if (N <= 0) {
                return;
            }
            brs = new BroadcastRecord[N];
            mPendingBroadcasts.toArray(brs);
            mPendingBroadcasts.clear();
        }
        for (int i = 0; i < brs.length; i++) {
            final BroadcastRecord br = brs[i];
            final int nbr = br.receivers.size();
            for (int j = 0; j < nbr; j++) {
                final ReceiverRecord rec = br.receivers.get(j);
                if (!rec.dead) {
                    //分发事件
                    rec.receiver.onReceive(mAppContext, br.intent);
                }
            }
        }
    }
}
复制代码

executePendingBroadcasts()的职责就是分发广播,在源码中也只有两个地方调用

一是构造函数Handler,二就是sendBroadcastSync()方法,两者调用区别在2.2.6小节已经讲过了,这里就不再赘述

3、小结

介绍完LocalBroadcastManager所有的成员属性成员方法后,我们可以总结本地广播使用的几个特点:

  1. 本地广播基于发布/订阅模式实现,通信范围只能在当前进程,若组件在manifest文件中指定运行在其他进程,就无法使用本地广播通信了
  2. Handler的加入让本地广播有切换到主线程执行代码的能力,不管注册广播和创建广播接收器的动作执行在哪个线程,最终的回调函数都会切换到Main线程
  3. LocalBroadcastManager在初始化后,getInstance(context)context参数可以不传
  4. sendBroadcastSync()发送同步广播最终的广播接收器的回调函数是运行在广播发送者线程的,要小心使用

三、本地广播总结

在本篇文章中经常把广播接收器称为订阅者,之所以这样称呼是因为LocalBroadcastManager的设计与发布/订阅模式太相似了,呐,你看Android开发者官网也是这样介绍的

image_android_component_local_broadcast_manager_android_developer.jpg

LocalBroadcastManager目前在开发者官网已经Google声明为弃用状态,猜测可能因为任何组件都可以使用本地广播来发送、注册与解除注册,甚至不需要在组件内使用,任意线程都可以获取本地广播实例来操作

若APP应用内完全依靠本地广播来通信,对整个工程来说,如何管理这些广播接收器是个问题,若再因为疏忽大意忘记手动解除绑定,那么还会造成内存泄漏的问题

不过不管是否弃用,LocalBroadcastManager内部的设计思想依旧值得学习

全文完

Supongo que te gusta

Origin juejin.im/post/7078652053559443492
Recomendado
Clasificación