SurfaceControl、leash 与 Transition 如何呈现窗口变化
SurfaceControl、leash 与 Transition 如何呈现窗口变化
问题场景
分屏拖动分隔条时,两个 Task 的逻辑边界要更新,屏幕上的图层也要连续移动;松手、进入或退出分屏时,还要让开始与结束画面在同一个动画生命周期中交接。它们不是同一件事:WindowContainerTransaction(WCT)描述窗口容器应怎样变化,SurfaceControl.Transaction 描述合成层应怎样变化,Shell Transition 负责收集、播放并结束一批变化。
本文以 AOSP Android 14 android-14.0.0_r75 为准,解释 WM Shell 如何把 Task 与 leash 对齐、SplitLayout 如何同时产出 WCT 和 Surface transaction、以及 Transitions / SplitScreenTransitions 如何管理 enter、resize、dismiss。不会展开 SurfaceFlinger 的合成算法、BLAST 内部实现或应用绘制管线。
一次分屏 resize 会同时产生布局结果、WCT 和 Surface transaction,Transition 则负责把变化 收集为一个有开始与结束条件的动画生命周期。三类数据不能互相替代。
核心结论
- Task 是 WindowManager 管理的逻辑窗口容器;
SurfaceControl是可在合成树中操作的图层句柄。 Task 出现时,TaskOrganizer.onTaskAppeared(taskInfo, leash)把两者配对交给 Shell。 - leash 是 Shell 获得的受控外层 Surface。 Shell 改它的位置、裁剪、层级或 alpha,而不必直接持有应用窗口的内部 Surface。
- WCT 与
SurfaceControl.Transaction分工不同。 前者提交 Task bounds、smallestScreenWidthDp等 WindowManager 状态;后者以setPosition()、setWindowCrop()等批量操作安排这一帧的图层状态。 - Transition 是一段生命周期,不是一次 transaction。
Transitions负责收集 ready 的变化并交给 handler 播放;SplitScreenTransitions为分屏的 enter、resize、dismiss 保存 token、finish transaction 和 finish callback。 - SurfaceFlinger 是 Surface transaction 的合成终点。 它不替代 Task/WindowManager 的逻辑决策,也不解释 WCT。
先观察边界:左边是 WM Shell(SystemUI 进程中的 Java 组件),右边是 system_server 的窗口管理;图中的“提交”不等于“动画完成”。
关键转折有两个:onTaskAppeared() 是从窗口管理到 Shell 的 Binder 回调;WCT 的应用方向则相反。SurfaceControl.Transaction.apply() 到达合成侧,但其完成并不自动说明关联 WCT 或 transition 已结束。
核心对象:先把“谁描述、谁执行、谁编排”分开
| 对象 | 所在层 | 负责什么 | 不负责什么 |
|---|---|---|---|
Task | WindowManager / system_server | 逻辑容器、windowing mode、Configuration、父子关系 | 直接被 Shell 任意移动的合成层 |
SurfaceControl | Surface 合成接口 | 表示一个可控制的 Surface 节点 | Task 的完整逻辑状态 |
| leash | Task organizer 回调给 Shell 的 SurfaceControl | 作为 Task 表面外层的控制边界 | 替代 Task token 或 WCT |
SurfaceControl.Transaction | Shell / 合成事务 | 原子描述 position、crop、alpha、layer 等图层操作 | 修改 Task Configuration |
| WCT | Shell → WindowManager | 描述 bounds、层级、窗口模式等容器变更 | 直接给 SurfaceFlinger 合成 |
| Transition | Shell transition 框架 | 收集变更、选择 handler、播放和 finish | 每一帧的唯一渲染 API |
这里可以借用“布景”和“舞台监督”的比喻:Task 像剧目单上的角色位置,leash 是可移动的布景框,Surface transaction 是一次写给舞台机械的操作清单,Transition 是监督整段换景的流程。比喻止于职责划分:真实系统中 Task 与 Surface 的创建、同步和可见性仍由 WindowManager、Shell 与合成侧的具体代码共同决定。
主流程:分屏 resize 为什么要走两条并行路径
阶段一:Task 出现时建立逻辑对象与可渲染对象的映射
- 执行者:WM Shell 中注册的
TaskOrganizer/ShellTaskOrganizer;回调从system_server经 Binder 到达,再由 Shell 的执行器或锁保护路径分派。 - 输入:
RunningTaskInfo taskInfo与SurfaceControl leash。 - 动作:Shell 以 task id 缓存信息并交给对应的
TaskListener;分屏 stage 因而同时知道 Task 的逻辑属性与可控制的表面。 - 下一跳:
StageCoordinator、stage listener 和SplitLayout使用 leash 安排表面。 - 边界:
TaskOrganizer回调跨进程;它不是 WCT 的逐笔完成通知。
阶段二:拖动过程中分别写 Surface transaction 和 WCT
- 执行者:WM Shell 的
StageCoordinator与SplitLayout,在 Shell executor 上串行处理布局。 - 输入:显示区域、divider position、两个 stage 的 leash 和当前
SurfaceControl.Transaction。 - 动作:
onLayoutSizeChanging()调用SplitLayout.applySurfaceChanges(),将 divider 与两个 leash 的position/crop写入 transaction,产生交互中的即时几何效果。 - 下一跳:手势稳定或结束时,
onLayoutSizeChanged()经updateWindowBounds()调用SplitLayout.applyTaskChanges(wct, mainTaskInfo, sideTaskInfo),把最终 Task bounds 和smallestScreenWidthDp写入 WCT,并与 Surface / transition 更新协调提交。 - 边界:两者都发生在 Shell 侧;WCT 后续经 Binder 交给 WindowManager,Surface transaction 后续提交给合成侧。
阶段三:以 Transition 把终态变化收束为可结束的操作
- 执行者:
SplitScreenTransitions与通用Transitions。 - 输入:transition token、WCT、可能已有的 resize transaction,以及 enter/dismiss 的目标 stage。
- 动作:
SplitScreenTransitions启动或接管分屏专用 transition;Transitions依次经历收集、ready、选择 handler、播放动画和 finish。handler 在 finish 时调用 callback,并提交它持有的 finish transaction。 - 下一跳:finish 后 Shell 清理临时状态;新的 Task / layout 回调继续建立稳定状态。
- 边界:Transition 的完成回调是动画生命周期边界,不能用一次
SurfaceControl.Transaction.apply()的返回来替代。
关键源码:从回调到布局,再到生命周期
源码节点:TaskOrganizer 明确交付 leash
- 类与方法:
TaskOrganizer.onTaskAppeared(RunningTaskInfo, SurfaceControl) - 路径:
frameworks/base/core/java/android/window/TaskOrganizer.java - 进程与线程:
system_server发起 Binder 回调 → organizer 所在进程的 Binder / executor 路径。 - 作用:声明 Task 出现时的两个关键输入:逻辑
taskInfo与渲染控制句柄 leash。
阅读问题:为什么 Shell 不能只保存 task id?
public void onTaskAppeared(@NonNull RunningTaskInfo taskInfo,
@NonNull SurfaceControl leash) {
// 默认实现为空;子类接收 Task 与可控制的 leash。
}结论:Task id 只能定位逻辑容器;布局动画还必须有 leash。该回调不承诺某个 WCT 或某段动画已经结束,只说明 organizer 现在可管理这个 Task 的表面。
源码节点:SplitLayout 同时处理 divider 与 stage leash
- 类与方法:
SplitLayout.applySurfaceChanges() - 路径:
frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java - 进程与线程:WM Shell 进程,
StageCoordinator的 Shell executor 路径。 - 作用:把当前 layout 计算结果写进传入的
SurfaceControl.Transaction;divider 的位置和两个 stage 的 leash 几何在同一 transaction 中安排。
阅读问题:拖动时,为什么画面会随 divider 走,而不必等待 Task 配置回调?
// SplitLayout.applySurfaceChanges() 中的关键职责,省略无关分支。
mDividerWindowManager.setSurfaceChanges(t, mDividerBounds);
applySurfaceChanges(t, leash1, mBounds1, mWinBounds1, ...);
applySurfaceChanges(t, leash2, mBounds2, mWinBounds2, ...);
// helper 对每个 leash 写入 setPosition(...) 与 setWindowCrop(...)结论:这是 Surface 路径:divider bounds 与每个 stage 的位置、裁剪被批量记录在 t。setPosition() 控制 leash 的放置,setWindowCrop() 限制显示区域;它们不等价于修改 Task 的 Configuration。
源码节点:最终 Task 几何要写进 WCT
- 类与方法:
SplitLayout.applyTaskChanges() - 路径:
frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java - 进程与线程:WM Shell 进程的布局处理路径。
- 作用:为两个
WindowContainerToken记录最终 bounds,并从 bounds 推导最小宽度配置。
阅读问题:只设置 leash crop 能否让应用得到正确的资源/Configuration?
// SplitLayout.applyTaskChanges() 的数据流,省略无关分支。
wct.setBounds(task1, mBounds1);
wct.setSmallestScreenWidthDp(task1, getSmallestWidthDp(mBounds1));
wct.setBounds(task2, mBounds2);
wct.setSmallestScreenWidthDp(task2, getSmallestWidthDp(mBounds2));结论:WCT 记录的是 WindowManager 要提交的逻辑终态。smallestScreenWidthDp 影响应用可见的配置和资源选择,因此单靠 Surface 裁剪只能“看起来”像 resize,不能取代这条路径。
源码节点:StageCoordinator 区分正在变化与已稳定
- 类与方法:
StageCoordinator.onLayoutSizeChanging()、onLayoutSizeChanged() - 路径:
frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java - 进程与线程:WM Shell 进程,Shell executor。
- 作用:前者服务连续拖动时的 Surface 更新,后者在布局稳定后触发 Task bounds 与 Surface bounds 的同步更新。
public void onLayoutSizeChanging(SplitLayout layout, int offsetX, int offsetY) {
// 省略无关代码
}
public void onLayoutSizeChanged(SplitLayout layout) {
// 省略无关代码
}这两个回调都不从调用方接收 SurfaceControl.Transaction。onLayoutSizeChanging() 接收的是 layout 与拖动偏移 offsetX / offsetY,在方法内部取得 transaction、调用布局的 Surface 更新逻辑并应用 transaction,让交互画面跟手。onLayoutSizeChanged() 只接收 layout,在内部创建或取得 WCT/transaction,安排稳定后的 Task 与 Surface 更新。
WCT 的真实调用路径由 StageCoordinator.updateWindowBounds() 收口:
private boolean updateWindowBounds(SplitLayout layout,
WindowContainerTransaction wct) {
return layout.applyTaskChanges(wct, mMainStage.mRootTaskInfo,
mSideStage.mRootTaskInfo);
}结论:方法名中的 Changing / Changed 是重要边界:前者以偏移量驱动交互中的合成层更新,后者使 WindowManager 的 Task 状态与稳定 Surface 几何收敛。SplitLayout.applyTaskChanges() 接收的是 WCT 和两个 RunningTaskInfo,不是 stage 对象;应沿 updateWindowBounds() 继续追踪 WCT 的提交与 transition token 的来源。
源码节点:SplitScreenTransitions 交给 Transitions 完成生命周期
- 类与方法:
SplitScreenTransitions的 enter / resize / dismiss 启动方法;Transitions的 ready、play 与 finish 路径。 - 路径:
frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java;frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java - 进程与线程:WM Shell 进程,Shell main executor / animation executor。
- 作用:分屏类保存正在处理的 transition 与 finish callback;通用类等待 WindowManager 收集完成,挑选 handler 播放,再在结束时回调。
流程伪代码:
SplitScreenTransitions.startEnter/startResize/startDismiss
→ Transitions.startTransition 或接管已有 token
→ WindowManager 收集 change 并通知 ready
→ Transitions.onTransitionReady 选择 handler,调用 startAnimation
→ handler 完成后调用 finish callback,提交 finish transaction结论:enter、resize、dismiss 共享“启动—ready—播放—finish”的框架,但分屏类记录的临时状态和动画策略不同。不要把 transition token 当作 leash,也不要把 finish transaction 当作 WCT;三者分别标识生命周期、控制合成层和描述逻辑容器。
验证与排障
目标:先验证本文引用的 r75 入口及其职责,再在设备上观察分屏的逻辑与表面状态;不把厂商 SystemUI 的视觉效果当作 AOSP 的通用实现。
环境与权限:源码命令在已同步 android-14.0.0_r75 的主机终端运行。设备观察需要支持分屏的 Android 14 设备或模拟器和 adb shell;普通应用不具有 Shell/WindowManager 所需权限。
# 主机终端:定位关键入口和它们使用的对象。
rg -n 'onTaskAppeared|applySurfaceChanges|applyTaskChanges' \
frameworks/base/core/java/android/window/TaskOrganizer.java \
frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
rg -n 'onLayoutSizeChanging|onLayoutSizeChanged' \
frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
rg -n 'startEnter|startResize|startDismiss|onTransitionReady' \
frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java \
frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
# 设备进入分屏后,在主机终端运行。
adb shell dumpsys activity activities
adb shell dumpsys window windows预期:前三条能定位 Task callback、布局双路径与 transition 生命周期;后两条可观察分屏时 Task、bounds 和窗口状态。若结果不符,先确认设备的分屏实现、当前用户与 Shell 日志,再从 StageCoordinator 的调用点追踪 token / WCT / transaction 的传递。
限制:dumpsys 是状态快照,不能单独证明某一帧已经合成或动画何时 finish;厂商可以替换 SystemUI/WM Shell 的动画策略和输出格式。
常见误区
- 误区:Task 就是
SurfaceControl。- 问题:一个是 WindowManager 的逻辑容器,一个是合成树的控制句柄。
- 正确理解:用
onTaskAppeared(taskInfo, leash)的双参数建立映射;修改谁取决于想改逻辑状态还是图层状态。 - 验证方式:对照
TaskOrganizer.java与SplitLayout的 WCT / transaction 两条调用。
- 误区:leash 是应用窗口本身,所以可直接代表 Task 完成。
- 问题:leash 是给 organizer 的控制边界;出现回调与某笔 WCT、某段动画的完成无一一对应。
- 正确理解:需要逻辑终态看 WCT / WindowManager,需要动画终态看 transition finish callback。
- 误区:
SurfaceControl.Transaction.apply()后所有窗口变化就已结束。- 问题:它只提交 Surface 操作;WCT、收集状态、动画与 finish transaction 仍可能处于不同阶段。
- 正确理解:按
Transitions的 ready → play → finish 追踪生命周期。
延伸阅读
- WindowOrganizer 如何跨进程重组窗口容器:理解 WCT 怎样进入
system_server。 - Task、Root Task 与 WindowContainer 如何描述多窗口层级:理解 Task token、层级与 bounds 的归属。
- 多窗口 Configuration 如何从 WindowManager 传到应用:理解
smallestScreenWidthDp与应用配置的关系。 - WM Shell 与 SystemUI 如何组织分屏:把 Surface / transition 路径放回完整的分屏职责图。
- SurfaceFlinger 为什么不画 View,却决定屏幕最终显示什么:理解 transaction 到达合成侧之后的职责边界。
- 应用窗口的 Surface 如何创建并连接到 SurfaceFlinger:区分 Surface、SurfaceControl、BLASTBufferQueue 与服务端 Layer。
源码入口
Transition 完成后,窗口逻辑对象与 Surface 的移除仍要收敛,见 动画、切换与窗口移除。
| 文件 | 关键位置 | 作用 |
|---|---|---|
| 见本章“关键源码”各节点 | 类名 / 方法名 | 证明主流程的关键动作 |
| Android 14 r75 对应源码文件 | 使用 rg -n 核对 | 发布前记录真实行号,避免跨 tag 漂移 |