WM Shell 为什么运行在 SystemUI 进程
WM Shell 为什么运行在 SystemUI 进程
问题场景
从最近任务把一个应用放到分屏位置后,屏幕上会出现分隔条、第二个应用的选择界面和过渡动画;但真正改变 Task 父子关系、窗口模式和 bounds 的仍是 system_server。这容易产生两个相反的误解:要么把分屏当成 SystemUI 的一组 View,要么认为 WMS 应当直接持有全部交互和动画。
Android 14 AOSP 的做法是在 SystemUI 进程中宿主 WM Shell:SystemUI 提供稳定的系统 UI 生命周期、依赖和进程身份;WM Shell 作为窗口管理客户端,通过受控 Binder API 请求 system_server 组织窗口,并在自己的 Shell 线程协调任务、布局和 Surface。本文以 android-14.0.0_r75 为准,只建立这一组件与边界模型;不展开一次完整的分屏进入/退出操作、Transition 动画细节或应用 Configuration 分发。
排查时应分别确认谁接收 Launcher 请求、谁提交 WindowContainerTransaction、谁维护两侧 Stage,再决定问题属于 SystemUI、WM Shell 还是 system_server 的窗口服务。
核心结论
WM Shell 运行在 SystemUI 进程,不表示 SystemUI View 在直接重排应用窗口;它表示一个具有系统 UI 生命周期和权限环境的客户端,在进程外通过 WindowOrganizer 与 system_server 协作。
WMShell是 SystemUI 的CoreStartable宿主。它初始化各个 Shell interface,并把 SysUI 状态、命令和配置事件桥接给 Shell 组件。WMShellModule.provideSplitScreenController()创建分屏控制器;WMShellBaseModule.provideSplitScreen()再把可导出的SplitScreeninterface 作为可选能力提供。feature gate 不满足时,该 interface 不应被误认为存在。SplitScreenController是外部 API 门面;StageCoordinator协调 Main/Side 两个 Stage、分隔条及转场;StageTaskListener是每个 Stage 对 Task 生命周期和 leash 的监听入口;SplitLayout只计算和分发分屏几何布局。- Launcher/Quickstep 到
ISplitScreen是“请求 Shell 做分屏”的 Binder;Shell 到WindowOrganizer是“请求窗口服务原子重组容器”的 Binder。两条 Binder 的方向、权限和语义不同,不能合并成一跳。 - Shell 的可变状态不应在任意 Binder 线程直接改动。
@ShellMainThread和ShellExecutor标示/调度 Shell 工作;ISplitScreenImpl用executeRemoteCallWithTaskPermission先验证调用权限,再把远程调用投递到控制器的 executor。
核心对象
| 对象 | 所在进程/线程 | 职责 | 不负责什么 |
|---|---|---|---|
WMShell | SystemUI 进程;SystemUI 启动路径 | 初始化并连接 Shell interface 与 SysUI 生命周期 | 不直接保存两侧 Task 的布局状态 |
SplitScreenController | SystemUI 进程;Shell executor | 对外发布 split-screen API,转交协调器 | 不替代窗口服务修改容器树 |
StageCoordinator | SystemUI 进程;Shell main executor | 协调进入、退出、配对、转场及两侧 Stage | 不把应用窗口变成 SystemUI View |
MainStage / SideStage | SystemUI 进程;Shell executor | 分别表示主、辅侧根任务的组织状态 | 不决定整屏的几何算法 |
StageTaskListener | SystemUI 进程;来自 organizer 的回调再串行化 | 跟踪 Stage root task、子 Task 与 leash | 不拥有 WindowContainer 逻辑树 |
SplitLayout | SystemUI 进程;Shell executor | 根据显示配置和 divider 位置计算 bounds | 不提交 Task 层级事务 |
WindowOrganizer | 客户端 Binder 入口 → system_server | 接受 WCT/transition 请求 | 不承担 SystemUI 的交互策略 |
下面的图重点观察两次 Binder:第一次让 Launcher 抵达 Shell,第二次让 Shell 抵达窗口服务;虚线表示 executor 的异步串行化,而不是同一线程上的直接调用。
关键转折是:ISplitScreen 接收的是外部的“分屏意图”,而 WindowOrganizer 接收的是 Shell 组织器整理后的“容器变更请求”。后者由系统服务在 WM/ATMS 的一致性规则下执行;Shell 收到 TaskOrganizer 回调后才据此更新自己的 Stage 与 Surface 侧状态。
主流程
阶段一:SystemUI 启动时装配 Shell
- 执行者:SystemUI 进程的启动路径;
WMShell为CoreStartable。 - 输入:Dagger 提供的可选 PIP、split screen、one-handed 等 Shell interface。
- 动作:
WMShell.start()初始化已经启用的接口,并注册与 SystemUI 状态、配置、命令/转储相关的桥接。 - 下一跳:split screen interface 的初始化进入 Shell 组件;后续外部请求由它暴露的 Binder implementation 接收。
- 边界:同进程的依赖注入与生命周期边界;不是到
system_server的窗口事务。
因此“运行在 SystemUI”首先是宿主关系。SystemUI 负责把 Shell 纳入开机启动、用户切换、配置变化和调试入口,而不是让状态栏 View 成为应用 Task 的父节点。
阶段二:模块决定 split screen 是否存在
- 执行者:Dagger provider,创建阶段在 SystemUI 进程。
- 输入:
ShellTaskOrganizer、SyncTransactionQueue、display/transition/ime 及 Shell executor 等依赖。 - 动作:
WMShellModule.provideSplitScreenController()组装SplitScreenController;WMShellBaseModule.provideSplitScreen()把可选 controller 映射为asSplitScreen(),其可用性由@BindsOptionalOf/@DynamicOverride的绑定边界决定。 - 下一跳:
WMShell只初始化已提供的 optionalSplitScreen;Quickstep 也只能在该能力存在时拿到远程接口。 - 边界:feature gate 是依赖可用性边界,不是 Launcher 可绕过的 UI 开关。
这解释了为什么阅读启动问题时要同时看两个 module:前者回答“控制器如何构造”,后者回答“它何时能作为可导出 Shell 接口出现”。设备配置、form factor 或产品裁剪可能改变 gate,不能把某台设备上没有分屏直接归因于 controller 的业务逻辑。
阶段三:外部 Binder 被串行化到 Shell
- 执行者:
SplitScreenController.ISplitScreenImpl的 Binder stub,随后是标注为@ShellMainThread的 Shell executor。 - 输入:Launcher/Quickstep 通过
ISplitScreen发来的启动、替换、退出等远程请求。 - 动作:实现使用
executeRemoteCallWithTaskPermission检查 Task 相关权限,并将调用交给 controller 的ShellExecutor;实际的协调状态由 executor 上的StageCoordinator改动。 - 下一跳:controller 转到
StageCoordinator的 enter/exit 或配对路径。 - 边界:Binder 线程 → Shell executor 的线程边界;权限检查在远程调用入口。
@ShellMainThread 是线程约定,ShellExecutor 是具体的投递机制。不要仅因方法名叫 ISplitScreenImpl 就断言它在 Shell 主线程同步执行;源码中的远程调用辅助方法正是把 Binder 接收与状态机执行分开的证据。
阶段四:协调 Stage、布局和窗口服务
- 执行者:
StageCoordinator,由 Shell executor 串行执行。 - 输入:经权限检查的 API 请求、TaskOrganizer 回调、显示配置和 divider 位置。
- 动作:协调
MainStage与SideStage的激活/可见性,要求SplitLayout计算两侧 bounds 和 divider bounds;每个 Stage 通过其StageTaskListener接收 root task、子任务及 leash 的出现/变化/消失。 - 下一跳:将 reparent、windowing mode、bounds 等写进
WindowContainerTransaction,由WindowOrganizer送往服务端;同步/转场细节交由后续专题。 - 边界:Shell →
system_server的 Binder;服务端回调 → Shell executor 是反向的 Binder/线程交接。
这里的角色划分避免了“一个 SplitScreenController 做完所有事情”的模型:controller 面向调用者,coordinator 面向协作和状态,listener 面向 Task 生命周期,layout 面向几何计算。它们共同运行于 SystemUI 进程,但逻辑 Task 容器仍属于 system_server。
关键源码
源码节点:SystemUI 宿主初始化
- 类:
WMShell - 方法:
start()、各init*()初始化路径 - 路径:
packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java - 进程:SystemUI
- 线程:SystemUI 启动路径;具体 Shell 工作再交给各 interface/executor
- 作用:证明 WM Shell 是由 SystemUI 以组件方式启动,并初始化 exported Shell interfaces。
阅读问题:宿主是否直接实现分屏逻辑,还是只负责初始化和桥接?
// AOSP r75 源码节选;省略其他 SystemUI 订阅与 Shell 组件。
@Override
public void start() {
mShell.onConfigurationChanged(mContext.getResources().getConfiguration());
mConfigurationController.addCallback(mConfigurationListener);
// 省略无关代码
mPipOptional.ifPresent(this::initPip);
mSplitScreenOptional.ifPresent(this::initSplitScreen);
mOneHandedOptional.ifPresent(this::initOneHanded);
// 省略无关代码
}
@VisibleForTesting
void initSplitScreen(SplitScreen splitScreen) {
mWakefulnessLifecycle.addObserver(new WakefulnessLifecycle.Observer() {
@Override
public void onFinishedWakingUp() {
splitScreen.onFinishedWakingUp();
}
});
mCommandQueue.addCallback(new CommandQueue.Callbacks() {
@Override
public void moveFocusedTaskToFullscreen(int displayId) {
splitScreen.goToFullscreenFromSplit();
}
});
}结论:WMShell 是宿主和适配层。r75 的 initSplitScreen() 连接的是唤醒完成与 CommandQueue 的“返回全屏”事件,并没有调用 asSplitScreen() 或 ShellInterface.onInit();真正的分屏状态机不应从这段 SystemUI 桥接代码推断为运行在状态栏 View 层。
源码节点:依赖注入与 feature gate
- 类:
WMShellModule、WMShellBaseModule - 方法:
provideSplitScreenController()、provideSplitScreen() - 路径:
libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java;libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java - 进程:SystemUI(Dagger 图在此进程构建)
- 线程:对象创建路径;后续可变状态遵循 Shell executor
- 作用:将实现控制器与可导出
SplitScreeninterface 分离,并保留 feature gate。
阅读问题:为什么 controller 被创建不等于所有调用方都一定能得到 split-screen Binder?
// AOSP r75 结构节选;省略构造依赖。
@WMSingleton
@Provides
@DynamicOverride
static SplitScreenController provideSplitScreenController(/* Shell dependencies */) {
return new SplitScreenController(/* Shell dependencies */);
}
@WMSingleton
@Provides
static Optional<SplitScreen> provideSplitScreen(
Optional<SplitScreenController> splitScreenController) {
return splitScreenController.map((controller) -> controller.asSplitScreen());
}
@BindsOptionalOf
@DynamicOverride
abstract SplitScreenController optionalSplitScreenController();结论:provider 层把实现、接口与能力条件拆开。r75 的 provideSplitScreen() 本身只是映射 Optional<SplitScreenController>;可选性来自 @BindsOptionalOf 与 @DynamicOverride 的绑定边界,而不是该方法内部虚构的条件分支。
源码节点:Controller 与远程调用线程边界
- 类:
SplitScreenController内部ISplitScreenImpl - 方法:
asSplitScreen()、远程 API 实现 - 路径:
libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java - 进程:SystemUI
- 线程:Binder 接收线程 →
@ShellMainThreadShellExecutor - 作用:证明 Launcher Binder 入口在权限检查后才进入 Shell 状态机。
阅读问题:外部 Binder 调用如何避免直接在 Binder 线程改 Stage 状态?
// AOSP r75:ISplitScreenImpl.startTasks() 的核心调用。
executeRemoteCallWithTaskPermission(mController, "startTasks", controller -> {
controller.mStageCoordinator.startTasks(taskId1, options1, taskId2, options2,
splitPosition, snapPosition, remoteTransition, instanceId);
});结论:这里没有第二次 mMainExecutor.execute()。executeRemoteCallWithTaskPermission 先检查 MANAGE_ACTIVITY_TASKS,再通过 mController.getRemoteCallExecutor().execute(...) 投递;它的 callback 已在该 executor 上执行,因此 lambda 直接调用 StageCoordinator.startTasks()。结合 controller 注入的 @ShellMainThread ShellExecutor,不能把 Launcher 的 Binder 线程当成 StageCoordinator 的工作线程。
源码节点:两侧 Stage 与几何布局
- 类:
StageCoordinator、MainStage、SideStage、StageTaskListener、SplitLayout - 方法:各 Stage 的 task 回调及
SplitLayout的 bounds/layout 回调路径 - 路径:
libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java;libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java;libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java - 进程:SystemUI
- 线程:Shell main executor;TaskOrganizer 的 Binder 回调须回到该串行上下文
- 作用:证明“任务生命周期”“协调策略”“几何计算”是不同职责。
阅读问题:拖动 divider 时,哪个对象计算 bounds,哪个对象再把它变成窗口服务可执行的请求?
// 教程注释:职责关系的流程伪代码,不是 AOSP 可直接编译的源码节选。
// StageCoordinator:读取 SplitLayout 给出的两侧 bounds。
// StageTaskListener:持有对应 Stage 的 taskInfo / leash 生命周期信息。
// StageCoordinator:把 reparent、windowing mode、bounds 组织为 WCT。
// WindowOrganizer:跨 Binder 交给 system_server 原子应用。结论:SplitLayout 描述“应该占多大区域”,StageTaskListener 描述“哪个 Task/leash 已到达”,StageCoordinator 决定“何时将哪些变化组织为一次请求”。后两步的容器合法性仍由服务端判断。
验证与排障
目标:确认设备上 SystemUI 中是否装载了 WM Shell split-screen 组件,并把运行时现象与源码入口对应起来。
环境与权限:Android 14 AOSP 风格设备或模拟器,已开启 USB 调试;先手工进入分屏。本节命令在主机终端执行,只读取调试服务输出,量产设备可能因 build 类型、SELinux 或厂商裁剪而省略字段。
# 主机终端执行;读取 SystemUI 的 dump,过滤 WM Shell / split 相关段落。
adb shell dumpsys activity service com.android.systemui/.SystemUIService | \
grep -i -E 'wmshell|split|stage|divider'预期:AOSP 调试镜像通常能看到与 WM Shell、split 或其 dump callback 有关的段落;配合实际进入/退出分屏时的变化,可支持“组件由 SystemUI 宿主且在运行”的观察。它不能单独证明某个方法的线程顺序,也不能替代源码对 provider gate 的核对。
异常方向:无输出时先执行不带过滤的 adb shell dumpsys activity service com.android.systemui/.SystemUIService,确认服务名/厂商实现是否可用;再检查设备是否支持分屏及目标 build 是否裁剪 Shell dump。若问题是“点击后无分屏”,从 Launcher 的 ISplitScreen 获取和 provideSplitScreen() gate 查起;若是“分屏后布局错误”,从 SplitLayout、StageCoordinator 与 WindowOrganizer 的 WCT 路径分开查。
限制:dumpsys 的文本格式不是稳定 API;OEM 可替换 Quickstep、SystemUI 或 WM Shell。它观察的是当前设备状态,不能把厂商输出反推为 Android 14 r75 的固定调用序列。
常见误区
误区:分屏 UI 在 SystemUI,所以 SystemUI View 直接拥有两个应用窗口。
- 问题:混淆了 Shell 进程宿主与
WindowContainer的服务端所有权。 - 正确理解:Shell 在 SystemUI 进程协调;Task 层级由
system_server经 WindowOrganizer 路径修改。 - 验证方式:对照
WMShell.java的初始化,与 WindowOrganizer 的 WCT 服务端入口。
- 问题:混淆了 Shell 进程宿主与
误区:
ISplitScreen与WindowOrganizer是同一个 Binder 调用。- 问题:前者表达 Launcher 到 Shell 的请求,后者表达 Shell 到窗口服务的容器事务;中间还有权限、executor 和协调逻辑。
- 正确理解:按本文流程图分别追踪两个跨进程边界。
- 验证方式:查看
SplitScreenController.ISplitScreenImpl的远程调用辅助方法,以及WindowOrganizer的 client API。
误区:
SplitLayout改了 bounds 就已经完成了分屏。- 问题:布局对象只计算几何;需要 coordinator 组织 WCT,服务端才能修改任务容器;Surface/Transition 还负责把结果平滑呈现。
- 正确理解:几何、容器事务、视觉呈现是相邻但不同的层。
- 验证方式:分别阅读
SplitLayout.java、StageCoordinator.java和 SurfaceControl 与 Transition。
延伸阅读
- 动画、切换与窗口移除如何保持一致:理解 WMS 逻辑退出与 Shell 编排的协作边界。
- WindowOrganizer 如何跨进程重组窗口容器:继续追踪 Shell 提交 WCT 后服务端如何更新 Task。
- SurfaceControl、leash 与 Transition 如何呈现窗口变化:继续区分稳定容器状态和视觉事务/动画。
- Android 14 双应用分屏如何从用户操作走到屏幕显示:把 Launcher、WM Shell、窗口服务和应用侧结果连成完整链路。
frameworks/base/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java:SystemUI 宿主入口。frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java:两侧 Stage 的协调入口。
源码入口
Shell 动画结束不代表窗口对象已同步销毁,退出路径见 动画、切换与窗口移除。
| 文件 | 关键位置 | 作用 |
|---|---|---|
| 见本章“关键源码”各节点 | 类名 / 方法名 | 证明主流程的关键动作 |
| Android 14 r75 对应源码文件 | 使用 rg -n 核对 | 发布前记录真实行号,避免跨 tag 漂移 |