Android12 显示框架之Transaction----从队列到合成的server端核心链路

📅 发布时间:2026/7/9 6:10:07 👁️ 浏览次数:
Android12 显示框架之Transaction----从队列到合成的server端核心链路
1. 事务队列SurfaceFlinger的“待办事项清单”大家好我是老张在Android显示系统这块摸爬滚打了十来年。今天咱们接着上回聊上次说到客户端比如你的App通过setTransactionState把一堆窗口状态、图层属性打包成一个“事务”Transaction然后“快递”给了系统服务SurfaceFlinger。这就像你把一封信投进了邮筒接下来邮局SurfaceFlinger怎么处理这封信就是咱们今天要深挖的“Server端核心链路”。SurfaceFlinger收到这封“信”TransactionState后第一件事不是立刻拆开处理而是把它放进一个“待办事项清单”里排队。这个清单就是事务队列。为什么需要排队想象一下你的手机屏幕上可能同时有几十个App的窗口、状态栏、导航栏、壁纸在活动每一帧画面都可能有多处变化。如果SurfaceFlinger收到一个请求就立刻处理一个那画面更新就会乱套可能出现图层闪烁、撕裂或者资源竞争导致崩溃。所以排队处理是保证画面稳定、有序合成的基石。在Android 12的SurfaceFlinger代码里这个“待办事项清单”主要由两个核心成员构成mTransactionQueue和mPendingTransactionQueues。你可以把它们理解成邮局的两个工作篮mTransactionQueue当前事务队列就像邮局门口刚收进来的“当日新邮件篮”。所有从客户端新提交过来的TransactionState第一时间都会被queueTransaction函数放进这个篮子里。它是一个简单的先进先出FIFO队列代码里用std::deque实现。mPendingTransactionQueues挂起事务队列映射这个结构稍微复杂点它是一个std::unordered_map。它的键Key是一个叫applyToken的东西通常是一个IBinder对象值Value是对应这个Token的一个事务队列。这就像是邮局里为“特殊客户”准备的“专属待办篮”。什么情况下事务会被放进这里呢主要就是当某个事务还没准备好被应用的时候。那么谁来决定一个事务是留在“新邮件篮”mTransactionQueue里等待处理还是被转移到某个“专属待办篮”mPendingTransactionQueues里挂起呢这个关键的“调度员”就是flushTransactionQueues函数。它会在SurfaceFlinger主线程的特定时机通常是在VSync信号到来后准备合成新一帧之前被调用然后开始检查这两个篮子。1.1 队列协同与状态检查事务的“就绪”判定flushTransactionQueues函数的工作流程我把它比作邮局分拣员在上班。他的工作有两步第一步先处理“专属待办篮”mPendingTransactionQueues。分拣员会遍历每一个“专属待办篮”每个applyToken对应的队列检查篮子里最上面的那封信事务是否已经“就绪”。判断“就绪”的核心函数是transactionIsReadyToBeApplied。这个函数检查什么呢我实测下来主要看四点期望显示时间desiredPresentTime这封信上写的“希望投递时间”到了吗如果设定了未来某个时间点显示那必须等到那个时间点之后。缓冲区栅栏acquire fence这封信关联的“包裹”图形缓冲区送到了吗acquire fence是一个同步原语只有它变成signaled状态才意味着GPU已经完成对该缓冲区的渲染SurfaceFlinger可以安全地读取它了。如果fence没到事务就得等着。缓冲区状态缓冲区本身是否有效、是否已经被丢弃。VSync对齐对于高刷新率屏幕还要考虑事务的提交时机是否与设备的VSync节奏对齐以避免卡顿或撕裂。如果检查通过事务“就绪”分拣员就把这封信从“专属待办篮”里拿出来放到一个叫transactions的临时“处理中”篮子里。如果检查不通过事务“未就绪”分拣员就会立刻设置一个“需要再次分拣”的标志setTransactionFlags(eTransactionFlushNeeded)然后跳过这个篮子去检查下一个。这保证了只要有一个事务没准备好下一轮VSync周期还会再来检查不会让它被遗忘。第二步再处理“新邮件篮”mTransactionQueue。分拣员会检查所有新来的信。对于每一封信他先看看有没有对应的“专属待办篮”通过applyToken查找mPendingTransactionQueues。如果有那没得说直接把这封信也放进那个“专属待办篮”里排队等它的前辈处理完。如果没有对应的“专属待办篮”那就再用transactionIsReadyToBeApplied函数检查这封信本身是否“就绪”。如果就绪放入“处理中”篮子如果未就绪就为它新建一个“专属待办篮”把它放进去挂起。这个双队列机制的精妙之处在于它实现了基于依赖和状态的事务分组与同步。同一个applyToken通常来自同一个客户端或同步屏障的事务会被序列化处理避免了状态竞争。而“就绪检查”则确保了合成系统只在所有资源尤其是图形缓冲区都到位时才进行状态更新这是画面流畅不撕裂的根本保障。我在实际调试中经常用Perfetto工具观察mTransactionQueue的深度变化。你会清晰地看到在queueTransaction调用时队列深度增加在flushTransactionQueues被处理时深度减少。如果某个事务因为缓冲区未就绪而卡在mPendingTransactionQueues里你也能通过跟踪其applyToken和缓冲区fence状态来定位瓶颈。2. 状态同步从“排队”到“生效”的临门一脚经过flushTransactionQueues的分拣“就绪”的事务被收集到了transactions这个临时容器里。但这只是完成了“分拣”还没开始“派送”。接下来就是让这些事务中的状态变更真正生效到各个图层Layer和显示Display上。这个过程我称之为状态同步。flushTransactionQueues函数的最后有一个关键的for循环它遍历所有“就绪”的transactions对每一个调用applyTransactionState函数。这个函数是状态同步的起点它负责解析事务包里的具体内容。一个TransactionState里面主要包含两种状态DisplayState显示设备状态如分辨率、旋转和ComposerState图层状态这才是大头。对于绝大多数来自普通应用的事务DisplayState是空的核心是ComposerState。applyTransactionState会遍历所有的ComposerState对每一个调用setClientStateLocked函数。setClientStateLocked这个名字听起来有点唬人其实干的是“搬运工”的活。它的核心作用是把客户端通过layer_state_t结构体传递过来的新属性比如位置、大小、透明度、缓冲区句柄等赋值到对应图层的mDrawingState成员变量里。这里有个非常重要的概念SurfaceFlinger中的每个Layer对象至少有两套状态mCurrentState这是图层最新的、已提交但可能还未用于绘制的状态。setClientStateLocked修改的就是这里。mDrawingState这是上一帧合成时实际使用的状态。为什么需要两套这是为了实现原子性的状态更新。客户端可以在任何时间提交事务但SurfaceFlinger必须在固定的VSync节奏点去合成画面。通过两套状态SurfaceFlinger可以一次性把所有待处理的事务都应用到mCurrentState上然后在合适的时机将mCurrentState整体同步到mDrawingState用于下一帧的合成。这避免了在合成过程中状态发生改变导致的画面撕裂。那么这个“合适的时机”是什么就是handleTransactionLocked函数中调用的commitTransaction。这个函数内部会执行一句至关重要的赋值mDrawingState mCurrentState;。这一行代码完成了所有已应用事务状态的最终提交让新状态对后续的合成流程可见。注意setClientStateLocked在搬运状态的同时还会处理事务回调ListenerCallbacks。它会创建CallbackHandle对象并暂存起来。这些回调会在事务状态真正提交到mDrawingState即commitTransaction执行后并且在对应的那一帧被合成上屏之后才会被触发通知客户端。这保证了客户端收到“事务已生效”回调时其效果确实已经显示在了屏幕上。3. 合成触发事务处理的最终乐章事务状态同步到mDrawingState之后是不是立刻就会合成新的一帧呢不一定。这取决于事务修改了什么。在applyTransactionState和setClientStateLocked的执行过程中代码会根据状态变更的类型设置一些transactionFlags。常见的标志位有eTraversalNeeded表示图层树的结构或状态发生了变化比如增加了子图层、改变了层级需要重新遍历整个图层树来计算可见区域、裁剪区域等。eTransactionNeeded一个更通用的标志表示有事务需要处理。eDisplayTransactionNeeded显示设备状态发生了变化。这些标志位会被setTransactionFlags函数收集起来。在handleMessageTransaction的最后如果判断出有非eTraversalNeeded的标志位被设置或者有一个强制遍历标志mForceTraversal被置位它就会调用handleTransaction函数。handleTransaction函数的核心是handleTransactionLocked而后者最终会调用我们刚才提到的commitTransaction来完成状态的最终提交。更重要的是在commitTransaction之后如果之前设置的标志位表明需要一次新的合成例如有图层的缓冲区内容更新了那么setTransactionFlags会设置一个eTraversalNeeded标志注意在applyTransactionState里这个标志被清除了但这里可能因为其他原因被再次设置。这个eTraversalNeeded标志是触发合成流程的关键。它会导致SurfaceFlinger在接下来的handleMessageRefresh阶段同样是主线程消息循环的一部分执行一次完整的遍历Traversal和合成Composition。遍历过程会基于最新的mDrawingState计算每个图层的几何信息、可见性、最终输出缓冲区。合成过程则是由HWC硬件合成器或GPU将这些缓冲区按照正确的顺序和混合方式最终绘制到屏幕上。所以整个链条就串起来了事务入队 - 就绪检查 - 状态应用到mCurrentState - 提交到mDrawingState - 设置合成标志 - 触发遍历与合成。这就是一个Transaction在SurfaceFlinger服务端从接收到生效的完整生命旅程。4. 实战解析从日志与工具看事务流光讲理论可能有点干咱们结合实战看看。理解这套机制对调试UI卡顿、掉帧问题非常有帮助。假设你遇到一个场景手指滑动列表很卡。用Systrace或Perfetto抓取trace后你可能会在SurfaceFlinger线程看到handleMessageTransaction或flushTransactionQueues耗时很长。这时候怎么分析第一看事务队列深度。在Perfetto的计数器轨道Counter Track里添加TransactionQueue这个跟踪点ATRACE_INT(“TransactionQueue”, mTransactionQueue.size())。你会看到队列深度在queueTransaction时上升在flushTransactionQueues时下降。如果队列深度持续很高说明事务生产速度大于消费速度有积压。这可能是因为主线程太忙VSync信号到了却没来得及处理事务。第二看事务是否被挂起。如果事务积压不在mTransactionQueue那很可能卡在mPendingTransactionQueues了。这通常意味着事务关联的图形缓冲区没有及时就绪。你需要检查App的渲染线程是不是dequeueBuffer拿不到缓冲区生产者速度慢或者是queueBuffer太晚GPU渲染慢缓冲区未就绪acquire fence就不会signaledtransactionIsReadyToBeApplied就会返回false事务就会被挂起。第三看事务处理的关键函数耗时。重点关注flushTransactionQueues特别是其中transactionIsReadyToBeApplied被调用的次数和耗时。setClientStateLocked如果单个事务包含非常多的图层状态变更比如一屏列表项同时更新这里可能耗时。commitTransaction当图层树非常复杂时遍历提交也可能耗时。我遇到过的一个典型坑是一个自定义View在onDraw里频繁且错误地调用Surface.lockCanvas和unlockCanvasAndPost这会导致它向SurfaceFlinger海量地提交只修改了很少像素甚至没修改的事务。SurfaceFlinger需要为每一个事务走一遍完整的检查、状态应用流程造成了巨大的CPU开销导致整体滑动卡顿。解决方案就是规范Canvas的获取和提交避免无效更新。另一个常见情况是动画。Android的动画系统通常会提交带有eAnimation标志的事务。在queueTransaction函数中对于动画事务有一个特殊的等待逻辑如果当前已有相同applyToken的动画事务在挂起队列中新的动画事务会等待直到旧的被应用。这保证了动画事务的严格顺序避免错帧。但如果你在UI线程密集操作时同时触发多个动画这个等待也可能成为卡顿点。理解Transaction在Server端的流转就像掌握了SurfaceFlinger的“交通规则”。它能帮你从系统层面洞悉UI卡顿的根源到底是应用提交太频繁还是GPU渲染太慢或者是SurfaceFlinger自身处理不过来。下次再分析性能问题不妨顺着这条“从队列到合成”的链路一步步追查下去相信你会有更清晰的诊断思路。