Android 14 双应用分屏如何从用户操作走到屏幕显示
Android 14 双应用分屏如何从用户操作走到屏幕显示
问题场景
用户把一个任务拖入分屏、选择第二个应用、移动分隔条或关闭其中一侧时,看起来只是两个窗口在屏幕上变化;但 AOSP Android 14 中,启动入口在 Launcher3/Quickstep,布局与动画在 WM Shell,任务层级与配置只能由 system_server 提交,最终才由 Surface 合成。把这些职责混在一起,往往会把“WCT 已提交”“应用收到 Configuration”或“leash 已移动”误当成同一件事。
本文以 AOSP android-14.0.0_r75 为准,追踪双应用分屏的进入、第二应用、显示、拖动和退出。 主线包含 Quickstep 到 Shell、Shell 到窗口组织器的两段 Binder,以及 WCT 逻辑路径与 SurfaceControl.Transaction 视觉路径。厂商入口和自由窗口行为不能套用这条 AOSP 主线。
证据状态:本文已按 android-14.0.0_r75 源码核对类、方法和调用职责,但尚未在具体设备执行操作与命令;验证段给出的是待复现步骤,不是本文已经取得的实测结果。
两条阅读路线:想先建立“任务、配置、WCT”模型,按容器层级 → 多窗口 Configuration → WindowOrganizer阅读;想先跟操作排障,读本文后回看 Binder 与 调试方法。这些文章解释基础对象,本文只把它们串成一次分屏操作。
核心结论
- 进入分屏的入口可以是 Launcher3/Quickstep 的
SplitSelectStateController。两个已有任务走SystemUiProxy.startTasks();其他目标组合分别走startIntentAndTask()、startIntents()或startShortcutAndTask()。这些接口跨第一个 Binder 边界请求 SystemUI 的ISplitScreen,并不直接修改 Task 或 Surface。 SplitScreenController.ISplitScreenImpl在对应 Binder 入口做任务权限保护,并把实际工作投递到 Shell executor;StageCoordinator的对应启动方法负责组织主/副 stage、WCT 和 transition。- WCT 路径把 Task 的父子关系、窗口模式和 bounds 提交到
WindowOrganizerController;Surface 路径只用已授权的 leash 做拖动期的即时视觉反馈。两条路径目的不同,不能互相替代。 - 第二应用既可能是已有任务,也可能来自 PendingIntent 或 shortcut;选择状态控制器必须按目标组合选择对应接口,不能把它们都归入
startTasks()。 - 拖动时优先更新 Surface,松手后才用 WCT/resize transition 稳定逻辑布局;吸附到 dismiss 目标后,
StageCoordinator调用退出链路,而不是仅隐藏分隔条。
核心对象
| 对象 | 进程/线程 | 职责 | 下一跳 |
|---|---|---|---|
SplitSelectStateController | Launcher3/Quickstep,主线程 | 保存首个任务和第二应用选择,按目标组合发起启动 | SystemUiProxy 的四类 split 启动接口 |
SystemUiProxy / ISplitScreen | Launcher3 → SystemUI,Binder | Quickstep 对 Shell 接口的代理 | ISplitScreenImpl |
SplitScreenController.ISplitScreenImpl | SystemUI Binder 线程 → Shell executor | 权限门和线程切换 | StageCoordinator |
StageCoordinator | SystemUI,Shell executor | 管理主/副 stage、布局、transition 和退出 | WCT 与 leash 事务 |
WindowOrganizerController | system_server Binder 线程/全局锁 | 原子应用 WCT 到窗口容器 | Task/Configuration |
SplitLayout / DividerView | SystemUI,Shell/输入线程 | 计算分隔位置并通知布局变化 | Surface 反馈或稳定提交 |
先看两个 Binder 边界与两条提交路径。实线的 WCT 改变服务端逻辑状态;虚线的 Surface 事务在已有 leash 上给用户连续反馈。第二条并不会绕过第一条去“创建分屏”。
主流程
进入与第二应用:Quickstep 只提出“同时启动”请求
- 执行者:Launcher3/Quickstep 主线程的
SplitSelectStateController。 - 输入:两个已有任务,或由 task、PendingIntent、shortcut 组成的受支持组合。
- 动作:控制器累计选择状态,完成第二应用选择后按 r75 的真实分支调用
SystemUiProxy:两个 task id 走startTasks();PendingIntent + task 走startIntentAndTask();两个 intent 目标走startIntents();shortcut + task 走startShortcutAndTask()。 - 下一跳:
SplitScreenController.ISplitScreenImpl中对应的startTasks()、startIntentAndTask()、startIntents()或startShortcutAndTask()Binder 实现。 - 边界:Binder ①,Quickstep → SystemUI。调用名称与 UI 无关;长按图标、概览手势或厂商多窗口入口只是到达这个请求前的不同产品路径。
第二应用并不必然是已有最近任务。关键不是把所有目标统称为 intent 后传给 startTasks(),而是保留 task、PendingIntent 与 shortcut 的形态,让相匹配的 AIDL 方法完成跨进程传递。本文没有从 r75 源码闭合“已在分屏中替换某一侧应用”的统一入口,因此不把设备上的替换按钮或菜单作为本文流程的一部分。
Shell 接收请求:权限、executor 与 stage 协调
- 执行者:
ISplitScreenImpl先在 SystemUI Binder 线程接收,再通过executeRemoteCallWithTaskPermission()转到 Shell executor。 - 输入:对应接口传来的 task、PendingIntent 或 shortcut 启动描述、stage position、split ratio 与 transition 信息。
- 动作:
StageCoordinator.startTasks()、startIntentAndTask()、startIntents()或startShortcutAndTask()按各自输入启动目标;共同的后半段准备 stage、WCT 与 transition。 - 下一跳:
WindowOrganizer提交 WCT,或由 transition 将 WCT 纳入收集生命周期。 - 边界:Binder 线程到 executor 是异步线程切换;权限检查说明这不是普通应用能直接调用的公共多窗口 API。
逻辑显示:WCT 让两个应用真正成为分屏布局
StageCoordinator 不把应用 View 缩到一半,而是描述 Task 应如何重组:父子关系、windowing mode、bounds 与可见性由 WindowContainerTransaction 一并携带。WindowOrganizer 通过 Binder ② 把它交给 system_server 的 WindowOrganizerController;服务端在窗口管理全局锁内更新 WindowContainer、Task 和 Configuration,并在合适的 transition 时机安排 Surface/动画。应用随后可能收到配置或多窗口模式变化,但 resize、回调和重建受其配置处理与具体变化影响,不能以“进入分屏”推断必然重建。
这条路径负责“最终真相”。反过来,TaskOrganizer/Shell 获得的 stage leash 只是被授权控制的 Surface 句柄:它适合摆放、裁剪和动画,不是一个可替代 WCT 的 Task 数据结构。
拖动与松手:一快一稳的双路径
拖动分隔条时,DividerView.onTouch() 处理 ACTION_MOVE,把当前位置交给 SplitLayout.updateDivideBounds()。布局算出两侧 bounds 后回调 onLayoutSizeChanging();StageCoordinator 以 SurfaceControl.Transaction 更新两侧 leash 的位置、crop 等视觉属性。这个阶段追求连续帧反馈,通常不应为每个触点位置都提交一次 Task 配置事务。
松手时,DividerView 通过 snap 算法选择目标;若目标仍是有效分割位置,SplitLayout 通知 onLayoutSizeChanged()。StageCoordinator 以 applyTaskChanges() 把稳定 bounds 写入 WCT,必要时经 resize transition 完成逻辑配置与动画的收敛。于是“手指在动”与“任务最终 bounds 已改”是相邻但不同的观察点。
退出:dismiss 目标会拆分 stage,而非只清除画面
如果 snap target 是 dismiss,SplitLayout 通过 onSnappedToDismiss() 告诉协调器哪一侧应退出;StageCoordinator 组织 WCT/transition,并走 exitSplitScreen() 清理分屏 stage、恢复留下任务的布局。显式退出或关闭一侧任务也会进入相应的退出收敛。最终显示恢复全屏,是逻辑 Task/Configuration 已更新再经 Surface 合成的结果;仅对 leash 设 alpha 并不等价于已退出分屏。
关键源码
下面的路径均以 android-14.0.0_r75 的 AOSP 目录为阅读锚点。方法签名会含有可选 transition、用户和启动选项;因此这里强调职责和边界,不把某个 overload 的参数顺序当作产品 API。
源码节点:Launcher3 如何按目标组合选择 Binder 接口
- 类与方法:
SplitSelectStateController;SystemUiProxy.startTasks()、startIntentAndTask()、startIntents()、startShortcutAndTask() - 路径:
packages/apps/Launcher3/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java;packages/apps/Launcher3/quickstep/src/com/android/quickstep/SystemUiProxy.java - 进程/线程:Launcher3/Quickstep 主线程。
- 作用:在第二应用确定后,按两端目标的真实类型选择
ISplitScreen启动请求。
阅读问题:为什么桌面不能把所有第二目标都传给 startTasks()?因为 r75 的 startTasks() 只接收两个 task id;SplitSelectStateController 会依据 PendingIntent、shortcut 与 task 的组合,分别调用四类接口。这些分支只负责跨 Binder 请求特权 Shell,窗口树仍在 system_server。
流程伪代码:
两个 task id → startTasks(...)
PendingIntent+task → startIntentAndTask(...)
两个 intent 目标 → startIntents(...)
shortcut+task → startShortcutAndTask(...)结论:Quickstep 保留目标类型并选择对应远端接口;此处没有 WCT,也没有对应用 Surface 的直接操作。代码块是流程伪代码,不能当作方法签名摘录。
源码节点:Binder Stub 为什么还要切到 Shell executor
- 类与方法:
SplitScreenController.ISplitScreenImpl的四类启动方法;executeRemoteCallWithTaskPermission() - 路径:
libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java - 进程/线程:SystemUI Binder 线程 → Shell executor。
- 作用:在受任务权限保护的远程入口中,把对应启动请求转交
SplitScreenController/StageCoordinator的同名路径。
阅读问题:Binder 调用是否直接在 Binder 线程修改 stage 状态?查该内部实现可见远程调用包装器先检查 task 权限,再把 controller 调用投递给 Shell executor。继续跟对应同名方法到 StageCoordinator,能看到各分支把目标启动与 WCT/transition 组织起来;这正是第一条 Binder 与 Shell 内部异步边界的分界线。
源码节点:WCT 与 Surface 事务各自改变什么
- 类与方法:
StageCoordinator.startTasks()、applyTaskChanges()、onLayoutSizeChanging()、onLayoutSizeChanged() - 路径:
libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java - 配套类:
SplitLayout.updateDivideBounds(),路径libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java;DividerView.onTouch(),路径libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java。 - 进程/线程:SystemUI 的 Shell executor/输入处理路径。
- 作用:首次/稳定布局组织 WCT;拖动中把计算结果反馈给 stage leash;松手把稳定结果提交回逻辑任务。
阅读问题:拖动每一帧为何没有必要进入 WindowOrganizerController?顺着 onTouch() → updateDivideBounds() → onLayoutSizeChanging() 看,布局变化会被反馈给 Surface 事务;而 onLayoutSizeChanged() 的稳定分支才进入 applyTaskChanges() 和 resize/transition 处理。两段回调名称相近,职责却刻意不同。
源码节点:WCT 在哪里获得服务端效力
- 类与方法:
WindowOrganizer.applyTransaction()/ transition 启动;WindowOrganizerController.applyTransaction() - 路径:
core/java/android/window/WindowOrganizer.java;services/core/java/com/android/server/wm/WindowOrganizerController.java(均相对frameworks/base/)。 - 进程/线程:Shell 调用线程 →
system_serverBinder 线程,并在 WMS 全局锁保护的窗口管理路径内应用。 - 作用:把 WCT 的 bounds、mode、reparent 等描述变成 Task/Configuration 的服务端状态。
这就是 Binder ②。它和 Binder ① 的方向、接口与职责不同:前者把 Quickstep 的用户选择交给 SystemUI,后者把 Shell 的窗口事务交给 system_server。若显示异常,先分辨 WCT 是否真正到达服务端,再判断 leash/transition 是否完成,而不要把两次 IPC 当成一次。
源码节点:从 dismiss 到 exitSplitScreen()
- 类与方法:
DividerView的 snap 处理;SplitLayout的 dismiss 回调;StageCoordinator.onSnappedToDismiss()、exitSplitScreen()。 - 路径:
libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java;.../SplitLayout.java;.../splitscreen/StageCoordinator.java。 - 进程/线程:SystemUI Shell executor。
- 作用:将“分隔条到了边缘”转为明确的 stage 退出和稳定事务。
阅读问题:为什么只隐藏 divider 仍会留下错误任务状态?从 dismiss target 的回调进入 onSnappedToDismiss(),再读 exitSplitScreen() 周围的 WCT/transition 清理,可见退出需要修改 stage/task 的逻辑组织;视觉层的隐藏只是其中一小段效果。
验证与排障
目标:分别观察“系统认为现在有分屏 Task”与“Shell 正在控制哪组 leash/transition”,不要只凭菜单或截图判断。
环境与权限:Android 14 调试设备,已开启 USB 调试;以下命令在主机终端执行。本文未在设备上实际执行这些步骤;它们是基于 r75 源码给出的复现方案。多数零售设备可读取基础 dump,但 SystemUI/Shell 字段、日志等级和菜单入口会因构建与厂商变化。
# 1) 先进入设备实际提供的双应用分屏,再在主机终端运行。
adb shell dumpsys activity activities
adb shell dumpsys window containers
# 2) 观察任务组织器、Shell transition 与 Surface 相关线索;无字段不表示分屏不存在。
adb shell dumpsys activity service SystemUIService
adb logcat -v threadtime -s WM-Shell ActivityTaskManager WindowManager
# 3) 在拖动分隔条期间取样:寻找 layer/leash 的位置与裁剪变化(输出很大)。
adb shell dumpsys SurfaceFlinger --list
adb shell dumpsys SurfaceFlinger预期:activity activities/window containers 应显示相关 Task、windowing mode 或 bounds 的组织变化;拖动期间 log 与 Surface dump 的位置/裁剪线索可能先变化,松手后 Task bounds 才应稳定。若能使用 userdebug/eng 构建,可提高 WM Shell 日志等级或在上述源码节点打断点,以确认 startTasks、onLayoutSizeChanging、onLayoutSizeChanged 和 exitSplitScreen 的先后。
异常方向:没有进入分屏时,先确认设备是否支持,并按目标类型检查 Quickstep 调用的是哪一个 ISplitScreen 启动接口;已显示但 task 状态不对时检查 WCT/WindowOrganizerController;任务状态正确却动画或拖动错位时检查 stage leash、SplitLayout 和 transition。
限制:dumpsys 格式不是兼容 API,SurfaceFlinger 输出也不能单独证明哪次 WCT 造成某层变化;需要把时间点与日志、源码调用一并对齐。
常见误区
- 误区:分屏是 Launcher 把两个应用窗口缩小。
- 问题:Launcher 只发起 Binder 请求,不能自行改
system_server的 Task 树。 - 正确理解:Quickstep →
ISplitScreen→ Shell → WCT/WindowOrganizerController才是逻辑状态主线。 - 验证方式:对照两个 Binder 边界和
dumpsys activity activities。
- 问题:Launcher 只发起 Binder 请求,不能自行改
- 误区:
SurfaceControl.Transaction与 WCT 是同一事务。- 问题:前者操作 leash 的合成属性,后者描述窗口容器的逻辑变更。
- 正确理解:拖动用前者给快反馈,松手/进入用后者稳定 Task 与 Configuration。
- 验证方式:比较
onLayoutSizeChanging()和onLayoutSizeChanged()的调用点。
- 误区:选完第二应用后,两个 Activity 必然立即重建。
- 问题:配置分发、应用声明和 transition 时机都会影响可观察现象。
- 正确理解:先确认 Task/Configuration,再分别观察应用回调与首帧。
- 验证方式:结合本文命令与多窗口 Configuration。
- 误区:第二应用无论来自哪里都走
SystemUiProxy.startTasks()。- 问题:r75 的
startTasks()是两个 task id 的分支,不能承载 PendingIntent 或 shortcut 目标。 - 正确理解:
SplitSelectStateController会选择startIntentAndTask()、startIntents()、startShortcutAndTask()或startTasks()。 - 验证方式:在 Launcher3 选择完成分支记录目标类型与实际调用的方法名。
- 问题:r75 的
- 误区:拖到边缘只需把 divider 隐藏。
- 问题:这会遗漏 stage/task 的退出和留下任务的恢复。
- 正确理解:dismiss target 需走
onSnappedToDismiss()到exitSplitScreen()的逻辑收敛。 - 验证方式:退出前后比较 activity/window dump 的 Task/bounds。
延伸阅读
- WindowOrganizer 如何跨进程重组窗口容器:深入第二条 Binder 与 WCT 的服务端提交。
- 多窗口 Configuration 如何从 WCT 到应用回调:继续追踪 Task bounds 到应用侧的配置处理。
- Task、Root Task 与 WindowContainer 如何描述多窗口层级:辨认分屏两侧的逻辑容器树与 leash 边界。
源码入口
| 文件 | 关键位置 | 作用 |
|---|---|---|
| 见本章“关键源码”各节点 | 类名 / 方法名 | 证明主流程的关键动作 |
| Android 14 r75 对应源码文件 | 使用 rg -n 核对 | 发布前记录真实行号,避免跨 tag 漂移 |