小明的玩物志
首页
搜索
归档
白色风车
文章
17
分类
4
标签
8
归档
搜索
分类
标签
技术分享
🗒️Android U新特性 - Modern广播队列机制介绍
发布于: 2023-2-2
最后更新: 2024-8-21
次查看
Android
开发
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,最重要的目的之一就是支持广播的批量分发:
notion image
也就是说,对于短时间内需要分发到同一进程的多个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
notion image
  • 传统的广播(非平行广播)分发,是将所有满足同一Action的Receiver组成一个BroadcastRecord,分发的时候按照优先级,将这些Receiver一个一个地分发到目标进程,这个BroadcastRecord的分发完,分发下一个BroadcastRecord的。也就是说,它是按照发送方进程组织Receiver,完全串行分发的,并且不存在BroadcastRecord插队的情况
  • Modern广播队列的多槽位分发,则是将要分发到同一个目标进程的Receiver放到一起,多个槽位(目标进程)并行分发,并且分发到同一进程的多个Receiver还可以打包分发。
广播的多槽位分发的好处是:
  • 能够更加充分地利用系统资源,提高分发的吞吐量,避免或者减轻广播队列的拥堵
既然Modern广播队列支持了非平行广播的并行分发,我们知道非平行广播的分发是有可能启动应用进程的,那么理论上就有并行启动多个进程的可能,这是不被允许的。Modern广播队列对于这一点有专门做控制,让它和传统的广播队列保持一致

2.3 新的timeout判定规则

Modern广播队列对于广播timeout的判定逻辑发生了变化:
notion image
• 在Android T及以前的版本,timeout的检测是定时检测,每隔固定的时间检测一次。开始分发一个BroadcastRecord(非平行)的头一个Receiver的时候埋下timeout消息,该BroadcastRecord的所有Receiver都处理完后移除消息。
notion image
  • 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广播队列。
notion image
• 但是Modern增加了一个HealthCheck操作,如上前面所讲。
我们前面说Android U 回退了广播批量分发的代码,原因应该和timeout判定的一个问题应该有关:
notion image
这个问题的关键在于:
  • 按照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,其主要成员如下:
notion image
BroadcastProcessQueue的主要功能包括:
  • 分门别类组织Receiver:将要分发给本进程所有的Receiver分成NORMAL、URGENT、OFFLOAD三类,并各自独立存储(按照enqueueTime排序),用一个BroadcastRecord + 一个index来唯一确定一个Receiver
  • 管理每个Receiver的分发优先级
  • 管理每个Receiver的增删改查。
  • 维护自身的分发状态
3.1.2 BroadcastQueueModernImpl
BroadcastQueueModernImpl是BroadcastQueue的子类,其主要成员如下:
notion image
  • 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中,状态切换如下图:
notion image
  • 非绿色的都是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 分发流程

简要流程如下:
notion image
  • 整体分为四部分(这四部分之间是异步的,不是顺序执行的):
    • 从发送广播到启动目标进程
    • 从目标进程启动完成到把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)状态:
notion image
状态切换的部分场景如下:
  • 新的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来得太晚,前面已经发车,它只能等下班车了。以后台平行广播为例:
notion image
  • 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 许可协议,转载请注明出处。
相关文章
案例:数据分析
AIGC持续火爆,生成式AI的应用场景有哪些?
加锁文章2
持续调教prompt(案例)
还有不知道 commit 规范 ?
Android Hilt
陆奇-新范式 新时代 新机会案例:数据分析
Loading...
目录
0%
📝 一、概述📡 二、Modern广播队列带来的变化2.1 广播的批量分发2.2 广播的多槽位并行分发2.3 新的timeout判定规则2.4 限制cached进程处理广播2.5 广播延迟处理机制被取消2.6 BroadcastQueue四合一三、Modern广播队列的实现3.1 核心类3.2 Receiver分发状态与流程3.2.2 分发流程3.3 分发调度策略3.3.1 mRunnableHead链表的维护3.3.2 mRunning数组的维护3.3.2.1 进入mRunning数组3.4 批量打包策略
白色风车
白色风车
为了不折腾而折腾
文章
17
分类
4
标签
8
最新发布
CD/CD
CD/CD
2024-8-21
AIGC持续火爆,生成式AI的应用场景有哪些?
AIGC持续火爆,生成式AI的应用场景有哪些?
2024-8-21
GPT辅助润色论文
GPT辅助润色论文
2024-8-21
基本提示模式
基本提示模式
2024-8-21
陆奇-新范式 新时代 新机会
陆奇-新范式 新时代 新机会
2024-8-21
Android Hilt
Android Hilt
2024-8-21
公告
🏄遥遥领先🏄
为了不折腾而折腾
 
目录
0%
📝 一、概述📡 二、Modern广播队列带来的变化2.1 广播的批量分发2.2 广播的多槽位并行分发2.3 新的timeout判定规则2.4 限制cached进程处理广播2.5 广播延迟处理机制被取消2.6 BroadcastQueue四合一三、Modern广播队列的实现3.1 核心类3.2 Receiver分发状态与流程3.2.2 分发流程3.3 分发调度策略3.3.1 mRunnableHead链表的维护3.3.2 mRunning数组的维护3.3.2.1 进入mRunning数组3.4 批量打包策略
2013-2025 白色风车.