本文由@溯光 出品,转载请注明出处 站点链接:技术博客页 | 公众号:溯光的图形笔记
核心问题:上一篇把"Vsync 之后 SF 做了什么"讲成三段——处理事务、latch、合成。第一段"处理事务"当时只占了一句话。这一篇就把它打开:一次事务到底改了什么、改动怎么攒起来、又凭什么说是"原子"的?
一个具体的现象就能把它逼出来:下拉通知栏的时候,状态栏和任务栏是一起动起来的,而且它们做的是同一段插入动画。 可是这两层在系统里是两个独立的层,它们凭什么"一起动"?如果去看一眼层的清单,答案就在眼前——它们的父节点在同一时刻被换成了同一个新层:
StatusBar的parentId从 110 变成 115、Taskbar的从 109 变成 114,而那个#115的名字里写着animation-leash of insets_animation。一次下拉,改的不是像素,是层树的结构。
承接上一篇。ep06 讲的是"关口":一轮刷新里 SF 做了什么、合成归属怎么被标出来、present 怎么与刷新周期对齐。ep04 讲的是"账":一条队列自己怎么记这一笔。而这两篇之间一直有个没打开的东西:应用是怎么把"我要改这一层的样子"这个意思告诉系统的? 答案就是本篇的主角——SurfaceControl 的事务(Transaction)。
这件事在前几篇里也挂过账。ep04 §8 讲 BLAST 时提过"缓冲交付被转写成一笔事务",ep06 §1.1 把"处理事务"列为三段里的第一段、并说明它属于 ep07 的题域,ep06 §9.3 的下一篇预告也写明下一篇要讲 SurfaceControl 的那层透明事务。
所以这一篇是来还债的:把"应用改层属性 → 攒成一笔事务 → 原子地交出去 → 系统怎么接"这条链讲清。它回答的不是"合成怎么决定"(那是 ep06),也不是"硬件怎么叠"(那是 ep08),而是改动的语义:改什么、怎么攒、凭什么原子。
三篇共用同一条链路,各篇量的东西仍然不同:
| 维度 | ep04《BufferQueue》 | ep06《SurfaceFlinger》 | ep07(本篇) |
|---|---|---|---|
| 关心的对象 | 记账:队列自己怎么记这一笔 | 关口:缓冲从哪一刻被推上去 | 改动:层的属性是怎么被改的 |
| 量什么 | 数量与状态 | 合成归属与送屏节拍 | 层树结构与属性:parentId、z、bounds、可见性 |
| 主要命令 | SurfaceFlinger 的队列段 |
--timestats + --latency |
SurfaceFlinger --list + 层段 + gfxinfo framestats |
| 典型结论 | 公式值 2、实打 2、已分配 3 | 静置 0 次 present;峰值 16 ms 对上 60 Hz | 一次下拉 = 一笔 reparent:StatusBar 的 parentId 110 → 115 |
本篇也不涉及:合成决策与送屏(ep06 已写,本篇只回指)、HWC 叠加层实现(ep08,需真机)、Vsync 偏移三步舞(ep10)、多刷新率与 ARR(ep11,需真机)、BLAST 的队列归属细节(ep04 已写,本篇只取"缓冲交付被转写成事务"这一句接口关系)、Perfetto 与 Winscope 自身的方法论(《图形性能剖析工具链》已写)、渲染算法与画面技术(《实时渲染管线探析》已写)。为什么不涉及:这一篇的价值集中在"改动的语义"这一个点,把事务能改什么、怎么攒、原子性保证到什么程度三样讲透,掺进别的题域,这件事就讲不透了。完整边界见 §0.3。
系列文章写作风格说明(沿用前六篇,不再重复解释)
- 术语双语:首次出现的核心术语写成「中文(English)」,后文可只用英文;
- 三层分开说:官方文档怎么写、AOSP 源码怎么做、本机跑出来是什么,这三层分开陈述,不混成一句;
- 读数标口径:每一个数字都标来源与条件(机型、Android 版本、命令、采样时刻);
- 模拟器与真机分开写:本机走 gfxstream + SwiftShader 软实现,凡是性能类结论一律不外推为真机;需要 root 的工具(如 Winscope 的逐事务抓取)与真机行为一律如实标"本机不可用/需真机"。
系列主要内容总览
- 【Android 图形显示系统】(一) 总览:从 App 到屏幕的图形管线
- 【Android 图形显示系统】(二) Choreographer:谁在召唤 Vsync
- 【Android 图形显示系统】(三) RenderThread 与帧提交:从显示列表到一块缓冲
- 【Android 图形显示系统】(四) BufferQueue:图形缓冲区的邮局
- 【Android 图形显示系统】(五) Vulkan on Android:低开销渲染入口
- 【Android 图形显示系统】(六) SurfaceFlinger:在 Vsync 之后做什么
- 【Android 图形显示系统】(七) ASurfaceControl:Android 10 的透明事务(本篇)
- 【Android 图形显示系统】(八) HWC:GPU 合成之外的硬件叠加
- 【Android 图形显示系统】(九) HWC2:Vsync 回调和虚拟显示路径
- 【Android 图形显示系统】(十) Vsync 偏移:应用、SF、屏幕的三步舞
- 【Android 图形显示系统】(十一) 三缓冲、多刷新率与 ARR
- 【Android 图形显示系统】(十二) 虚拟显示与屏幕录制
- 【Android 图形显示系统】(十三) 剖帧实战:Perfetto FrameTimeline 抓 jank
相关资源链接
- 官方一手来源:SurfaceFlinger 与 WindowManager | SurfaceControl.Transaction | NDK · ASurfaceControl / ASurfaceTransaction | Winscope · 分析 SurfaceFlinger | Winscope · 用 adb 抓取
- 源码入口:
libs/gui/SurfaceControl.cpp|libs/gui/SurfaceComposerClient.cpp|services/surfaceflinger/BufferStateLayer.cpp - 与本文强绑定的前置:ep01《总览》| ep03《RenderThread 与帧提交》| ep04《BufferQueue》| ep06《SurfaceFlinger》
这一篇回答什么,不回答什么
OK,OK,先把坐标系挪正。前六篇给了地图、叫醒链、交接、账本、入口、关口;这一篇要给的是改动的语义——应用想让屏幕上某个东西"换个位置、换层、藏起来",这个意思是怎么被表达、被打包、被一次交出去的。
0.0 内容总览:本篇知识地图
好了,先把十章各自回答的问题钉在一张表上。它同时也是本篇的目录总览。如果时间紧,第一章(事务是什么)、第三章(原子性)、第七章(一次真实的 reparent)是这篇最不可替代的三章:第一章给定义,第三章讲保证,第七章给现场。
| 章 | 回答的问题 | 关键结论 |
|---|---|---|
| 零 | 这一篇补哪一段 | 一篇讲"改动语义"的文章;与 ep06/ep08/ep10/ep11 的分工 |
| 一 | 事务到底是什么 | 官方一句定义:一组对 SurfaceControl 的原子改动;攒 → 合并 → apply |
| 二 | 事务能改哪些属性 | 七个 setter 的官方口径;setGeometry 已在 API 33 废弃 |
| 三 | 原子性保证到什么程度 | merge 把对方当作已应用清空;一笔事务里多块缓冲要等所有 present fence signal |
| 四 | ASurfaceControl 是什么 | Android 10 把事务搬到了 NDK;三件套与"组合到一个事务包" |
| 五 | 事务的完成状态怎么读 | OnCommit / OnComplete / latchTime + present fence + 已废弃的 acquireTime |
| 六 | 层树在设备上长什么样 | 本机 94 层 / 33 层带 z / 1 层带相对父级;层段能读到的属性清单 |
| 七 | 一次真实的 reparent | 下拉通知栏:StatusBar 的 parentId 110 → 115,挂到动画牵引层下 |
| 八 | 四处流传很广的说法 | 逐条核对,不立空靶 |
| 九 | 四项可执行动作 | 每条命令与判据 |
0.1 本篇那个"反直觉的现象"
好,先把这个现象摆出来,因为它撑住了整篇的骨架。
下拉通知栏时,状态栏和任务栏会一起做同一段插入动画——但它们是不相干的两层。 一个在屏幕顶上,一个在屏幕底下,平时各管各的。它们凭什么"一起动"?
答案不是"它们在同一个动画里",而是它们的父节点被换成了同一个:
下拉前: StatusBar#87 parentId=110 Taskbar#80 parentId=109
下拉后: StatusBar#87 parentId=115 Taskbar#80 parentId=114
新增层:…/insets_animation#115 parentId=86
(另一条:…/insets_animation#114 parentId=79)
为什么读层段值得花时间? 原理上讲,层段读的是层树在某一瞬间的完整快照——它同时包含结构(谁挂在谁下面)与属性(几何、可见性、缓冲、变换)。而事务改的恰恰就是这些东西。所以层段可以当作事务的结果面来读:你不需要知道是谁改的、怎么改的,只要比较两次快照,就能看出改动落在了哪些字段上。读出来的意思是:系统为了让这两层一起做动画,给它们各挂了一个"动画牵引层"(名字里带 animation-leash of insets_animation),然后把状态栏与任务栏重新挂到牵引层之下。这样只要动牵引层,挂在它下面的两层就一起动。改的是层树的结构,不是任何一层的像素。
而这件事,就是一笔事务干出来的。
0.2 本篇不涉及什么(边界声明)
开篇把边界说死,是这几篇的统一约定。本篇:
- 不讲合成决策与送屏:哪一层交给硬件叠、present 与刷新周期怎么对齐,都是 ep06 的内容,本篇只在需要时回指。
- 不讲 HWC 叠加层的实现与合成模式:那是 ep08,且需真机。
- 不讲 Vsync 偏移三步舞与多刷新率:分别属 ep10 与 ep11(后者需真机)。
- 不搬厂商配图、不使用任何 logo 文件:Commons 上 Android 与 SurfaceControl 相关标识均为注册商标,且对技术论证没有贡献(§8.4)。
- 不假装读到读不到的东西:Winscope 的逐事务抓取需要
adb root(官方明确要求),而本机是user构建、ro.debuggable = 0,拿不到 root;本机旋转实验也未生效。这两件事都会在 §5.3 如实登记(§7.1)。
本机观测环境(本篇 11 张本机真图的来源,先交代清楚):无物理设备,统一用 Pixel_7 无窗冷启动(-no-window -no-audio -no-boot-anim -gpu swiftshader_indirect -no-snapshot);宿主 Android emulator,图形后端 gfxstream;系统镜像 android-36/google_apis_playstore/x86_64;设备侧 ro.build.version.release = 16、sdk = 36、user / release-keys、ro.debuggable = 0。
0.2.1 本机观测环境与四类读数来源面板
--list)/ 层段属性 / 逐帧时间戳 / 做不到的登记,本篇 11 张本机真图全部来自这套无窗冷启动环境@ 本机实测(Pixel_7 AVD,Android 16 / API 36)这五条边界不是客套,它们直接决定了本篇的证据类型。原理上,一篇技术文章的结论强度受限于它的取证手段:能用命令读出来的(层清单、属性字段、前后对照)是一手证据;官方文档写明的规则是权威依据;社区文章与教程只能作旁证。本篇把这三类出处逐处标出来,凡属"模拟器环境产物"或"需要 root 才能拿到"的结论一律注明。作为对照,SurfaceFlinger 与 WindowManager 与 SurfaceControl.Transaction 两页是本篇回引最多的权威依据——凡是它们已经写明的事,本篇尽量引原话。
一、事务到底是什么
OK,这一章先给定义。事务这个词被用得很泛,但在 Android 图形栈里它有非常具体的官方定义,而那一句定义里就藏着本篇全部内容。
1.1 官方那一句:一组原子改动
开发者文档对 SurfaceControl.Transaction 的第一句定义就是本篇的题眼:
An atomic set of changes to a set of
SurfaceControl.
这一句里有三个要点,值得逐一拆开:
- "a set of changes"(一组改动):一笔事务可以同时改多个对象、多项属性——不是"改一处交一次";
- "to a set of SurfaceControl"(改的是若干 SurfaceControl):改动的对象是层的句柄,不是缓冲区内容;
- "atomic"(原子):这一组改动要么都生效、要么都不生效——不存在"改了一半"的中间状态。
1.2 生命周期:攒 → 合并 → apply
定义的下一句是生命周期。官方说:创建一个事务对象之后,可以往里塞对 SurfaceControl 的操作,然后用 apply() 原子地应用它;而 apply() 的语义还包括"清空它的状态、并让它能被当作一个新事务继续复用"。
把这句话摊成四步:
① 开一笔事务 Transaction()
② 往里塞操作 setPosition / setAlpha / setLayer / setVisibility / setBuffer …
③ (可选)把别的事务并进来 merge(other) ← 对方被清空,如同已被应用
④ 一次交出去 apply() ← 原子生效;对象随后可复用
(以上构造与生命周期口径见 SurfaceControl.Transaction。)这里有个容易被忽略的设计:apply() 之后事务对象可以复用,说明它更像一支"笔"而不是一张"表单"——你可以一直用它写、写完交、交了再写。这也解释了为什么高频改层属性的场景(比如动画)不会疯狂分配对象。
1.3 本机这一眼:94 条层记录
好,定义讲完了,落回设备看一眼"被改的那些对象"到底长什么样。
dumpsys SurfaceFlinger --list 会把系统里注册的层一次列出来,每条形如:
RequestedLayerState{WindowedMagnification:0:31#1 parentId=45}
RequestedLayerState{ImePlaceholder:13:14#10 parentId=7 z=2}
RequestedLayerState{HideDisplayCutout:0:14#2 parentId=1}
本机一次列出 94 条。逐条解析之后(layers_parsed.json):
94 条全部解析成功
其中带 z= 33 层
其中带 relativeParentId= 1 层
1.3.1 本机层清单现场
--list 的现场:一次列出 94 条层记录,逐条带 parentId / z / relativeParentId——这三样正是事务能改的属性在清单里的样子@ 本机实测(Pixel_7 AVD,Android 16)这三行读数正好对应事务能改的三类属性:parentId(对应 reparent)、z(对应 setLayer)、以及那些只有个别层才带的字段。"33 层带 z、1 层带相对父级"这个比例本身也在说话:绝大多数层用的是绝对 z 序,只有极个别层用了相对父级的锚点——下一章会讲这两者的差别。
口径警示 ①:
--list里那个#N不是层段里的[N]。同一时刻,状态栏在清单里是StatusBar#87、在层段里却是Layer [104] StatusBar#104。两套编号不能互相代入,跨命令比对时认层名,不要认编号。
二、事务能改哪些属性
好了,定义说"一组改动",那到底能改什么?官方给了一串 setter,每一个都有明确的作用范围——这一章把七个主要的逐一对齐。
2.1 七个 setter 的官方口径
| setter | 官方口径 | 一句话理解 |
|---|---|---|
setPosition(sc, x, y) |
相对父层设置位置 | 位置是相对父节点算的,不是绝对坐标 |
setLayer(sc, z) |
设置相对兄弟层的 Z 序 | 层内前后关系,只和同级兄弟比 |
setAlpha |
取值 0.0–1.0 | 不透明度,作用在该层自身上 |
setVisibility |
"Toggle the visibility of a given Layer and it's sub-tree." | 作用于整棵子树,不只是这一层 |
reparent |
"Children inherit transform (position, scaling) crop, visibility, and Z-ordering from their parents" | 换父节点会连带继承一整套属性 |
setBuffer |
交出这块缓冲 | 与 BLAST 的接缝(ep04 讲过) |
setGeometry |
API 33 起废弃,官方建议改用 setCrop / setBufferTransform / setPosition / setScale |
一个被拆解掉的复合 setter |
2.1.1 七个 setter 的对照
为什么这些 setter 大多是「相对」的? 原理上讲,层树是一棵会变的树:父层可能被换掉、兄弟可能被增删。如果位置与 Z 序都写成绝对值,那么父层一动,所有子层的绝对坐标就全错了——每次结构调整都要把所有后代重算一遍。反过来,把位置与 Z 序都定义成相对父层、相对兄弟,父层动的时候子层只需要跟着走,不用任何重算。这也是第七章那个实验能成立的前提:换父节点之所以能一次性解决两层一起动的问题,正是因为位置和前后本来就是相对的。
把这张表读进去,会发现一个规律:事务改的是"关系",不是"值"。(七个 setter 的逐条口径同样见 SurfaceControl.Transaction。) 位置相对父层算、Z 序相对兄弟比、可见性连带子树、reparent 还会继承一整套属性——这些属性的意义都取决于它在层树里的位置。这也解释了为什么本篇第七章那个"改父节点"的操作值得单独讲一章:在这样一棵树里,换父节点等于换掉了这一层的整套参照系。
2.2 两条最容易误读的规则
第一条:setVisibility 不是"把这一层藏起来",而是"把这一层和它下面整棵子树藏起来"。 官方原话里那句 "and it's sub-tree" 是这个 setter 的定义的一部分。所以对一棵有子层的层调用它,效果会波及全部子层。
第二条:reparent 会连带继承。 官方说明写着子层会从父层继承 transform(位置与缩放)、crop、visibility 与 Z 序。换父节点之后,这一层的实际呈现是"它自己的属性 + 新父层传下来的属性"叠加的结果。
2.2.1 子树语义与继承规则
不过继承有个官方写明的例外,值得单独记:相对父级(relativeParentId)那一层,子级不会从它继承任何属性,只继承隐藏标志。所以,"相对父级"只是用来定 Z 序锚点的,不参与属性继承。本机 94 层里只有 1 层用了相对父级——这条例外在现实中是少数派,别拿它去解释所有层的行为。
2.3 本机层段能读到的属性
好了,那这些被改的属性,在设备上读得到吗?能。dumpsys SurfaceFlinger 的层段会把每层当前的样子列出来,本机原样长这样:
Layer [104] StatusBar#104
visible reason= buffer=3938485010454 frame=163 color{< 0, 0, 0, 1 >}
bounds={0,0,136,1080}
input{(NOT_FOCUSABLE | TRUSTED_OVERLAY) touchableRegion={0,0,136,1080}}
Layer [97] IME Handwriting Surface#97
invisible reason=nothing to draw
bounds={0,0,2400,1080}
2.3.1 层段可读字段
把这些字段与第二章的 setter 表对一下,对应关系就出来了:
| 层段字段 | 对应哪个 setter 改的东西 |
|---|---|
bounds= |
setPosition / setGeometry 一类几何改动 |
visible / invisible reason= |
setVisibility(注意 invisible 会给出原因字符串) |
buffer= + frame= |
setBuffer(每一轮交出的那块缓冲与它的序号) |
toDisplayTransform= |
层树传递下来的变换 |
input{...} |
该层的输入属性与可触区域 |
注意最后两行的差别:bounds 是该层自己的几何,toDisplayTransform 是沿层树传下来的变换——这两者不一致时,说明有父层在参与。这正是第二章那条"属性沿层树传递"在读数上的样子。
三、原子性保证到什么程度
OK,第一章那句定义里的第三个词是 atomic。但"原子"是个很重的词,得说清它到底保证到什么程度——在哪一层原子、对什么原子、以及不保证什么。
3.1 合并的语义:把对方当作"已应用"
先来看看 merge。官方原文是:
Merge the other transaction into this transaction, clearing the other transaction as if it had been applied.
注意后半句的措辞:它把被合并的那笔事务"当作已经被应用那样"清空。这句话解决了一个很实际的问题——同一层在一轮里被改了多次怎么办?
设想一个动画每帧都在改同一层的位置。如果每次 setPosition 都立刻生效,这一层会在一个刷新周期内被"撕"成好几个位置。一笔事务其实是先攒后交:同一层的多次修改先在事务里累加,apply 时交出去的只有最终态。这就是"同一个层的改动会被并成一笔"的意思。
3.1.1 merge 的语义
merge 的语义:把另一笔事务并进来、并把对方"当作已应用"那样清空——apply 时交出去的是最终态,中间态不会出现在屏幕上@ 本系列绘制3.2 多缓冲的原子性:等所有 present fence signal
再看原子性的"加码"部分。这是官方在 setBuffer 文档里单独写的一段,也是本篇最容易被跳过、但最该抄下来的一段:
If multiple buffers are set as part of the same transaction, the presentation fences of all of them must signal before any buffer is displayed. That is, the entire transaction is delayed until all presentation fences have signaled, ensuring the transaction remains consistent.
翻译过来:一笔事务里如果设了多块缓冲,那么在所有这些缓冲的 present fence 都 signal 之前,任何一块都不会被显示——整笔事务被推迟,直到所有 fence 都 signal。
把多缓冲原子性落成调用顺序,大致是这样:
// 同一笔事务里设多块缓冲:全部 fence signal 之前,任何一块都不显示
ASurfaceTransaction_setBuffer(txn, controlA, bufferA, fenceA); /* 1 */
ASurfaceTransaction_setBuffer(txn, controlB, bufferB, fenceB); /* 2 */
ASurfaceTransaction_apply(txn); /* 3 原子应用:1 与 2 一起生效 */
下面对这段代码特别说明:1、2 两块的设置顺序不影响原子性——要点在第 3 步应用之后,两块都要等自己的 present fence signal,任何一块没就绪,整笔都往后推。所以把不相干的缓冲塞进同一笔事务并不会让它们更快,只会让整笔等最慢的那一块。
这段话的用处在于"反着读":它告诉你原子性的边界在哪(原文见 SurfaceControl.Transaction 的 setBuffer 段)。原子性保证的是显示时机一致,而不是"更快"。如果一笔事务里塞了多块缓冲,而其中一块迟迟画不完,整笔事务都要等——这也从另一个角度说明为什么事务不应该越攒越大。
3.2.1 多缓冲原子性
3.3 "原子"的直觉:一张外部对照图
"原子"这个词最容易借到的直觉来自数据库——那里的事务也是"要么全做、要么全不做"。这两个词在中文里是同一个"原子",在英文里也是同一个 atomic,所以借它的直觉很自然:一组操作要么全部落地、要么全部不落地,中间态不可见。Commons 上恰好有一张讲这件事的示意图,本机没有对位的图,所以这里借它的直觉作对照。
这张图也得说清边界:它是数据库领域的图,本篇只借「原子」一词的直觉——Android 的 LayerState 事务不是数据库事务:它没有回滚、没有持久化、也不保证跨进程的隔离级别。
口径警示 ②:借用"原子"这个词的直觉是安全的,但不能把数据库事务的性质平移过来。Android 这件事物没有"回滚"(不存在"提交失败就把改动撤回去"),也不保证跨层之间的时序隔离——它保证的是"这一组改动在同一个生效点一起落地"。边界说清,类比才有用。
3.4 原子性在 Android 侧的准确含义
那把"原子"落到 Android 这套机制上,它保证了什么、没保证什么?可以收成三条:
- 保证了"同一生效点":
apply()之后,这笔改动在同一个生效点一起落地——而那个生效点是 SF 处理它的那一轮(也就是 ep06 讲的"第一段:处理事务"),不是apply()调用的那一刻; - 保证了"不出现中间态":同一层的多次修改只交出最终态,屏幕上不会出现"改了一半"的样子;
- 没保证"立刻":事务是异步应用的(第四章的
OnComplete文档里明确写着 "Since the transactions are applied asynchronously")。apply()返回不等于屏幕已经变了。
为什么它会做成异步的? 原理上讲,事务改的是系统侧共享的层状态,而应用与 SF 是两个进程——如果 apply() 要同步等到改动落定,那么每一次改属性都会变成一次跨进程往返加等待,动画场景下这个代价无法接受。异步 + 回调这套设计,正是为了把提交与完成解耦:提交要快,完成才需要知道。第三条最容易被误读。 很多人把 apply() 理解成"提交并立即生效",于是写出"改完属性马上截图验证"的代码——但那一瞬间屏幕很可能还是旧的。正确的心智是:apply() 只是把这笔改动交出去,它什么时候真正影响屏幕,要看 SF 什么时候处理到它,以及 fence 什么时候 signal。
四、ASurfaceControl:Android 10 把事务搬到了 NDK
好了,前三章讲的是 Java 侧的 SurfaceControl.Transaction。但对原生应用来说,真正的分水岭在 Android 10——官方为它开了一套 C API。这一章讲这套 API 是什么、以及它与 Java 侧的关系。
4.1 官方那一句:事务被组合成"一个包"
AOSP 的原文写得很清楚:
Android 10 新增了 ASurfaceControl,这是 SurfaceFlinger 接受缓冲区的另一种方式。ASurfaceControl 将 Surface 和 SurfaceControl 组合到一个事务包中,该包会被发送至 SurfaceFlinger。ASurfaceControl 与层相关联,应用可通过 ASurfaceTransactions 更新该层。
这句话里"另一种方式"四个字是重点(原文见 SurfaceFlinger 与 WindowManager):它不是说"多了一个 API",而是说向 SF 交东西的方式多了一条——除了传统的 BufferQueue 那条路(ep04 讲的那条),还有"把层与缓冲一起打包成事务交上去"这条。
官方给的三件套:
| 组件 | 官方定位 |
|---|---|
ASurfaceControl |
对 SurfaceControl 的封装,让应用能创建与屏幕上层对应的对象(可作为 ANativeWindow 或另一个 ASurfaceControl 的子级) |
ASurfaceTransaction |
对事务的封装,让客户端能修改层的描述性属性(比如几何图形)并把更新的缓冲发给 SF |
ASurfaceTransactionStats |
通过预先注册的回调,把锁定时间、获取时间、上一次释放栅栏等送回去 |
为什么要在 Android 10 为 NDK 单开一套? 因为原生应用与游戏引擎的渲染循环不能每一帧都跨一次 Java 边界——那是渲染循环里最不该出现的东西。把层与事务做成 C API,等于把改层属性这件事从 Java 侧挪到了原生侧,让引擎能在自己的线程里直接表达改动。所以这一章讲的不只是多了一个 API,而是改动这条通道被铺到了原生侧。
4.1.1 ASurfaceControl 三件套
4.2 NDK 侧的原子性措辞更硬
同一个概念,NDK 的模板说明里用的是更硬的措辞:ASurfaceTransaction 被描述成「is a collection of updates to the surface tree that must be applied atomically」——"必须原子地应用"。
对比一下两侧的措辞:Java 侧说 "an atomic set of changes",NDK 侧说 "must be applied atomically"。同一个语义,两种语气——这不是笔误,而是这份 API 的定位差异:C API 的读者是引擎与原生应用,措辞更强调约束。
4.3 两个入口,同一套底层对象
OK,那 Java 侧和 NDK 侧是两套东西吗?不是。 官方在 NDK 参考里提供了互转接口:
ASurfaceControl_fromJava(JNIEnv*, jobject surfaceControlObj)
ASurfaceTransaction_fromJava(JNIEnv*, jobject transactionObj)
"把 Java 对象包成 ASurfaceControl / 包成 ASurfaceTransaction"——能这样来回包,说明底层是同一个事务对象。
4.3.1 双入口同源
"对象可复用"这个设计还牵出一个心智能上的差别:如果你以前用惯了"提交即销毁"的一次性对象,容易下意识地在每轮循环里新建一个事务;而 apply() 之后对象已被清空,同一个事务对象可以反复用来写下一轮改动。这不是省一点点内存的问题——在动画这种每帧都要改属性的场景里,它把"分配 / 释放"从热路径上挪走了。要留意边界:可复用指的是对象本身,不是它携带的改动——一旦 apply,之前塞进去的操作就都交出去了,不会残留到下一轮。
这个结论有实用价值:如果一台设备上某个 Java 侧的行为与 NDK 侧不一致,那不是"两套事务打架",而是同一套东西被两个入口以不同顺序使用了。排查时应该往"调用顺序与时机"上找,而不是去找"两套实现"。
4.4 谁来创建与持有这些层
下面补一个角色问题:这些 SurfaceControl 是谁的?官方在讲层的时候给了答案:层是"Surface(含 BufferQueue)+ SurfaceControl(含层的元数据)"的组合;WindowManager 向 SurfaceFlinger 请求层,SF 创建层并下发给 WindowManager,随后把 Surface 交给应用、但自己保留 SurfaceControl,用来操控应用在屏幕上的外观。
这段说明两件事:第一,层的创建者与持有者是 WindowManager/SF,不是应用;第二,应用拿到的是 Surface(画的地方),而"改这一层长什么样"的权力在持有 SurfaceControl 的那一方手里。
这是通用窗口管理器的示意图,不是 Android 的实现,本篇只借它承担「谁持有层」这一处示意。
这对应用的实际含义是:一个应用只能改自己那条通道上的层,而系统想让一组层一起动时,走的是它自己手里那棵层树——两条通道互不越界。所以当你在设备上看到某个系统层的 parentId 被改掉时,可以合理地推断:这不是某个应用干的,而是层树持有方在系统侧调整自己的结构。反过来说,如果应用想让自己的若干个子层一起动,它得先拿到这些层的 SurfaceControl,而这就回到了「谁持有层」这个问题上。
这也解释了第一章那个现象为什么成立:能给状态栏挂"动画牵引层"的,不是状态栏自己,而是持有这些层的那一方。 应用想改自己的层,走的是自己那条通道;而系统想让一组层一起动,走的是它手里的那棵层树。
五、事务的完成状态怎么读
OK,前三章讲"改什么",第四章讲"用哪套 API"。但还有一个问题没答:事务交出去之后,怎么知道它什么时候真的生效了? 这一章讲可读的三个状态,以及本机在哪一步被卡住。
5.1 三个可读的状态
ASurfaceTransactionStats 通过回调把状态送回来,官方对三个关键点的口径是:
| 状态 | 官方口径 | 读的是哪个时刻 |
|---|---|---|
OnCommit |
事务被应用、更新已准备好呈现时触发 | 改动已进入系统,可以呈现了 |
OnComplete |
由于事务是异步应用的,当包含该事务更新的某一帧被呈现后通知 | 屏幕上真的出现了 |
getLatchTime |
该帧被框架 latch 的时刻 | 改动进入了这一轮显示集合 |
getPresentFenceFd |
该事务已被呈现时 signal 的栅栏 | 呈现完成的判定 |
为什么要区分这两级状态? 原理上讲,事务从提交到上屏要穿过两个不同的世界:应用侧的提交世界(改了什么已经交给系统)与系统侧的呈现世界(这一帧有没有真的出现在屏幕上)。OnCommit 落在第一个世界的末尾,OnComplete 落在第二个世界的末尾——它们之间隔着排队、latch 与 fence 这三道关。把两级状态混起来,就会写出"回调没来所以事务失败"这类误判。
(四个回调与取值的逐条口径见 NDK · native-activity 参考。)
5.1.1 事务完成状态的时序
apply() 返回不等于屏幕已变,真正的分界点是 latch 与 present fence@ 本系列绘制把多缓冲原子性落成调用顺序,大致是这样:
// 同一笔事务里设多块缓冲:全部 fence signal 之前,任何一块都不显示
ASurfaceTransaction_setBuffer(txn, controlA, bufferA, fenceA); /* ① */
ASurfaceTransaction_setBuffer(txn, controlB, bufferB, fenceB); /* ② */
ASurfaceTransaction_apply(txn); /* ③ 原子应用:①与②一起生效 */
这段代码有两处要留意:①②两块的设置顺序不影响原子性——要点在③那一次应用之后,两块都要等自己的 present fence signal,任何一块没就绪,整笔都往后推。所以把不相干的缓冲塞进同一笔事务并不会让它们更快,只会让整笔等最慢的那一块。
把这张图和 ep06 接起来看,会有个很顺的连接:这里的 getLatchTime 那个"latch",就是 ep06 第二章讲的那一步——把新缓冲扣到层上。这就是对应关系:事务在应用侧的可读状态,与合成侧的"三轮"是同一个时间轴上的两个观察面。
口径警示 ③:
getAcquireTime已经废弃。官方说明是它可能返回SIGNAL_PENDING,调用方应当改用传入setBuffer的那个 acquire fence。所以"读 acquire 时间"这件事在新代码里不该出现——这是本篇最容易被老资料带偏的一处。
5.2 本机能做到什么:逐帧时间戳
事务的回调是应用侧 API,dumpsys 读不到它。那本机有没有替代观测面?有——逐帧时间戳。
dumpsys gfxinfo <包名> framestats 会打出应用侧的逐帧 CSV。本机拿到 139 行,表头在 ---PROFILEDATA--- 之后,每行形如:
Flags, IntendedVsync, Vsync, OldestInputEvent, NewestInputEvent,
HandleInputStart, AnimationStart, PerformTraversalsStart, DrawStart,
FrameDeadline, FrameStartTime, …
5.2.1 本机逐帧时间戳的形状
framestats 的读数形状:139 行逐帧 CSV,含 IntendedVsync / Vsync / DrawStart / FrameDeadline 一类时间戳@ 本机实测(Pixel_7 AVD,Android 16)它能近似什么、不能近似什么,要说清:它能给出"这一帧从打算到画完"的几个时刻,可以当作"事务提交之后这一帧走到哪一步"的近似观测面;但它不是事务的回调——getLatchTime、presentFenceFd 那些量在应用侧 API 里,dumpsys 拿不到。
5.3 本机两处"做不到"
好了,有两件事必须如实登记,因为其中一件恰好是很多人推荐的做法。
第一件:Winscope 的逐事务抓取在本机不可用。 官方在 Winscope 的抓取文档里明确要求:"在使用 adb 收集 Winscope 轨迹之前,请运行 adb root";而要从 Perfetto 侧抓事务数据,用的是 android.surfaceflinger.transactions 这个数据源(MODE_CONTINUOUS 模式下 SF 会把内部事务环形缓冲写出来)。本机是 user 构建、ro.debuggable = 0,拿不到 root,因此这条最理想的路径在本机走不通。
抓取口径与「先 adb root」这条要求见 Winscope · 用 adb 抓取;事务数据源名与模式见同一页。第二件:旋转实验在本机没有生效。 我按常规做法写 settings put system user_rotation 1,写入成功,但随后 dumpsys 里 orientation= 仍然是 ROTATION_0、displayFrame 仍然是 0 0 1080 2400。无窗模拟器上这次旋转没有落地,所以本篇不用"旋转改几何"当证据。
5.3.1 两处"做不到"的登记
adb root(本机 user 构建不可用)、旋转写入成功但 orientation 仍是 ROTATION_0@ 本机实测(Pixel_7 AVD,Android 16)那在没有 root 的机器上,这套排查还能走多远? 可以按三条往下走:① 用两次快照的 diff 确认"改了什么"(本机那一组 parentId 对照就是这么做出来的);② 用层段字段确认"改动落在哪一层、哪个字段上"(bounds / visible reason= / frame= 都能指认);③ 用 gfxinfo framestats 的逐帧时间戳确认"这一帧走到了哪一步"。三条合起来,能答的问题已经覆盖了日常排查的绝大多数;答不了的那一类只剩"一笔事务内部的顺序与合并次数"——而这一类的价值主要在于写文档与做工具,不在于定位现场问题。
"读不到"不等于"得不出结论",只等于"要换一组证据"。本篇的替代方案是:用两次层清单快照的 diff 来呈现事务的效果——也就是第六章与第七章要做的事。它能证的东西比事务流少(看不到"一笔事务内部的顺序"),但"改了什么"这件事它证得很干净。
六、层树在设备上长什么样
OK,前五章讲的是官方语义。这一章换个视角:这些东西在设备上到底长什么样。 层的清单就是它的样子。
6.1 规模:94 层、33 层带 z、1 层带相对父级
把第一章那一瞥补全。好了,先说一句口径:94 这个数是这台设备在这一刻的数——它随当前有哪些应用在前台、有哪些系统 UI 元素可见而变化,不是 Android 的常量,也不是「正常值」。它的用处在于量级感与结构感:知道这台机器上同时活跃着几十个层,才能理解为什么「层数超过 overlay 数」是常态而不是异常。
总条数 94
带 z= 33
带 relativeParentId= 1
6.1.1 层清单规模统计
z、只有 1 层带 relativeParentId——绝大多数层用绝对 z 序,相对父级是少数派@ 本机实测(Pixel_7 AVD,Android 16)这组比例本身是一条结论:如果"相对父级"是主流,relativeParentId 应该出现在大量层上;但它只出现 1 次。所以第二章那条"相对父级的子级只继承隐藏标志"的例外,在现实里是一条小众路径——知道它存在是必要的,但别拿它去解释大多数层的行为。
6.2 各层被更新的频率并不一致
第二个观察面是各层自己走了多远。层段里每层都带一个 frame= 序号,本机同一时刻读到:
StatusBar#104 frame=163
Taskbar#91 frame=19
IME Handwriting Surface#97 frame=15
6.2.1 三层 frame 偏斜
frame= 序号:163 / 19 / 15——状态栏被更新的次数比任务栏与手写笔层高出一个量级@ 本机实测(Pixel_7 AVD,Android 16)读 frame= 时有三条边界要记住:① 它是快照值,不是累计计数——同一层在不同时刻读到不同的数,说明它在这段时间里被更新过,但不能把两次读数的差当成"更新了几次"(中间可能绕回或跳号);② 跨层不可比——两张不同的层各有各的起点,163 与 19 之间不能算"差多少倍";③ 它只说明"这一层被更新过",不说明"更新了什么"——要看更新了什么,还得回到 buffer= 与 bounds= 上。这三条合起来是一句话:frame= 是活跃度的指示器,不是计量器。
这张图想说的是"事务不是全局事件":如果每一次事务都把整棵层树刷一遍,那所有层的 frame= 应该同频增长;而实际差出一个量级,说明每一层都有自己的更新节奏——改哪一层就只改哪一层,这是"改动是按层走的"最直白的证据。
6.3 层树的三条规则
好了,那"层树"本身怎么读?官方给了三条规则,正好对应清单里那三个字段:
parentId:父层 ID——决定这一层挂在哪棵子树下(reparent改的就是它);z:Z 序——官方原话是"z 值为 0 的层会位于其父级前方;如果多个层具有相同的 z 值,则 ID 较高的层会位于前方";relativeParentId:相对父级——官方原话是"表示 z 轴顺序中的相对父级层;子级不会从其相对父级继承任何属性,隐藏的标志除外"。这三条规则的官方出处见 Winscope · 分析 SurfaceFlinger。
6.3.1 层树的三条规则
parentId 决定归属、z 决定同级前后(z=0 在最前、同 z 时 ID 高者在前)、relativeParentId 只作 Z 锚点不参与继承@ 本系列绘制这三条规则的设计原理是什么? 原理上讲,它们分别回答层树上的三个不同问题:归属(我挂在哪棵子树下)、同级次序(我在兄弟里排第几)、以及锚点(我的 Z 序参照谁)。前两个问题决定了这一层"在哪里",第三个问题只决定它"跟谁比前后"。把归属与次序分开处理,是为了让一组层能整体动——这正是第七章那个实验背后的机制。把这三条与第二章的 setter 对照,一件事就清楚了:reparent 改 parentId、setLayer 改 z,而 relativeParentId 那条路是给"只想在 Z 序上插队、但不想继承任何属性"的场景准备的。三种字段对应三种改法,互不替代。
6.4 层段现场:三层属性并置
第三条证据是层段本身。把三个层放在一起看,能读出属性的多样性:
Layer [97] IME Handwriting Surface#97
invisible reason=nothing to draw
bounds={0,0,2400,1080}
input{... NOT_TOUCHABLE ... INTERCEPTS_STYLUS) touchCropId=97 ...}
Layer [101] [Gesture Monitor] swipe-up#101
invisible reason=nothing to draw
bounds={-10800,-24000,24000,10800}
6.4.1 层段现场
invisible reason=nothing to draw 这种带原因的不可见、bounds 的几何、以及 input{...} 的触摸属性@ 本机实测(Pixel_7 AVD,Android 16)这里最值得记的一点是 invisible 后面那个原因字符串。reason=nothing to draw 不是"没数据",而是系统告诉你这一层为什么不可见。这也是本篇的一条通用经验:在这套读数里,"不可见"与"不可见的原因"是分开写的——判断一层为什么没出现,先读原因,别先猜。
把这两条读法串起来,现场排查的顺序就出来了:先看 bounds(这一层自己的几何是不是你要的)→ 再看 toDisplayTransform(父层有没有在它身上叠加变换)→ 如果两者不一致,就往上找父层,而不是继续在这一层的属性里找原因。这一步的价值在于它把"我改的东西没生效"和"我改的东西生效了但被父层盖住了"两种情况分开——前者是事务没落地,后者是层树位置不对,两者的排查方向完全不同。
口径警示 ④:
--list的#N与层段的[N]是两套编号(StatusBar#87vsLayer [104] StatusBar#104),跨命令比对认层名不认编号;另外Requested / Calculated这两个词是 Winscope 图层视图的用语,本机dumpsys SurfaceFlinger文本里搜不到(计数 0/0)——不要把它当 dumpsys 的字段名。
七、一次真实的 reparent
OK,第六章是静态的层树。这一章做一个前后对照,看一笔事务在层树上留下的痕迹——这也是本篇最硬的一处证据。
7.1 实验设计:两次快照的 diff
思路很简单:同一台设备、同一条命令、只改一个变量——屏幕内容有没有变化。具体做法是:
① 唤醒屏幕,抓一份层清单与全量 dump (对照前)
② 执行 cmd statusbar expand-notifications (只做这一件事)
③ 等 2 秒,再抓一份层清单与全量 dump (对照后)
④ 把两份清单逐行 diff
不用 Winscope、不用 root、不装任何东西——只用 adb shell 就能做的两条命令。这也是 §5.3 说的那个替代方案。
7.2 结果:94 → 95 层,两个 parentId 被改
diff 的结果如下。要注意:下面列出的只是真正变化的那几行——两份清单里其余 90 多条记录逐字相同,包括那些层名、z 值、以及各种属性字段。变量只有一个(下拉了通知栏),所以差异也只集中在少数几行上,这正是做这种对照实验的意义:把噪音压到最小,剩下的变化就是你要看的东西。
下拉前:94 层 下拉后:95 层
新增(前 3 条):
RequestedLayerState{315b870 StatusBar#87 parentId=115}
RequestedLayerState{96c9d8b Taskbar#80 parentId=114}
RequestedLayerState{NotificationShade#112 parentId=85}
消失(前 3 条):
RequestedLayerState{315b870 StatusBar#87 parentId=110}
RequestedLayerState{96c9d8b Taskbar#80 parentId=109}
7.2.1 一次 reparent 的前后对照
StatusBar#87 仍在清单里,但它的 parentId 从 110 变成 115;Taskbar#80 从 109 变成 114@ 本机实测(Pixel_7 AVD,Android 16)怎么读这张图:注意"消失"与"新增"那两行层名是同一个(都是 StatusBar#87),变的只有 parentId。可见这一层并没有被销毁重建,只是换了个父亲。这正是 reparent 的字面意思。
7.3 那个新父亲是谁:动画牵引层
那 #115 是什么?它在清单里的名字写得很清楚:
RequestedLayerState{Surface(name=315b870 StatusBar)/@0x7442588
- animation-leash of insets_animation#115 parentId=86}
7.3.1 动画牵引层的出现
animation-leash of insets_animation——系统为了做插入动画新建了一层"牵引层",把状态栏与任务栏重挂到它下面@ 本机实测(Pixel_7 AVD,Android 16)"leash"(牵引绳)这个词很形象:它不是一个有内容的层,而是一个"把手"——动这个把手,挂在它下面的层就一起动。 这就是 §0.1 那个现象的全部机制:状态栏与任务栏不是"同时收到了一个动画指令",而是"被挂到了同一个父亲下面"。
配套的可见性变化也印证了这一点:visible reason= 的条数从 20 变成 22——多出的两条正是被拉起来参与动画的那部分。
7.4 这条证据能证什么、不能证什么
为什么这个实验能做,而 Winscope 那条路不能做? 原理上讲,两次快照的 diff 只需要读权限(dumpsys 本身就是只读命令),而逐事务抓取要读的是 SF 内部的事务环形缓冲——那需要 root。同一件事实可以用两种不同权限等级的通道去观测,这一条在排查现场问题时很有用:先用手上有的权限拿到能拿的,再判断缺的那块有多要紧。抓取侧的口径见 Winscope · 用 adb 抓取。
能证:一次系统行为在层树上留下的痕迹是可读、可对照的;改的是层树关系(parentId),不是像素;而且层本身没有被重建(层名与 ID 都不变)。
不能证:看不到"一笔事务内部的顺序"——因为两次快照之间可能发生过多笔事务,diff 只能告诉你"终态变了什么",不能告诉你"它们是怎么攒的、合并了几笔"。这一层要靠 Winscope 的 android.surfaceflinger.transactions 数据源,而它需要 root(§5.3)。所以,同样是"看事务",两种手段看到的是两个不同粒度的东西:一个是两次快照之间的净变化,另一个是每一笔各自的来龙去脉。先想清楚自己要回答哪一个问题,再决定去拿哪种证据——这是本篇最想留下的一条方法论。
7.4.1 两种证据的分辨率对照
下次遇到"我改的属性没生效"这类问题,建议按三步走:① 先确认改动有没有交出去——如果手上能拿到应用侧回调或 fence,先看 OnCommit 有没有来;② 再确认改动落在哪一层——连抓两次 --list,看目标层的 parentId / z 有没有变化,层名有没有变;③ 最后确认有没有被父层盖住——读该层的 toDisplayTransform,与 bounds 对一下。这三步的顺序不能反:先证"交了",再证"落在预期的那一层",最后才去怀疑层树里的传递——反过来做,很容易在层树里找半天,而真正的原因其实是第一笔事务压根没交出去。
口径警示 ⑤:"两次快照的 diff" 与 "逐事务抓取" 是两种不同分辨率的证据。 前者能回答"改了什么",后者才能回答"怎么改的"。别拿前者去论证后者的结论——本篇所有结论都限定在"改了什么"这一层。
八、四处流传很广的说法,逐条核
收集资料时遇到不少流传很广的说法,本篇把它们逐条拿到官方文字或本机读数上核一遍。口径:每条都给"流传说法 → 出错处 → 实证/官方依据 → 本篇结论"。
8.1 四处说法一览
先把四处说法汇总成一张对照表:每条都给"原文、它错在哪一步、拿什么去核、本篇最终怎么落笔"四列。这样读者不用翻正文也能一眼看到四处结论各自站哪、依据是什么;逐条独立核验,不把几条说法含糊捏成一段。
8.2 说法的分辨
说法一:「apply() 之后改动物立即生效」。 出错在把"提交"当成了"生效"。实证/依据:官方在 OnComplete 的说明里明写 "Since the transactions are applied asynchronously",并在 OnCommit 里区分出"已准备好呈现"这一步。本篇结论:apply() 只是把改动交出去;真正影响屏幕要看 SF 什么时候处理到它、fence 什么时候 signal(§3.4)。
说法二:「事务是同步的,交完就完成」。 出错在把"调用返回"当成"事情做完"。依据:官方为事务提供了 OnCommit / OnComplete 两个回调,"需要回调"这件事本身就说明它是异步的。本篇结论:事务是异步应用的,要判断是否真的上了屏,得看回调或 present fence(§5.1)。
说法三:「改个属性会让界面重建一趟」。 出错在把"改层属性"与"重建视图"混为一谈。实证:本机那次下拉里,StatusBar#87 层名与 ID 都没变,只有 parentId 变了(§7.2)。本篇结论:事务改的是层的属性,不要求应用重建界面——这正是"透明"两个字的来源。
说法四:「Winscope 抓事务是常规操作,随时能抓」。 出错在忽略前提条件。依据:官方抓取文档(Winscope · 用 adb 抓取)明写"在使用 adb 收集 Winscope 轨迹之前,请运行 adb root"。实证:本机 user 构建、ro.debuggable = 0,adb root 不可用,这条路径在本机走不通(§5.3)。本篇结论:抓事务流有门槛,不是随手可做;没有它就得换一组证据。
这四条自查,落成命令都很短。比如要验说法三(改属性是否重建了界面),只需要两次快照:
adb shell dumpsys SurfaceFlinger --list > before.txt
adb shell cmd statusbar expand-notifications
adb shell dumpsys SurfaceFlinger --list > after.txt
diff before.txt after.txt # 看层名是否还在、变的是哪个字段
这几行里有两处要留意:把第二行换成任何一次你想观察的操作都可以;关键在最后一行——如果同一个层名在两份文件里都在,只有 parentId 或 z 变了,就说明这次操作改的是层属性而不是重建了层(§7.2 就是这么读出来的)。
把四处说法各配一句"正确写法",方便直接对照。"apply 之后立即生效"这条,正确写法是"apply 只是交出改动,生效点在 SF 处理它那一轮";"事务是同步的"这条,正确写法是"它是异步应用的,完成状态要靠回调或 fence 判";"改属性会重建界面"这条,正确写法是"改的是层的属性,层本身不必重建,因此是透明的";"Winscope 随时能抓"这条,正确写法是"它需要 root,user 构建上抓不到"。
原理上讲,这四处错的是同一件事:把"提交"当成了"完成"。提交是应用侧的一个动作,完成是系统侧的一串事件(应用 → 排队 → latch → present),中间隔着好几步。看到"调用了某个 API"就推定"结果已经产生",是这四处共同的思想根源。
8.2.1 每条说法"错在哪一步"的证据链
8.3 两次"搜不到",性质完全不同
本篇在准备外部素材时做了 6 组检索,其中两次"搜不到"值得单独说,因为它们的性质完全不同:
- 第一次:搜
atomicity transaction database,命中 20 条——回来的是原子能委员会、原子钟、原子咖啡壶。这是撞词噪声:素材是有的(数据库原子性示意图确实存在),只是关键词撞上了别的东西; - 第二次:搜
window manager architecture,命中 20 条——回来的是建筑图纸("window" 撞上了"窗户")。同样是撞词噪声; - 而真正的稀缺长这样:搜
surfaceflinger android,命中 1 条,且是泛化的体系图——Commons 上确实没有 Android 事务机制的专属图。
把这两者混为一谈,会往两个相反的方向出错:把撞词噪声当成"素材很多",就会拿无关图充数;把真稀缺当成"再搜搜就有",就会浪费大量检索轮次。判据很简单:看命中条目的相关性,而不是看命中条数。
8.4 商标与素材边界
属于素材纪律:Commons 上 Android 相关标识全是注册商标,所以本篇不使用任何 logo 文件。logo 对技术论证没有贡献,反而挤掉一张机制图,还有商标风险。
同理,本篇三张外部图都是通用体系与概念图(显示服务器版图、窗口管理器、数据库原子性),不是 Android 的层与事务;尤其那张数据库原子性图,只承担"原子"一词的直觉(§3.3 已声明边界)。
九、四项可执行动作
好了,把这一章的内容收成四项动作,每条都给命令与判据。
9.1 四项动作与判据
动作一:清点层树。
dumpsys SurfaceFlinger --list
判据:能逐条看到 RequestedLayerState{...};统计其中带 z= 与带 relativeParentId= 的条数,两者比例能告诉你这台设备上"绝对 z 序"与"相对父级"哪种是主流。
动作二:读某一层的当前属性。
dumpsys SurfaceFlinger | grep -A6 "<层名>"
判据:能读到 visible / invisible reason=、buffer=、frame=、bounds=、toDisplayTransform=;invisible 后面应带原因字符串,没有原因的"不可见"要警惕。
动作三:用两次快照的 diff 看"改了什么"。
dumpsys SurfaceFlinger --list > before.txt
(做一次你想观察的操作)
dumpsys SurfaceFlinger --list > after.txt
判据:对比两份清单里同一层名的 parentId / z / 字段变化;层名不变而 parentId 变,就是一次 reparent。
动作四:判断"是不是快照分辨率不够"。(逐事务那一侧的数据源与可查询表见 Perfetto · SQL tables。)
判据:如果 diff 只能看到"终态变了"——原理上讲,这已经说明你手上的证据分辨率不够,因为 diff 天生只记录两个时刻之间的净差,不记录中间过程。,而你要回答的是"一笔事务怎么攒、合并了几笔",那就需要逐事务抓取——先确认能不能 adb root;拿不到 root 就如实说明这一层结论测不到,不要用 diff 的结论去替代。
9.1.1 四项可执行动作与判据面板
9.2 本篇的结论
好了,把十章收成三句。
第一句:事务是一组"原子"的层属性改动,原子指的是同一生效点,不是"立即生效"。 官方那句 "an atomic set of changes" 里,"原子"保证的是"不出现中间态、同一生效点落地";而 apply() 是异步的,返回不等于屏幕已变。
第二句:改动改的是层树里的"关系",不是像素。 位置相对父层算、Z 序相对兄弟比、可见性作用于子树、reparent 连带继承——本机那次下拉里,StatusBar#87 的层名与 ID 都没变,变的只有 parentId(110 → 115)。
第四句:改动的语义边界要背下来,它决定了哪些推断是合法的。 事务保证的是「一组改动同时落地、中间态不可见」,不保证立刻生效、不保证跨层先后、不保证可回滚。凡是超出这三条边界的推断——比如「apply 一定意味着屏幕已经变了」「先改父层再改子层一定会被按序观察到」——都站不住。把边界记牢,比记住 API 名字有用得多。
第三句:证据是有分辨率的,要按问题选。 "改了什么"用两次快照的 diff 就能答;"怎么改的"必须靠逐事务抓取,而它需要 root。本机拿不到 root,所以这一层结论只答到"改了什么",剩下的如实标"需 userdebug/真机"。
还有一条它没保证的,值得补上:跨层之间的先后顺序。一笔事务里对多个层的改动会在同一个生效点一起落地,但落地之后,不同层的属性之间没有规定的先后——不存在 A 层的改动一定先于 B 层被读到这类保证。所以如果你的逻辑依赖先改父层、再改子层这样的次序,不要指望在同一个生效点里能观察到它;需要次序的地方,要么拆成两笔事务,要么靠应用侧自己同步。这条边界官方没明写,但它是「一组改动同时落地」这句话的逻辑推论。
未解的挑战也留一句:一笔事务内部到底合并了几笔、顺序如何,在本机只能答到"终态",真正要答清需要一台能 adb root 的 userdebug 设备(或真机)。这部分留给后续自跑篇。
9.3 下一篇预告
OK,下一篇回到合成的下一层,讲 HWC:既然 SF 把清单交给 HWC、由它逐层标注 device 或 client,那 HWC 这一侧到底是什么、它凭什么能"叠加"、真机上它和 GPU 合成的分工又是怎么定的——也就是 ep06 与本篇反复提到、但一直没打开的那个硬件抽象层。
相关链接 - 上一篇:【Android 图形显示系统】(六) SurfaceFlinger:在 Vsync 之后做什么 - 下一篇(预告):【Android 图形显示系统】(八) HWC:GPU 合成之外的硬件叠加 - 相关系列:实时渲染管线探析 | 移动端图形与功耗 | GPU 架构剖析 | 图形性能剖析工具链 | 2D 矢量引擎深挖
With Best Wishes.
参考资料
- SurfaceFlinger 与 WindowManager(含 ASurfaceControl 是 Android 10 新增的原文)
- SurfaceControl.Transaction(原子性 / merge / 各 setter 官方口径)
- SurfaceControl(类定位)
- NDK · native-activity 参考(ASurfaceControl / ASurfaceTransaction / Stats)
- NDK · native-activity 示例
- Winscope · 分析 SurfaceFlinger(z 规则 / relativeParentId / Requested 与 Calculated)
- Winscope · 用 adb 抓取(数据源名与「先 adb root」的要求)
- Winscope · overview(Transactions 抓取类型)
- 追踪窗口转换
- 实现 HWC(layer 生命周期与事务对接边界)
- Perfetto · SQL tables(surfaceflinger_transactions 表)
- Perfetto · FrameTimeline
- Android · dumpsys 工具
- SurfaceControl.TransactionCommittedListener(API 33)
frameworks/native/libs/gui/SurfaceControl.cppframeworks/native/libs/gui/SurfaceComposerClient.cppframeworks/native/services/surfaceflinger/BufferStateLayer.cppframeworks/native/services/surfaceflinger/SurfaceFlinger.cpp- DeepWiki · Client-side graphics APIs(apply → setTransactionState)
- DeepWiki · Layer system(RequestedLayerState 字段表)
- DeepWiki · BLASTBufferQueue(把缓冲交付转写成事务)
- juejin · SurfaceComposerClient 事务提交(位标志累加)
- CSDN · BufferStateLayer::setBuffer
- Go 绑定 · NDK surfacecontrol(函数全集交叉核对)
- Wikimedia Commons · Free and open-source-software display servers and UI toolkits
- Wikimedia Commons · Database transaction atomicity simple
- Wikimedia Commons · Window manager diagram mul