20、Activity 转场动画:AMS如何协调WMS实现启动/退出动画
说实话,转场动画这块,我早年刚接触Framework时觉得挺简单的——不就是开个窗口、放个动画嘛。直到有一次我在优化一个第三方ROM的启动速度,发现动画卡顿的根因竟然是AMS和WMS的同步时序出了问题。嗯,从那以后,我对这套机制就再也不敢小瞧了。
今天我们就来聊聊,AMS到底是怎么拉着WMS一起,把Activity的启动和退出动画给“演”出来的。
一、动画的“导演”与“舞台”
先理清两个角色:
- AMS(ActivityManagerService):负责调度Activity的生命周期,决定什么时候该播动画、播什么动画。
- WMS(WindowManagerService):负责窗口的显示、层级、动画执行。说白了,动画的每一帧都是WMS在背后驱动的。
AMS是导演,WMS是舞台执行。导演喊“Action”,舞台才开始动。
核心原则:AMS只负责“触发”和“回调”,动画的渲染、插值、同步全交给WMS。两者通过Binder通信,配合一个叫 WindowContainerTransaction 的东西来传递动画参数。
二、启动动画的完整链路
我们以Activity A启动Activity B为例,走一遍流程。
1. AMS发起“启动请求”
当你在代码里调 startActivity(),最终会走到AMS的 startActivityLocked()。AMS会先判断目标Activity是否需要新建进程、是否需要冷启动。然后,它会调用:
// AMS 中关键调用
mWindowManager.startActivityTransition(displayId, sourceToken, targetToken, options);
这里的 options 就是你在 overridePendingTransition() 里传的动画资源ID。AMS把这些参数打包,扔给WMS。
2. WMS准备“舞台”
WMS收到请求后,会做两件事:
- 为B Activity创建新的
WindowState,并设置初始透明度为0(如果是淡入动画)。 - 记录A Activity的当前窗口快照,用于退场动画。
这里有个细节:WMS不会立刻播动画。它会等AMS把B Activity的窗口真正“附加”到窗口树之后,才触发动画。为什么?因为动画需要窗口已经存在,否则你播给谁看?
3. AMS通知“窗口可见”
当B Activity的 onResume() 执行完毕,AMS会调用:
// AMS 通知 WMS 窗口已就绪
mWindowManager.setWindowVisibility(token, true);
这一步是关键。WMS收到后,开始执行预先配置好的动画。
4. WMS执行动画
WMS内部有一个 AnimationHandler,它会根据动画类型(平移、缩放、淡入淡出)计算每一帧的变换矩阵。然后通过 SurfaceFlinger 合成到屏幕上。
我个人的习惯是,把动画分为两类:
- 系统动画:比如默认的从左向右滑入。这类动画由WMS内置的
AnimationSpec定义。 - 自定义动画:通过
overridePendingTransition()传入。WMS会解析成AnimationSet,然后逐帧执行。
避坑指南:我曾经遇到一个bug,自定义动画在低端机上掉帧严重。后来发现是动画的插值器用了 AccelerateDecelerateInterpolator,在CPU负载高时计算量太大。换成 LinearInterpolator 后流畅多了。所以,动画别太花哨,性能要紧。
三、退出动画的“反向”逻辑
退出动画和启动动画基本对称,但有一个关键区别:退出时,AMS会先通知WMS播动画,再销毁Activity。
流程如下:
- 用户按Back键,AMS收到
finish()请求。 - AMS调用
mWindowManager.startExitAnimation(token, options)。 - WMS开始播放A Activity的退场动画(比如向右滑出)。
- 动画结束后,WMS回调AMS:
onAnimationFinished()。 - AMS这才真正执行
destroyActivity()。
为什么要等动画结束再销毁?你想想看,如果动画还没播完就把窗口销毁了,用户会看到窗口突然消失,体验极差。所以AMS必须等WMS的“动画完成”回调。
四、AMS与WMS的同步机制
这里有个核心问题:AMS和WMS是跨进程的,怎么保证动画时序不乱?
答案是:通过 WindowContainerTransaction 和 Transaction 的序列化。
每次AMS发起动画请求,都会生成一个 WindowContainerTransaction 对象,里面包含了:
- 目标窗口的token
- 动画类型(启动/退出)
- 动画参数(时长、插值器、起始位置)
- 同步标记(是否等待动画完成)
WMS收到后,会把这个事务加入到一个队列中,按顺序执行。这样就不会出现“动画还没播完,窗口就被销毁”的乱序问题。
注意:如果AMS在动画执行期间又发了一个新的启动请求(比如用户疯狂点击),WMS会如何处理?答案是:WMS会取消当前动画,直接跳转到最终状态,然后执行新动画。这就是为什么快速点击时,动画会“闪一下”的原因。
五、一张图看懂整体流程
下面我用SVG画了一张流程图,把AMS和WMS的交互过程展示出来。你可以对照着看,会更清晰。
六、常见问题与优化建议
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 启动动画卡顿 | AMS在动画期间做了耗时操作(如ContentProvider初始化) | 将耗时操作延迟到动画结束后执行,或使用 reportFullyDrawn() |
| 退出动画闪白屏 | 窗口销毁时机早于动画结束 | 检查 onAnimationFinished 回调是否被正确调用 |
| 自定义动画不生效 | overridePendingTransition 调用时机不对 |
必须在 startActivity() 或 finish() 之后立即调用 |
个人经验:我在做系统级优化时,发现一个提升动画流畅度的技巧——减少AMS和WMS之间的Binder调用次数。比如,把多个动画参数合并到一个 WindowContainerTransaction 中,而不是分多次发送。这样能减少IPC开销,动画帧率能提升5-10%。
七、总结
Activity转场动画,说白了就是AMS当导演、WMS当舞台的一场戏。AMS负责喊开始和结束,WMS负责把每一帧演好。两者通过Binder和事务队列来同步,确保动画不跳帧、不闪白。
嗯,这里要注意的是:不要试图在动画执行期间干预窗口状态。我曾经犯过一个错,在 onPause() 里改了窗口的透明度,结果动画和手动修改冲突,导致界面闪烁。后来我学乖了,动画期间让WMS全权负责,AMS只做“旁观者”。
好了,这一章就到这里。希望下次你看到Activity滑入滑出时,能想起AMS和WMS在背后默默配合的样子。