4. 快照重计算流程剖析:从 Layer 更新到 SurfaceFlinger 绘制通道的完整链路
说实话,很多做性能优化的朋友,一碰到 ANR 就直奔主线程堆栈。
但快照重计算这个坑,藏得更深。
它不卡你的应用主线程,而是卡在 SurfaceFlinger (SF) 这边。你的 App 可能早就画完了,但 SF 就是刷不出来。为什么?因为它在忙着重建快照。
这章我们就来彻底扒开这条链路,看看从 App 更新一个 Layer,到 SF 最终合成画面,中间到底发生了什么。
4.1 触发时机:什么时候会重算快照?
先搞清楚一个事儿:不是每次界面变化都会触发快照重计算。
我个人习惯这样去记:只有那些会改变 显示层级结构 的操作,才会触发 SF 去重建一份新的可见窗口列表(也就是快照)。
| 触发场景 | 关键操作 | 流程描述 |
|---|---|---|
| Window 添加/移除 | startActivity, finish, dialog show/dismiss | WMS 通过 binder 通知 SF 更新 Layer 树 |
| Window 层级变化 | setWindowType, 焦点切换 | 修改 Z-order,SF 需要重新排序 |
| Window 属性变化 | setAlpha, setMatrix, setSize | 这些改变直接导致快照内容无效 |
| SurfaceControl 事务 | Transaction.apply() | 将多个属性变更打包提交 |
我记得有一次排查一个桌面启动器的 ANR,发现就是用户快速切换主题时,触发了大量 setAlpha 和 setMatrix 调用,导致 SF 主线程被快照重计算塞满,最后演变成全局卡死。
关键结论:快照重计算是 SF 主线程的同步操作。只要它开始算,所有新的 buffer 提交都在排队等着。
4.2 流程拆解:一步一步走到 SF 主线程
我们来一条一条看。整条链路大概可以分为四个阶段:
- App 端: 更新 Layer 属性,提交事务(Transaction)
- BufferQueue: 生产新的 GraphicBuffer,通过 Binder 发往 SF
- SF 主线程(关键): 处理 MessageQueue,进入
onMessageInvalidate→handleTransaction→setClientStateLocked - 快照更新: 执行
rebuildLayerStack,完成后通知合成通道(HWC 或 GPU)
这里最容易忽视的,就是第二个环节到第三个环节之间的 队列等待。
一个小技巧:你可以通过抓取 SF 的 trace(命令 atrace sf),观察 handleTransaction 和 rebuildLayerStack 这两个 marker 的间隔时间。如果超过 8ms 甚至 16ms,说明快照计算已经压垮了主线程。
下面这张图,是我自己梳理的流程结构,你们可以对照着看:
4.3 关键瓶颈:主线程被卡在哪?
流程清楚了,真正的难点在于:一个简单的 Transaction,为什么就触发了那么重的计算?
其实核心就在 rebuildLayerStack 这个函数里。我把它内部做的事情拆开看:
- 遍历所有 Layer 树: 计算每个 Layer 的可见性(
isVisible) - 应用变换矩阵: 每个 Layer 可能带着复杂的变换,需要重新计算其在屏幕空间的位置
- 裁剪与遮挡: 计算各个 Layer 之间的遮挡关系,决定哪些部分需要合成
- 生成 Z-order 排序: 按照 Z 轴重新排列可见 Layer
- 更新快照映射表: 把新的 Layer 列表传递给合成引擎
我当初在一个双屏手机上做优化时,发现 rebuildLayerStack 一次要跑 12ms。为什么?因为那台机器有 6 个显示屏幕,每个屏幕都要独立计算这套流程。你想想看,一个操作触发 6 次 12ms 的计算,主线程直接就卡死了。
避坑指南:我曾经遇到过三方 SDK 在后台疯狂调用 Transaction.setAlpha(0f),每帧都发,明明界面都不可见了还在刷。这种无意义的属性变更,空转了大量快照重计算,还导致 GPU 侧产生大量空帧合成。抓到后直接让业务方改为全局状态监听,避免高频重复提交。
4.4 定位实战:怎么抓到偷时间的凶手?
光说不练假把式。我一般定位这类问题,会抓三样东西:
- SF 的 systrace: 看
handleTransaction和rebuildLayerStack的时长分布 - SF 的 logcat: 观察
SF_DEBUG级别的日志,看是否有大量事务涌入 - App 侧的 Binder 调用: 使用
systrace -a 包名,看 App 是否存在高频的Transaction.apply()
这三个维度一结合,基本能锁定到底是谁在给 SF 主线程塞垃圾。
一个实战命令:
// 抓取 SF 相关的 trace,动态观察事务处理耗时
adb shell atrace --async_start --time_before_end=true sf gfx
// 复现问题后,拉取 trace
adb shell atrace --async_dump -o /data/local/tmp/sf_trace.txt
然后用 Perfetto 打开,重点看 sf.mThread 这个 slice 的宽窄。如果每个 slice 都超过 12ms,那你的快照重计算一定有问题。