Android有序广播的缺陷与“改进”--四大组件系统

戳蓝字"牛晓伟"关注我哦!

用心坚持输出易读、有趣、有深度、高质量、体系化的技术文章,技术文章也可以有温度。

本文摘要

本文主要介绍Android有序广播的缺陷以及官方都做了哪些"改进",通过本文您将了解到为啥要有超时机制惩罚机制是啥分类机制是啥等等。 (文中代码基于Android13)

本文大纲

1. 有序广播的缺陷

在介绍有序广播的缺陷之前,我觉得有必要给大家展示下有序广播的分发原理,只有对原理有了了解,才知道为啥有缺陷,如何改进。如下图展示了有序广播的分发过程:

图解

上图展示的是有序广播的分发过程,为了让大家更容易理解,图中有些地方做了简化,比如有序广播不单单只从有序广播队列 中去取;比如每一轮分发广播时是先分发所有无序广播 ,再分发有序广播 (因为分发所有无序广播不是本文的重点,故没有体现出来)。更具体的信息以及分发广播模块 等的介绍请看一文彻底搞懂Android广播的所有知识 这篇文章

有序广播的分发是由广播分发模块 分为一轮一轮的在进行,如上图先从有序广播队列 中从index = 0位置取出BroadcastRecord1,再去按照优先级把第一个接收者取出来,通过binder通信把广播消息发送给对应接收者,当接收者处理完毕,必须需要发送处理完毕信息分发广播模块 ,这样分发广播模块才能在下一轮中把广播分发给下一个接收者。

当BroadcastRecord1的所有接收者都收到广播后,再从有序广播队列中取出BroadcastRecord2,还是依照上面的步骤把广播发送给BroadcastRecord2的所有接收者。

因此有序广播的分发就是广播分发给接收者后只有收到接收者处理完毕的消息后才会把广播继续分发给下一个接收者该广播的所有接收者都收到广播后 ,才会从有序广播队列中取出下一个广播继续分发 。这里的广播指BroadcastRecord对象 。关于BroadcastRecord可以查看一文彻底搞懂Android广播的所有知识 这篇文章

上图展示的都是理想情况,各个接收者在处理广播时都不存在耗时的情况,并且都顺利把处理完成信息 发送给了广播分发模块,在这种理想的情况下,有序广播是没有啥缺陷的。

但是现实情况可不是这样的,开发者完全可以在接收广播的时候写出耗时操作的代码,往往由于这个耗时操作导致有序广播队列内的广播只能延迟分发,假如广播队列中有非常重要的系统广播,比如SCREEN_ON、SCREEN_OFF这种,会完全影响这些功能。这真是"一颗老鼠屎坏了一锅粥"。

在实际项目中我就遇到过由于某个接收者耗时,导致后面的广播分发延迟一分钟左右 。综上所述有序广播的缺陷就是由于某个接收者处理耗时,导致其他的广播延迟分发

那既然发现了缺陷,那看下官方是如何一步一步"改进"的,改进俩字加引号的原因是:这个改进其实是在打补丁,治标不治本。

2. "改进"之超时机制

超时机制 分为针对一个接收者的超时机制针对所有接收者的超时机制

2.1 针对一个接收者的超时机制

针对一个接收者的超时机制就是在发送广播给它的接收者之前会启动超时机制如果在规定的时间内没有收到它的接收者发送的处理完成的消息则认为该接收者已经超时 。超时机制的惩罚措施是针对该接收者的进程报ANR (ANR Application not response)。超时机制的时间对于前台广播是10s ,对于后台广播是60s

如果在规定的时间内收到了接收者发送的处理完成的消息 ,则会取消超时机制,接着进行下一轮把广播分发给它的下一个接收者;如果在规定时间内没有收到接收者发送的处理完成的消息,则会针对该接受者的进程报ANR,同样也会接着进行下一轮把广播分发给它的下一个接收者。

2.2 针对所有接收者的超时机制

先来看一幅图:

图解

all_timeout代表一个广播所有接收者接收、处理广播的最大时间

now代表当前的时间

spend_time代表当前时间 - 广播的dispatchTime,其中广播的dispatchTime在广播第一次分发时的开始分发时间

针对所有接收者的超时机制就是在分发广播之前,会判断spend_time是否大于all_timeout ,若大于则认为出现超时,超时则找到当前广播的当前接收者对应的进程报ANR,开始下一轮的广播分发;否则没有超时,则进入下一步流程。

下面是相关代码:

scss 复制代码
//以下代码位于BroadcastQueue的processNextBroadcastLocked方法

            // 接收者的个数
            int numReceivers = (r.receivers != null) ? r.receivers.size() : 0;
            //r.timeoutExempt为true则代表超时机制对于该广播来说无效,r.dispatchTime > 0代表该广播已经开始分发
            if (mService.mProcessesReady && !r.timeoutExempt && r.dispatchTime > 0) {
                //numReceivers大于0 代表广播的接收者存在, (2 * mConstants.TIMEOUT * numReceivers) 就是上面的 all_timeout
                if ((numReceivers > 0) &&
                        (now > r.dispatchTime + (2 * mConstants.TIMEOUT * numReceivers))) {
                    //已经出现超时,强制结束这个广播
                    broadcastTimeoutLocked(false); // forcibly finish this broadcast 
                    forceReceive = true;
                    //把该广播的状态置为IDLE
                    r.state = BroadcastRecord.IDLE;
                }
            }

2.3 小结

超时机制也就是为了"改进"有序广播的缺陷,因此也只有有序广播的分发才有超时机制 ,而无序广播的分发是没有此机制 的,超时机制的惩罚措施就是找到接收者对应的进程报ANR

超时机制并不是会对所有的广播都有效,有些广播是可以对于超时机制豁免的,比如一些系统广播,是否豁免全在于BroadcastRecord对象的timeoutExempt属性,如下相关代码:

arduino 复制代码
//BroadcastRecord

boolean timeoutExempt;  // true if this broadcast is not subject to receiver timeouts

虽然有了此机制,但是有序广播的缺陷还是会存在,假如一个广播有10个接收者,每个接收者都不会出现超时,并且这些接收者们接收处理广播所花费的总时长也小于但是接近于all_timeout,那该广播的所有接收者花费的时间也是很长的,有序广播的缺陷还是会存在。那咱们接着看下下一个"改进"是否能改掉。

3. "改进"之惩罚机制

惩罚机制就是针对接收处理 广播慢的接收者进行处罚,针对慢的接收者会把它们加入延迟队列中,而到底多慢才会被认为是慢的接收者呢?请看下面相应代码:

typescript 复制代码
//BroadcastQueue

//一个接收者处理完毕广播后,会执行该方法
public boolean finishReceiverLocked(BroadcastRecord r, int resultCode,
            String resultData, Bundle resultExtras, boolean resultAbort, boolean waitForServices) {
            
           省略代码······
           
           //niu 该广播对于超时不豁免
           if (!r.timeoutExempt) {
            //elapsed代表接收者接收、处理广播花费的时间,超过SLOW_TIME就被认为是慢的接收者了
            if (r.curApp != null  && mConstants.SLOW_TIME > 0 && elapsed > mConstants.SLOW_TIME) { 
                //不是核心app的话,没有超时豁免机制
                if (!UserHandle.isCore(r.curApp.uid)) {
                    省略代码······
                    // 由于被认为是慢接收者,则加入延迟队列
                    mDispatcher.startDeferring(r.curApp.uid);
                } else {
                    if (DEBUG_BROADCAST_DEFERRAL) {
                        Slog.i(TAG_BROADCAST, "Core uid " + r.curApp.uid
                                + " receiver was slow but not deferring: "
                                + receiver + " br=" + r);
                    }
                }
            }
        } else {
            省略代码·····
        }
        省略代码······
}

如上代码,如果接收者接收处理 广播所花费的时间elapsed 大于mConstants.SLOW_TIME (mConstants.SLOW_TIME的值是5s),则认为该接收者是慢接收者,就会把该接收者对应的uid加入延迟队列 (uid是Apk安装成功后分配的唯一id),延迟队列位于BroadcastDispatcher对象的mDeferredBroadcasts属性,如下相关代码:

swift 复制代码
//BroadcastDispatcher

private final ArrayList<Deferrals> mDeferredBroadcasts = new ArrayList<>();

既然慢接收者的uid加入了延迟队列,那在分发广播给该接收者时,会判断该接收者的延迟时间是否到了,到了则把广播分发给它;否则暂停该广播分发给该接收者 (只有延迟时间到了后,才会把广播分发给该接收者),接着把广播分发给下一个接收者。

如下是相关代码:

ini 复制代码
//以下代码位于BroadcastQueue的processNextBroadcastLocked方法

            //r.deferred为false,非延迟交付的广播
            if (!r.deferred) {
                //获取接收器的uid
                final int receiverUid = r.getReceiverUid(r.receivers.get(r.nextReceiver));
                
                //为true:该接受者是处于延迟状态
                if (mDispatcher.isDeferringLocked(receiverUid)) {
                    省略代码······
                    BroadcastRecord defer;
                    //是最后一个广播接受者了
                    if (r.nextReceiver + 1 == numReceivers) {
                        defer = r;//记录下延迟广播
                        mDispatcher.retireBroadcastLocked(r);//跳过该广播
                    } else {
                        //不是最后一个接收者
                        defer = r.splitRecipientsLocked(receiverUid, r.nextReceiver);
                        省略代码······
                    }
                    //把defer广播放入延迟接收者中
                    mDispatcher.addDeferredBroadcast(receiverUid, defer);
                    r = null;
                    looped = true;
                    continue;
                }
            }

惩罚机制也就是发现慢的接收者 ,则把它放入延迟队列 ,当下一次分发广播给该接收者时,如果接收者的延迟时间还没有到,则暂停该广播分发给该接收者 (只有延迟时间到了后,才会把广播分发给该接收者),接着把广播分发给下一个接收者;否则把广播分发给它,并且把它从延迟队列中移除。

惩罚机制也是只能解决一部分有序广播的缺陷,假如广播是在延迟时间后分发给慢接收者的,这时候若慢接收者接收处理广播的时间还是长,则依然会有缺陷存在。那再来看下最后一种"改进"。

4. "改进"之分类机制

其实在一文彻底搞懂Android广播的所有知识 这篇文章中已经介绍过分发广播中心 又可以分为注册模块分发广播模块 ,而分发广播模块 是由BroadcastQueueBroadcastDispatcher类组成的。

分类机制就是对分发广播模块进行分类 ,分发广播模块可以分为前台广播分发模块后台广播分发模块offload广播分发模块 。分类的目的就是专模块做专事

offload广播分发模块只分发boot complete阶段的广播,而前台广播分发模块只处理前台广播的分发,后台广播分发模块只处理后台广播的分发。不同分类模块都有自己的有序广播队列无序广播队列,这样boot complete阶段的有序广播和无序广播就放在对应模块的队列中,前台广播的有序广播和无序广播放在对应模块的队列中。这样它们之间互不干扰。

此"改进"同样也是解决部分有序广播的缺陷,比如后台广分发模块内部同样还是存在有序广播缺陷问题。

5. 总结

本文带大家了解了有序广播存在的缺陷,虽然官方一再的通过各种"改进"来解决掉此缺陷,但是都没有起到完全解决的目的。我在实际中也遇到过由于某个或多个接收者耗时,导致有序广播队列中的很多广播分发被延迟了,有时候延迟时间能达到一分钟左右。

吐槽下:其实广播分发的代码,官方写的并不好,尤其类的命名上,BroadcastQueue类看着名字是广播队列,但其实还干了广播分发的事情,BroadcastDispatcher类看着名字是广播分发,但却干了有序广播队列的事情。

好消息是在Android14上已经把广播相关的代码重构了,有序广播的缺陷应该也解决了,有时间我再来分析下。

相关推荐
syy敬礼2 小时前
Android实现简易计算器
android
CL_IN6 小时前
高效集成销售订单数据到MySQL的方法
android·数据库·mysql
devlei7 小时前
Android JankStats实现解析
android
Vesper637 小时前
【Android】‘adb shell input text‘ 模拟器输入文本工具使用教程
android·adb
MyhEhud8 小时前
Kotlin apply 方法的用法和使用场景
android·kotlin·kotlin apply函数
Code额8 小时前
MySQL的事务机制
android·mysql·adb
蓝莓浆糊饼干10 小时前
请简述一下String、StringBuffer和“equals”与“==”、“hashCode”的区别和使用场景
android·java
李斯维12 小时前
深入理解 Android Canvas 变换:缩放、旋转、平移全解析(一)
android·canvas·图形学
_一条咸鱼_12 小时前
Android Retrofit 框架日志与错误处理模块深度剖析(七)
android
顾林海12 小时前
Flutter Dart 面向对象编程全面解析
android·前端·flutter