type
status
slug
date
summary
tags
category
password
icon
这里写文章的前言:
一个简单的开头,简述这篇文章讨论的问题、目标、人物、背景是什么?并简述你给出的答案。
可以说说你的故事:阻碍、努力、结果成果,意外与转折。
📝 一、概述
Android U上,
BroadcastQueue成为了一个抽象类,它有两个实现类BroadcastQueueImpl和BroadcastQueueModernImpl(BQMI):前者对应Android T以及以前的广播队列实现,后者则是Android U上新引入的一种实现。📡 二、Modern广播队列带来的变化
2.1 广播的批量分发
modern广播队列被引入Android U,最重要的目的之一就是支持广播的批量分发:

也就是说,对于短时间内需要分发到同一进程的多个Receiver,如果它们满足一定条件,可以将它们打包,一次分发到目标应用,这样做的好处是:
- 减少Binder通信的次数,提高分发效率
- 减少目标进程oom_adj的计算与调整次数,节省系统资源
Android U上默认的max batch size 是1,也就是说虽然
BroadcastQueueModernImpl支持批量分发,但是默认仍然是一个一个分发的。当然,这个batch size的值是可以调整的,通过这个属性:persist.device_config.activity_manager_native_boot.bcast_max_batch_size但是这个批量分发的特性在最近的代码中被Google Revert掉了,具体原因后面会说。
2.2 广播的多槽位并行分发
旧的广播队列实现里,所有的非平行广播都是一个接一个绝对串行分发的,Modern广播队列允许那些满足一定条件的Receiver并行分发,默认最大并发量4 + 1(低内存设备2 + 1),当然这个可以通过下面的属性来配置:
- persist.device_config.activity_manager_native_boot.bcast_max_running_process_queues
- persist.device_config.activity_manager_native_boot.bcast_extra_running_urgent_process_queues

- 传统的广播(非平行广播)分发,是将所有满足同一Action的Receiver组成一个
BroadcastRecord,分发的时候按照优先级,将这些Receiver一个一个地分发到目标进程,这个BroadcastRecord的分发完,分发下一个BroadcastRecord的。也就是说,它是按照发送方进程组织Receiver,完全串行分发的,并且不存在BroadcastRecord插队的情况
- Modern广播队列的多槽位分发,则是将要分发到同一个目标进程的Receiver放到一起,多个槽位(目标进程)并行分发,并且分发到同一进程的多个Receiver还可以打包分发。
广播的多槽位分发的好处是:
- 能够更加充分地利用系统资源,提高分发的吞吐量,避免或者减轻广播队列的拥堵
既然Modern广播队列支持了非平行广播的并行分发,我们知道非平行广播的分发是有可能启动应用进程的,那么理论上就有并行启动多个进程的可能,这是不被允许的。Modern广播队列对于这一点有专门做控制,让它和传统的广播队列保持一致
2.3 新的timeout判定规则
Modern广播队列对于广播timeout的判定逻辑发生了变化:

• 在Android T及以前的版本,timeout的检测是定时检测,每隔固定的时间检测一次。开始分发一个
BroadcastRecord(非平行)的头一个Receiver的时候埋下timeout消息,该BroadcastRecord的所有Receiver都处理完后移除消息。
- Android U上timeout的检测更像是实时检测,每个Receiver(非平行)开始分发时都会埋下timeout消息,finish回来的时候移除。
- Android U上timeout区分了SOFT和HARD:SOFT timeout对应于之前版本中广播的timeout,而HARD timeout则是在SOFT timeout的时间内,假设有n秒的时间应用进程都在等待被调度,那么就会在SOFT timeout之外再延长n秒的时间,这n秒就是HARD timeout时间。ANR的判定超时时间为:SOFT timeout时间 + HARD timeout时间
- Android U上去掉了
BroadcastRecord总的处理时间的限制。这个是因为Android T及以前的Receiver是按照所属的BroadcastRecord一个接一个分发的,同一个BroadcastRecord的Receiver分发过程中不会被别的BroadcastRecord的Receiver插队;但是Android U同一个BroadcastRecord的所有Receiver被打散到不同的进程,理论上每一个Receiver 都有可能被别的BroadcastRecord的Receiver block住,所以这个限制不适用于Android U的Modern广播队列。

• 但是Modern增加了一个HealthCheck操作,如上前面所讲。
我们前面说Android U 回退了广播批量分发的代码,原因应该和timeout判定的一个问题应该有关:

这个问题的关键在于:
- 按照Google回退前的代码,同一目标进程的所有
msg.obj都是一样的:queue
- finishReceiver的时候,也只能根据
queue去mLocalHandler.removeMessages(MSG_DELIVERY_TIMEOUT_SOFT, queue)
最终导致该进程所有的timeout msg被remove,剩下的receiver如果发生了timeout,就无法处理了。要解决该问题,
msg.obj的值就不能简单放一个queue,其值必须能够标识哪个BPQ的哪个receiver。目前为止,Google还没有修复。新的timeout判定规则,增加了 timeout HARD,一改之前版本定时判断的方式,改用即时判断(和Service超时的判定类似),这个带来的变化有:
- 总体来讲timeout判定更加宽松
ActivityManager线程的消息队列不但有超时消息,还有别的消息,一旦拥堵,可能导致ANR不能及时抛出,HealthCheck虽然有检测,但是尺度比较松。
2.4 限制cached进程处理广播
对于进入cached状态的进程,会根据其BPQ所有的Receiver来做不同程度的处理,比如:
- 如果该进程所有Receiver所属的
BroadcastRecord都可以被无限期延迟,那么进程将进入无限期延迟状态,期间不会分发其BPQ里的Receiver,除非它退出这种状态
- 如果该进程要处理的下一个Receiver所属的
BroadcastRecord可以被无限期延迟,但是该进程有不可被无限期延迟的其它BroadcastRecord,且下一个要处理的Receiver所属的BroadcastRecord是前台广播、或者它是优先级广播、或者该广播要等结果,该进程不会被延迟
- 其它情况,一律被延迟2分钟,期间不会分发其BPQ里的Receiver,除非它退出这种状态
2.5 广播延迟处理机制被取消
这里的延迟处理机制是指Android Q引入的对于消息处理比较慢(前台广播大于5S)的进程(确切地讲是其uid下的所有进程)的延迟分发机制,在Modern广播队列里被取消了。
2.6 BroadcastQueue四合一
在之前版本的AMS中,广播队列有四个:
background
foreground
offload_bg
offload_fg
在Android U上,虽然还区分普通广播、offload广播以及前台广播、后台广播,但是只有一个广播队列:
modern
三、Modern广播队列的实现
3.1 核心类
核心类只有2个:
BroadcastProcessQueue(BPQ)
BroadcastQueueModernImpl(BQMI)
其它新增了
BroadcastReceiverBatch、BroadcastRecord做了较多修改,都不是核心,这里不罗列了。3.1.1
BroadcastProcessQueue传统广播队列中的Receiver是按照发送方进程组织到
BroadcastRecord中的,而Modern广播队列中的Receiver是按照接收方进程组织在一起的。BroadcastProcessQueue就是将这些Receiver组织起来的数据结构。每个应用进程都会对应一个BroadcastProcessQueue,其主要成员如下:
BroadcastProcessQueue的主要功能包括:- 分门别类组织Receiver:将要分发给本进程所有的Receiver分成NORMAL、URGENT、OFFLOAD三类,并各自独立存储(按照enqueueTime排序),用一个
BroadcastRecord+ 一个index来唯一确定一个Receiver
- 管理每个Receiver的分发优先级
- 管理每个Receiver的增删改查。
- 维护自身的分发状态
3.1.2
BroadcastQueueModernImplBroadcastQueueModernImpl是BroadcastQueue的子类,其主要成员如下:
- mProcessQueues 系统所有应用进程的BPQ,按照uid分组,同一uid的所有进程放在一个链表中
- mRunnableHead 一个双向链表,这个链表中的所有BPQ都是当前或者未来某一确定时间,可以分发Receiver的(或者说可以放入mRunning中)
- mRunning 当前正在分发Receiver的BPQ列表
- mRunningColdStart 用来表示正在冷起动进程的BPQ,分发Receiver时,发现这个进程尚未启动则为其赋值并去启动该进程,进程起来到system_server报到后重置为null
它主要的主要功能包括:
- Receiver分发流程与状态的控制
- Receiver超时的监控
- 批量分发策略的实现
- 并发控制策略实现
3.2 Receiver分发状态与流程
3.2.1 分发状态
一个Receiver(非平行)从进入
BroadcastQueueModernImpl起,到分发流程走完,一共有7个状态:- DELIVERY_PENDING
- DELIVERY_DELIVERED
- DELIVERY_SKIPPED
- DELIVERY_TIMEOUT
- DELIVERY_SCHEDULED
- DELIVERY_FAILURE
- DELIVERY_DEFERRED
每一个Receiver的状态都会被保存在它们对应的
BroadcastRecord中,状态切换如下图:
- 非绿色的都是terminal状态,其中只有Delivered状态是正常的terminal状态
- 一个Receiver进入广播队列默认是Pending状态
- Pending状态和Deferred状态可以相互切换,比如进程退出和进入cached状态
- 在分发前,如果这个广播不符合系统的要求(比如没有权限),这个时候对应Receiver的状态就会由Pending转为Skipped,结束这个Receiver的分发,处理下一个Receiver
- 在分发前,如果需要拉起应用进程,但是启动进程失败,这个Receiver的状态会由Pending转换为Failure
- 如果都没有问题,在开始分发前,这个Receiver的状态会由Pending转换为Scheduled
- 当然,如果是平行广播,则不需要finishReceiver流程,这个Receiver的状态会由Pending转换为Delivered
- 在分发时,如果分发失败(比如binder通信失败),这个Receiver的状态会由Scheduled转换为Failure
- 分发完成后,如果是非平行广播,等应用处理完成后,finishReceiver时,状态会由Scheduled转换为Delivered
- 如果分发后,足够长的时间都没有finishReceiver,状态则会由Scheduled转换为Timeout
3.2.2 分发流程
简要流程如下:

- 整体分为四部分(这四部分之间是异步的,不是顺序执行的):
- 从发送广播到启动目标进程
- 从目标进程启动完成到把Receiver分发给应用
- 从应用处理完成finishReceiver到分发本BPQ的下一批Receiver
- 从本BPQ分发完成到重新调度
- 广播分发策略有三种,都是用来处理频繁发送的情况:
- DELIVERY_GROUP_POLICY_ALL 每个都分发,默认值
- DELIVERY_GROUP_POLICY_MOST_RECENT 只分发最近一次的,之前还未来得及分发的丢弃
- DELIVERY_GROUP_POLICY_MERGED 只分发最近一次的,之前未来得及分发的合并到最近这次
- 分类、排序、计算状态中的计算状态主要是决定本BPQ的
mRunnableAt,若其不为Long.MAX_VALUE则认为是Runnable状态,每个BPQ在BroadcastQueueModernImpl的mRunnableHead中就是根据mRunnableAt排序的,这个会直接影响先分发哪个BPQ。计算为lazy模式
- 有以下四种情况时,不会将一个BPQ从
mRunnableHead移入mRunning: - BPQ不是Runnable状态
mRunning槽位已满- BPQ虽然是Runnable状态,但是现在未到其
mRunnableAt时间点 - 有别的BPQ正处在冷起动过程中(保证广播不会并发启动应用进程)
- 调度冷起竞争失败的BPQ 进程启动完成后,重新调度,如果
mRunning中还有空余槽位,则可以把之前冷起竞争失败的BPQ放进来,并进行分发
- 判断是否继续分发本BPQ的Receiver规则是,同时满足以下三个条件:
- BPQ是Runnable状态
- BPQ对应的进程已经起来
- BPQ连续分发的Receiver没有达到上限(8/16)
3.3 分发调度策略
分发调度策略要解决的核心问题:下一步要分发哪个BPQ的哪一个Receiver
Modern广播队列的分发调度策略比较复杂,但是它提高了并发量,能够更充分利用系统资源,提高分发效率。并且它采取了以下措施尽可能避免或者减轻广播队列的拥堵:
- 一个在
mRunning数组中的BPQ,一旦其连续分发的Receiver数量达到阈值,则会将其从mRunning数组踢出,让其它还未进入mRunning数组中的BPQ有被调度到的机会
- 一个BPQ的Receiver数量一旦达到阈值,则会尽可能提高其优先级
- 一个BPQ内部的,一旦其连续分发的高优先级种类Receiver数量达到阈值,则会降低其优先级,保证公平性
分发调度主要是通过对
mRunnableHead链表和mRunning数组的维护实现的3.3.1 mRunnableHead链表的维护
mRunnableHead这个双向链表中的每一个BPQ都是Runnable状态,且它们按照mRunnableAt升序排列,链表前面的元素优先放入mRunning数组被调度。3.3.1.1 BPQ的状态
每个BPQ有三类五种(Blockded、Deferred、Empty、Runnable、Running)状态:

状态切换的部分场景如下:
- 新的Receiver(同一BroadcastRecord中优先级最高的)放入空的BPQ,会使该BPQ的状态由Unrunnable(Empty)转换为Runnable
- 进程进入cached状态,如果这个时候该BPQ里的Receiver都是可以被无期限延迟的,则会使这个BPQ的状态由Runnable转换为Unrunnable(Deferred)
- 当Running列表的槽位空出时,就有可能把BPQ从
mRunnableHead移动到mRunning里,开始分发,这个时候该BPQ的状态会由Runnable转换为Running
- 如果这个BPQ连续分发了16个(第内存设备8个)Receiver,则会将其从
mRunning里踢出,如果满足条件,会把它重新放入mRunnableHead,它的状态就会由Running转换为Runnable
- 当一个BPQ把自己的所有Receiver都分发完了,它就会被踢出
mRunning数组,状态由Running转换为Unrunnable(Empty)
3.3.1.2 BPQ内部Receiver的优先级
BPQ把它所有的Receiver分成NORMAL、URGENT、OFFLOAD三类,并创建了三个数组分开存放,那么当我们需要取出一个Receiver分发的时候,先取哪个呢?
- 每类Receiver内部按照enqueueTime排序,enqueueTime越小越靠前;enqueueTime相同的按照Receiver的
android:priority排序,android:priority越大越靠前
- 三类Receiver之间,默认的优先级是URGENT > NORMAL > OFFLOAD
- 同时满足以下三个条件,会打破三类Receiver之间的优先级顺序,选取低优先级数组中的元素:
- 连续分发的高优先级Receiver的数量达到上限(URGENT默认3个,NORMAL默认10个)
- 低优先级的数组中的第一个Receiver没有处在block状态
- 低优先级的数组中的第一个Receiver的enqueueTime小于等于高优先级的数组中的第一个Receiver
3.3.1.3
mRunnableAt和状态的计算方法mRunnableAt的计算比较复杂,且一个BPQ是否是Runnable的,也是根据它来决定的。而且mRunnableAt和该BPQ的头一个Receiver(具体获取看前面)的enqueueTime关系密切,大致有以下几种结果(注意判断顺序有先后):- 该Receiver在等别的Receiver finish,则为Long.MAX_VALUE,BPQ状态为Blocked
- 该BPQ含有未被延迟的前台广播、Interactive广播或者发送广播的进程为测试进程(或者root,shell进程等),则为enqueueTime - 2min,BPQ状态为Runnable
- 该BPQ含有ordered广播、alarm广播、优先级广播、静态注册的Receiver或者该BPQ对应的进程为persistent进程,则为enqueueTime,BPQ状态为Runnable
- 进程进入cached状态,且该BPQ的每一个Receiver都是可无期限延迟的,则为Long.MAX_VALUE,BPQ状态为Deferred
- 进程进入cached状态,且该Receiver是不可无期限延迟的,则为enqueueTime + 2min,BPQ状态为Runnable
- 进程进入cached状态,该Receiver可无期限延迟,但是BPQ含有不可无期限延迟的Receiver,有几种可能:
- 如果该Receiver是前台广播,则为enqueueTime - 2min,BPQ状态为Runnable
- 如果该Receiver是优先级广播或者发送者要等处理结果,则为enqueueTime,BPQ状态为Runnable
- 以上两种情况都不是,则为enqueueTime + 2min,BPQ状态为Runnable
- 该BPQ含有发送者需要等结果的Receiver,则为enqueueTime,BPQ状态为Runnable
- 以上情况都不是,则为enqueueTime + 500ms,BPQ状态为Runnable
最后,如果该BPQ的Receiver的数量达到了上限(128/256),则会取以上获取到的
mRunnableAt和头一个Receiver的enqueueTime中的较小值,这样就可以提高该BPQ被放入到mRunning中,也就是被调度到的概率,尽快分发其Receiver,尽可能避免广播队列拥堵的发生。3.3.1.4
mRunnableAt和状态的计算时机理论上,当一个进程的状态改变(cached)以及它所对应的BPQ中有Receiver的变化时,都应设置该BPQ的
mRunnableAtInvalidated为true,在需要获取其状态或者mRunnableAt值的时候重新计算,具体场景包括:- 进程退出时,需要remove掉其动态注册的所有Receiver,并将它们置为Skipped状态。如果确实有remove掉Receiver,则需要重新计算其对应BPQ的状态和
mRunnableAt值
- 有新的Receiver放入BPQ时
- 正常finishReceiver时,如果所属BPQ连续分发的Receiver的数量达到了上限,则需要将其从
mRunning中踢出,这个时候需要从新计算,以决定是否将其放入mRunnableHead链表
- 设置Receiver状态时,如果是Terminal状态,标志着当前Receiver的分发已经完成,在分发下一个Receiver前,当然需要重新计算状态和
mRunnableAt值
- 进程进出cached状态时,需要重新计算该BPQ的状态和
mRunnableAt值
以上场景计算完对应BPQ的状态和
mRunnableAt值后都会调用updateRunnableList方法更新mRunnableHead链表:- 如果BPQ已经在
mRunning中,则什么都不做,updateRunnableList不会改变正在分发中的BPQ的状态
- 如果BPQ不是Runnable状态,且它不在
mRunnableHead链表里,同样什么都不做
- 如果BPQ不是Runnable状态,且它还在
mRunnableHead链表里,则将其踢出
- 如果BPQ是Runnable状态,且它不在
mRunnableHead链表里,则在里面找个合适的位置别放入
- 如果BPQ是Runnable状态,且它在
mRunnableHead链表里,则更新其在里面的位置
3.3.2 mRunning数组的维护
mRunning是一个默认长度为5的数组,也就是说Modern广播队列最高支持5个槽位并行分发Receiver。3.3.2.1 进入mRunning数组
一个BPQ的Receiver要想得到分发,它必须得在
mRunning数组中,并且一旦进入,它就会在分配的槽位上开始广播循环(一批接一批串行分发Receiver),直到被踢出mRunning数组。进入
mRunning数组是通过enqueueUpdateRunningList这个方法,在需要槽位或者有可能空出槽位的时候就会调用该方法,尝试把mRunnableHead里的BPQ往mRunning里移动,主要场景包括:- 进程冷启动完成,让和其冷起竞争失败的BPQ这个时候可以进入
mRunning数组了
- 进程死亡,前面讲过会移除其动态注册的Receiver,这个时候会导致其BPQ状态更新,有可能空出槽位;另外,如果这个进程恰好是正在冷起的进程,会将其正在分发的Receiver状态置为Failure,这个时候也有可能空出槽位给竞争失败的那些进程
- 发送新的广播,新的Receiver所在的BPQ要找空闲槽位
- finishReceiver时,如果该Receiver所属BPQ不再需要分发,被踢出
mRunning数组,则有新的槽位
- 设置Receiver的状态为Terminal状态时,有可能会解除被这个Receiver block住的BPQ,这个时候需要更新其状态,为其寻找槽位
- BPQ Deferred状态和进程cached状态改变时,进入这两种状态时,有可能空出槽位给别的BPQ,退出这两种状态时,需要新的槽位
另外,有以下四种情况时,不会将一个BPQ从
mRunnableHead移入mRunning:- BPQ不是Runnable状态
mRunning槽位已满
- BPQ虽然是Runnable状态,但是现在未到其
mRunnableAt时间点
- 有别的BPQ正处在冷起动过程中(保证广播不会并发启动应用进程)
3.3.2.2 退出
mRunning数组退出槽位的场景主要包括:
- Receiver所属BPQ连续分发Receiver的数量达到了阈值
- BPQ进入Unrunnable状态
- 进程退出
3.4 批量打包策略
BPQ批量打包策略总的原则有三点:
- 平行广播的Receiver可以放到一起批量打包分发
- 一个包里最多只能有一个非平行广播的Receiver(打包时装完一个非平行广播Receiver就会封包)
- 被打包的这些Receiver之间没被指定先后顺序
但并不是说只要和以上3个原则不冲突,一个Receiver就一定能打进包里去。比如出现以下情况之一,它都不会被打进包里去:
- 如果BPQ的状态是Unrunnable,则不会打进去
- 如果进程还没启动,则不会打进去
- 如果该BPQ已经连续分发的Receiver的数量已经达到上限,则不会打进去
- 如果包的容量已经达到上限,则不会打进去
另外,如果两个Receiver的enqueueTime相差较大(也可以简单理解为发送广播的时间间隔较大),它们也有可能不会打到同一个包里。原因简单来讲就是:后面的Receiver来得太晚,前面已经发车,它只能等下班车了。以后台平行广播为例:

- t时刻R0进入该BPQ,R0是该BPQ的首个Receiver,此时会更新BPQ的
mRunnableAt= t + 500,状态为Runnable
- 不久后R1和R2进入该BPQ,
mRunnableAt值不变,状态不变
- t + 500时刻,
mRunnableAt时间到,该BPQ有了进入mRunning数组的机会
- t + 580时刻,槽位空出,该BPQ进入
mRunning数组,它的Receiver会被批量打包,分发给目标应用进程
- T + 650时刻,R3进入BPQ,显然它已经赶不上前面的那批了,只能等500ms后的下一批
- 最终结果就是R0、R1、R2一批,R3、R4一批
- 作者:白色风车
- 链接:https://f.appa.me/article/android_u
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。