十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Android 14 ShellTransitions双事务与SurfaceControl显示时机解析

Android 14 ShellTransitions双事务与SurfaceControl显示时机解析 说实话Android 14这套ShellTransitions我第一次深入看的时候完全是被一块内容卡住的一起看起来不该发生的闪白动画没有按预期衔接所有窗口状态都对可SurfaceControl就是在不该出现的时机出现了。那次排查到最后的根因是开发者没有搞懂Transition“就绪”这件事的真正含义也不清楚ShellTransitions里那两条Transaction——一条给起始状态、一条给结束状态——到底该在哪个时间点apply。这个问题做系统开发的、做窗口动效的、甚至做桌面Launcher定制的十有八九都会踩到。所以这篇我打算把Android 14 ShellTransitions里Transition就绪机制和双Transaction配合SurfaceControl显示时机的来龙去脉完整讲一遍。这不仅仅是源码注释层面的搬运而是结合我自己实际调试经验总结出来的理解希望对做Framework开发的人有实质帮助。1. 先搞清楚背景为什么Android 14要引入ShellTransitionsAndroid 14这套ShellTransitions本质上是一次窗口动画架构的大挪移把原本分布在不同模块、由不同系统服务协作才能完成的动画管理逻辑统一收口到SystemUI侧的Shell进程里。在它之前的平台版本上窗口过渡动画的编排分散在多个地方。WindowManagerService自己管理一部分窗口状态变化SystemUI侧又通过WindowManagerShellCommand或者直接操作WMS内的WindowState来做动画动画过程中的各种回调、同步、生命周期管理都需要在WMS和SystemUI之间来回穿梭。这套老方案最大的问题是每一处窗口动画的编排逻辑都是特例。Activity启动是一种路径Dialog弹出是一种路径通知面板展开又是另一种路径每个路径都各自维护一套独立的Surface控制序列。平时各跑各的但只要有两个动画同时发生或者一个动画被另一个动画打断各种状态不同步、Surface层级错乱的问题就会冒出来。ShellTransitions要解决的就是这个积弊。它把所有窗口过渡动画抽象成统一的“Transition”模型明确了每个Transition从创建到结束的状态机规范了SurfaceControl的归属和操作方式。窗口动画不再散落在各个模块里各自为政而是统一汇入Shell侧的一套管线里由Shell统一管理动画的播放、合并、取消。这个架构对系统级开发者的影响是巨大的。如果你在Android 12、13上做窗口动画定制拿到的常常是WMS内部回调、WindowState的直接引用操作Surface时还有不少隐式的层级关联要靠经验去猜。到了Android 14这些操作全部转换成围绕Transition和SurfaceControl的显式控制你拿到的是TransitionInfo、Change对象、以及两条由Shell创建好的Transaction句柄控制关系清晰了很多但前提是你必须先理解这套模型。2. 核心机制拆解Transition的Ready到底由谁决定2.1 状态机的一步步流转为什么先讲Ready因为这是整个ShellTransitions最容易让新手懵掉的一环。很多人拿到TransitionInfo就开始创建Transaction往里丢操作结果发现动画总是慢半拍、窗口画面先闪出来再跳回初始状态原因就是没搞清楚Transition还没就绪。在WMS侧每一个Transition实例都维护着一个明确的状态机。常见的几个状态包括PENDING等待收集参与者、STARTING准备开始、READY所有参与者就绪可以执行动画了、FINISHED动画结束、ABORT取消等。关键点在于READY这个状态。一个Transition创建之后会进入收集窗口的阶段系统会通知每一个受影响的窗口“该你了告诉我们你想以什么状态参与这次动画”。所谓“受影响”涵盖的范围不止是你肉眼看到的那个Activity还包括status bar、IME输入法、wallpaper、甚至是一些不可见的窗口容器。每个窗口都要给出自己的参与意见保持原样、跟随动画移动、还是根本不参与。只有当所有参与者都反馈完毕WMS侧的TransitionController才会把状态推进到READY并将构建好的TransitionInfo交给Shell去播放动画。这就引出一个容易被忽略的事实Transition的Ready不是某个动画“准备好了”而是所有参与者都已经完成了状态确定。如果过程中有一个窗口迟迟不反馈整个Transition就会卡在PENDING/STARTING动画的启动时间也会被无限拖后。不是所有参与者都一定能及时反馈所以Android 14在WMS侧提供了超时保护机制。当等待超过一定时间Transition会强制执行过渡。这个超时保护保证了系统不会因为一个坏掉的窗口就一直卡死但代价是超时强行推进时动画可能没有建立起正确的初始状态界面会出现不符合预期的闪烁。2.2 “双Transaction”的由来与职责当Transition进入READY并交给Shell侧时频道就切换到ShellTransitions上了。Shell侧收到这个“播放指令”后会创建一个TransitionAnimator相关实例在这个实例里会看到两条成双成对的SurfaceControl.Transaction。代码里或者调试日志中它们往往被称为startTransaction和finishTransaction。有些地方也叫startT和finishT。名字不同职责却是清晰的startTransaction负责复现Transition启动那一刻的屏幕状态。它把每个参与动画的窗口SurfaceControl通常是一个个Leash即由WMS为每个窗口创建的控制节点设置成动画起始时的位置、大小、透明度、可见性。finishTransaction负责描述Transition动画结束后的目标状态。动画播放完毕的一瞬间屏幕应该落到这套设置上然后动画Leash这个临时容器被拆掉各个窗口的SurfaceControl回到它们真实的状态。为什么要搞两条而不是一条因为对于动画本身来说起始条件和结束条件往往是完全相反的操作。一个窗口要进入屏幕起始是invisible结束是visible要退出屏幕的话则反过来。如果只靠一条Transaction用“增量修改”的思路去管理每次动画开始前都必须小心记录当前状态、再手动补齐差值非常容易漏操作。而双Transaction的方案是“全量快照”式思路起始快照放一条结束快照放一条动画系统只需关心两帧之间怎么插值即可。另一个现实原因是Shell侧的动画可能需要和人手手势的进度同步比如手势返回、手势关闭App。手势进行中屏幕状态会实时变化而动画真正播放前起始Snapshot必须和当前用户看到的画面完全一致否则动画一开始就跳变。双Transaction保证了WMS捕捉到的起始Snapshot可以随时作为“还原现场”的命令被快速apply。3. SurfaceControl显示时机双事务到底该在哪里apply3.1 核心规则可见性不能乱设理解完双Transaction的角色接下来是最容易出问题的地方SurfaceControl的显示时机。先说结论性经验Leash的setVisibility(true)操作必须和动画的起始帧严格绑定不能早也不能晚不然就会闪白或者闪黑。我在调试时看到过不少次这样的场景开发者在创建TransitionAnimator之后急忙忙地手动拿到一个Leash调用setVisibility(true)然后apply。表面看好像没问题窗口确实显示了但紧接着Transition开始播放它的startTransaction会把这个Leash重新设置成invisible。于是一个看起来很奇怪的动画就发生了窗口先显示一帧再消失再从动画初始状态播放出来。这就是典型的“显示时机早于Transition Ready”的经典现象。为什么会这样因为Transition的Leash在WMS准备好之前它的可见性由WMS的显示策略控制。Transition准备就绪后控制权转交到Shell侧Shell通过startTransaction统一接管所有参与窗口的可见性。如果你在Shell接管之前就私设了可见性相当于你和WMS同时在一个Leash上写参数期间会产生竞态谁先apply、谁后apply完全取决于执行到哪一行结果不可预测。正确做法是把窗口可见性设置放进startTransaction里并确保它和TransitionAnimator的启动逻辑处于同一帧时序。简单说transition的动画播放器在启动动画时会把起始Snapshot的apply同步到当前帧的VSYNC边界让用户在这一帧看到的就是动画的初始画面。SurfaceControl.Transaction的apply默认是异步的也就是调用后不会立即生效而是要等到系统渲染管线下一个处理点。对于普通操作来说这没问题但窗口显示这种关键时机异步意味着时序不可控。如果你确实需要在某个明确帧点让Leash立即可见可以使用同步apply。3.2 显示时的Leash结构还有一个需要深入理解的层级问题。在ShellTransitions中动画不是直接操作每个窗口的Surface而是操作每个窗口SurfaceControl之上的“Leash”。一个Transition还会有一个Transition Root Leash一个用于承载所有参与窗口的临时根节点。Transition真正开始播放动画的那一瞬间所有参与Transition的独立Leash都会被reparent到Root Leash之下。这样做的好处是动画播放期间所有窗口的拓扑关系被固定在一个临时树上动画只需控制Root Leash的位置、比例、透明度就能带动整棵子树一起动。这比逐窗口控制高效得多。但是这个reparent动作也让SurfaceControl的显示时机变得微妙。子窗口Leash的setVisibility要设置为true前提是它已经被正确挂载到Root Leash下。如果你在reparent还没发生时就设了可见性这个设置可能因为父节点层级不完整而丢失或者导致Surface在错误的位置显示一帧。所以一个很实用的经验是尽量通过Root Leash做整体的可见性控制而不是单独控制每个参与窗口的Leash。尤其在动画的起始和结束阶段Root Leash的visible为false时整个动画子树都是不可见的Root Leash visible为true时子Leash的真实可见状态才由各自的属性决定。用这个思路来组织动画时序会简单很多。在ShellTransitions源码里TransitionAnimator的启动流程其实就是按这个逻辑设计好的。它会先从TransitionInfo中取出所有Change读取每个Change的起始SurfaceControl状态也就是start状态然后构建startTransaction和finishTransaction其中Root Leash的可见性设置被安排在了正确的位置。我们做定制时只要不绕过这套流程就不会踩上面的坑。4. 实操手写一个配合Transition的窗口动画4.1 核心代码骨架理论讲了这么多直接上代码。这里给一个最简化的自定义Transition动画启动流程展示startTransaction和finishTransaction的正确打开方式。我以ShellTransitions中的TransitionAnimator实现思路为例去掉一些无关的边缘处理保留主干// 伪代码框架来源是对AOSP Android 14 ShellTransitions实现的理解 class SimpleTransitionAnimator implements TransitionAnimator { Override public void onTransitionAnimationStart(TransitionInfo info, SurfaceControl.Transaction startTransaction, SurfaceControl.Transaction finishTransaction) { SurfaceControl rootLeash info.getRootLeash(); // 1. 先把所有子Leash按起始状态设置好 ListTransitionInfo.Change changes info.getChanges(); for (TransitionInfo.Change change : changes) { SurfaceControl leash change.getLeash(); // 注意这里不直接apply只是往startTransaction里塞操作 startTransaction .setVisibility(leash, change.getStart().isVisible()) .setPosition(leash, change.getStart().getPositionX(), change.getStart().getPositionY()); } // 2. 在startTransaction里 设置子Leash依赖Root Leash // 这里假设动画处理线程已经拿到正确的Surface层级 startTransaction.reparent(rootLeash, null); // 防止重复挂载 // 3. 同步apply起始快照 startTransaction.apply(); } Override public void onTransitionAnimationEnd(TransitionInfo info, SurfaceControl.Transaction finishTransaction) { SurfaceControl rootLeash info.getRootLeash(); ListTransitionInfo.Change changes info.getChanges(); for (TransitionInfo.Change change : changes) { SurfaceControl leash change.getLeash(); finishTransaction .setVisibility(leash, change.getEnd().isVisible()) .setPosition(leash, change.getEnd().getPositionX(), change.getEnd().getPositionY()); } // 动画结束时将Root Leash的状态恢复给真实窗口层级 finishTransaction.apply(); } }这段代码看起来简单但有三处细节是决定成败的关键。第一startTransaction里的setVisibility用的是change.getStart().isVisible()也就是WMS在Transition真正READY时捕获的起始可见性而不是你“以为”的起始可见性。这个区别很重要。如果你写死成true滑动返回手势的场景下就会出问题因为这时窗口其实已经处于半透明滑动状态。第二所有set操作只是写入了Transaction真正生效靠apply。在start中apply和finish中apply之间我们不应该穿插任何其它对同类SurfaceControl的setVisibility操作。第三finishTransaction的apply意味着Transition动画的结束。它把各个Leash的状态设置成最终值后Shell会随后释放Root Leash和各个子Leash的控制权真实的窗口Surface恢复自主管理。4.2 利用动画进度回调刷新多个帧上面演示的是简化骨架但实际项目中动画通常持续几百毫秒中间每一帧都需要根据插值器来更新Surface位置。这里就牵扯到另一个容易出错的点动画进行中到底用哪条Transaction。ShellTransitions中动画进行中的每一帧操作都是基于一个新的Transaction而不是继续往startTransaction里塞东西。startTransaction在起始帧apply后基本就完成使命了中间帧的操作需要用独立的Transaction持续apply。finishTransaction负责最后一帧。ShellTransitions内部有一整套WithinSurfaceControl.Transaction的复用和同步机制确保动画帧的Transaction能排队到正确的渲染时序。对定制开发者来说我们可以不强求完全复用内部那套但必须明白startT和finishT是有明确生命周期边界的startT在动画启动帧applyfinishT在动画结束帧apply中间每帧的状态更新用你自己的Transaction。为了达到流畅的60fps效果动画帧的Transaction通常用Transaction.setFrameTimelineVsync或者配合Choreographer的帧回调做对齐避免跳帧。若随意apply可能造成动画与屏幕刷新不同步的“颤抖”感。private void applyAnimationFrame(SurfaceControl.Transaction tx, SurfaceControl leash, float progress) { float x startX (endX - startX) * progress; float y startY (endY - startY) * progress; tx.setPosition(leash, x, y); tx.setFrameTimelineVsync(Choreographer.getSfInstance().getVsyncId()); tx.apply(); }这段是动画中间帧的操作示例。特别注意这里不能把leash的visibility再重新设置一次动画面板中间设置visible不会有明显效果反而会引起时序追踪困难。只需要设置位置、缩放这类几何属性即可。4.3 验证动画启动帧的同步性代码写完后怎么验证你的时序是对的我一般用两种手段。第一种打开开发者选项里的“显示Surface更新”和“显示屏幕更新”。如果动画启动时看到窗口闪烁两下或者在一个非常短的帧内显示了两遍相同内容那多半是起始快照没有与动画帧同步。第二种打开日志追踪。在startTransaction.apply()前后打两个带时间戳的log同时记录SurfaceFlinger的提交时间观察间隔。正常情况下两者的间隔应当控制在一帧以内。如果间隔超过两个VSYNC说明你的apply时机被延迟了屏幕上可能出现肉眼可见的空窗期。注意这里的日志不是常规的Log.d需要看系统侧的EventLog比如wm_shell_transitions相关的event tag可以直接看到Transition的启动帧。在Android 14上通过adb shell dumpsys activity transitions也能看到最近Transition的状态和耗时信息这是个非常实用的排查工具。5. 常见问题与排查技巧实录5.1 问题速查表这部分我直接给一个速查表全部来自我实际调试中积累的问题和结论。你可能遇到的现象多半能在这里找到对应。现象核心原因解决思路动画一开始窗口闪现逻辑状态再回到起点setVisibility在Transition未READY时被手动apply把可见性交给startTransaction统一设置动画结束的一瞬间窗口位置跳变、闪烁finishTransaction没有设置正确的end状态检查Change的end状态是否抓取正确确认finish.apply在动画最后一帧动画整个不显示只有黑屏或延迟显示Transition长期未进入READY等待参与者超时排查是否有窗口一直未反馈参与状态通过dumpsys activity transitions看状态卡在哪动画结束后窗口一直不消失卡在屏幕上残留finishTransaction未正确处理invisible窗口检查end状态中isVisible为false的Leash是否在finish事务中被置为不可见手势拖动时窗口位置有延迟、跟不上手指每帧Transaction未做VSYNC对齐使用setFrameTimelineVsync并配合Choreographer回调Transition过程中另一个Transition被发起动画衔接错乱没有正确处理Transition的merge或cancel在Shell侧监听合并请求必要时先替换finishTransaction再统一收尾5.2 一个典型的超时排查现场有一类问题特别值得展开讲就是Transition一直卡在未READY的状态。我处理过的一个案例是应用A启动应用BB界面迟迟不显示过了大概几百毫秒才突然蹦出来中间黑屏时间长得像是系统卡死。第一反应是应用启动慢但通过抓取dumpsys activity transitions发现这次Transition一直停留在WAITING状态超时后才被强制拉起来。进一步排查发现B应用在启动过程中有一个联动窗口参与到了这次Transition里是一个不可见的辅助窗口。这个窗口的应用侧没有及时回调WMS报告自己的状态导致整个Transition等待它的反馈超时。强制推进后动画虽然播放了但初始Snapshot已经和用户真实看到的画面存在差异所以出现了跳动感。这个案例想说明的是一个看似毫不相关的窗口都可能拖垮整个动画。所以做系统级定制时对于自己创建的窗口要在窗口添加后尽快确认它是否能正确响应Transition参与。如果能做到这个窗口最好在初始化时就注册成非Transition参与者或配置合适的排除规则否则就是动画卡顿的隐性炸弹。5.3 Transaction泄漏问题SurfaceControl.Transaction本身也是资源如果不手动释放每次动画都会消耗内存。在ShellTransitions中startTransaction和finishTransaction是框架传给Animator的不是由我们创建的。但动画中间帧你自己创建的Transaction用完必须release。SurfaceControl.Transaction tx new SurfaceControl.Transaction(); try { tx.setPosition(leash, x, y); tx.apply(); } finally { tx.release(); }很多做过自绘动画的开发者只记得调用apply忘了release结果就是一部手机上累积成百上千个未释放Transaction。虽然Transaction对象不是直接对应一块大显存但它内部持有了与SurfaceFlinger通信的资源长时间不释放会造成client端句柄泄漏最终导致系统surface创建失败或动画越来越卡。5.4 排查工具组合拳最后把我平时处理ShellTransitions问题的工具链整理一下。adb shell dumpsys activity transitions查看当前所有Transition的状态、参与窗口、是否超时。adb shell dumpsys SurfaceFlinger --latency确认Surface提交是否掉帧。adb shell dumpsys window确认窗口的真实可见性和层级。systrace抓取wm.shell.Transition和SurfaceFlinger相关标签看两个进程间的transaction时序。systrace这里多说一句。ShellTransitions把动画编排搬到了Shell进程WMS和Shell之间的交互非常依赖Binder跨进程。以前Windows动画跨进程问题不多现在TransitionReady的传递、Transaction apply的反馈都是跨进程的。抓systrace时如果不带上wm.shell.Transition这些自定义标签你只能看到两个进程各自忙活看不到因果关系。带上后能直观看到WMS侧Ready事件和Shell侧启动动画事件的时间差这个差值通常就是问题所在。我个人排查时还有个习惯先不看代码先抓一次systrace和dumpsys把Transition全生命周期的时序拉出来。绝大多数问题在时序图上就能看出端倪代码审查反而容易漏掉时序上的先后依赖。弄清楚了时间关系再去代码里找逻辑错误效率会高很多。6. 最后再分享几个实操取向的经验代码细节聊完再说几个我实际开发中总结出来的、文档里可能不太会写的习惯算是给这篇文章做个收尾。第一个经验是关于“能不动就不动”的原则。ShellTransitions这套架构设计的初衷就是让窗口动画标准化。做定制时如果只是想改某个动画的时长、插值器或者缩放效果优先去调整动画配置而不是去改TransitionAnimator的实现逻辑。多写代码不一定是好事尤其是在系统框架层面每多一次手工Transaction操作就多一分时序风险。第二个经验是善用ShellTransitions的动画合并能力。多个Transition几乎同时发生时框架会尝试合并它们。比如Activity启动动画和StatusBar通知动画同时来系统会判断是否能把这些动画合成一个。我在定制时发现很多看似需要自己写“打断逻辑”的场景比如快速连续启动两个Activity其实ShellTransitions已经帮我们处理好了前提是我们不要破坏Transition的merge判定条件。具体来说尽量少去修改系统默认的Transition参与规则保持各窗口对是否参与Transition的响应逻辑清晰。第三个经验是关于测试。窗口动画这类东西在模拟器上跑得顺不代表真机上没问题因为真机的VSYNC节奏、SurfaceFlinger的负载都和模拟器差异巨大。测试ShellTransitions相关的改动一定要用真机而且要覆盖低端机。低端机上SurfaceFlinger处理速度慢更容易暴露出时序问题。我踩过最多次的坑就是模拟器上动画顺畅得不行一上低端机就闪屏、抖动、黑屏最后定位全是显示时机的问题。ShellTransitions是个值得深挖的架构改进它把透明性不强、组合性差的窗口动画变成了一套可以预期、可以标准化的流程。搞懂了Transition的Ready语义和双Transaction各自的生命周期SurfaceControl显示时机的问题就成功了一大半。剩下的就是多在真机上跑、多抓systrace、多看时序图用数据说话。
返回列表