WindowingMode、bounds 与 Configuration 如何让应用变成半屏
WindowingMode、bounds 与 Configuration 如何让应用变成半屏
问题场景
把应用拖成半屏时,改变的不是某个 View 的宽度,而是任务容器的窗口配置:windowingMode 表示窗口处于全屏、分屏、画中画等模式,bounds 表示该容器实际占用的矩形。ATMS/WMS 将它们合成为 Activity 的 Configuration;随后应用决定自己处理配置、重建 Activity,还是只让 View 树重新布局。
下面以 AOSP android-14.0.0_r75 为准,只追踪 organizer 通过 WindowContainerTransaction 修改既有容器后的配置分发。分屏配对策略、Shell transition 动画和 View 测量属于相邻链路,不在这里混讲。
核心结论
WindowingMode是模式状态,bounds是几何状态;分屏使用的WINDOWING_MODE_MULTI_WINDOW与容器边界都位于Configuration.windowConfiguration,但两者不能互相替代。WindowContainerTransaction.setBounds()写入变更掩码,setWindowingMode()单独记录请求;WindowOrganizerController在system_server应用它们,ConfigurationContainer向子树重新计算配置。- 配置到达
ActivityThread后,窗口模式“是否跨入/离开多窗口”才触发Activity.onMultiWindowModeChanged();仅拖动分屏分隔条通常不会因模式未变而触发它。 - 配置变化不等于必然重建:
ActivityRecord先按 manifest 声明判断能否处理;未处理的显著变化才 relaunch。无论是否重建,资源更新与ViewRootImpl的 relayout、Surface/transition 完成都是不同阶段。
先观察这张图:左半部是系统服务中配置的生成,右半部是应用收到配置后的三条可能结果。实线为同进程调用,粗箭头为 Binder/ClientTransaction,虚线为异步提交或绘制阶段。
关键转折是 ConfigurationContainer 处理的是容器配置树,ActivityThread 处理的是单个 Activity 的客户端状态。Transition/Surface 的提交可以与客户端配置分发同步,但“动画已完成”不是 onConfigurationChanged() 或 onMultiWindowModeChanged() 已返回的同义词。
主流程
阶段一:事务描述请求,而非立刻改屏幕
- 执行者:organizer 进程调用
WindowContainerTransaction;真正执行者是system_server的WindowOrganizerController。 - 输入:容器 token、目标 mode 或
Rect bounds。 - 动作:
setBounds()写入windowConfiguration,同时设置CONFIG_WINDOW_CONFIGURATION与WINDOW_CONFIG_BOUNDS;setWindowingMode()保存目标模式。 - 边界:
applyTransaction()经 Binder 进入system_server,并需要相应 task 管理权限;普通应用不能随意控制任意 Task。
阶段二:容器树重算 Configuration
WindowOrganizerController.applyChanges() 先合并可控配置;当同一事务同时改 bounds 和 mode 时,它会让 setWindowingMode() 触发统一的配置更新,避免把同一帧应一致的状态拆开。ConfigurationContainer.onRequestedOverrideConfigurationChanged() 调用 onConfigurationChanged(parentConfig),重算自身 full configuration 并向 Task、ActivityRecord 等孩子分发。可见 Activity 再由 ensureActivityConfiguration(true) 检查。
阶段三:服务端选择“发送配置”或“重建”
ActivityRecord.updateReportedConfigurationAndSend() 比较上次上报配置与当前配置。若没有需要 Activity 处理的显著差异,仍可调度 ActivityConfigurationChangeItem;若 shouldRelaunchLocked() 判定 manifest 未声明处理相应变化,则调用 relaunchActivityLocked()。因此不要把 android:configChanges 当作“禁止所有尺寸变化”的开关,它只影响这次差异是否由现有实例处理。
阶段四:应用先处理模式,再处理 Configuration
ActivityThread.performActivityConfigurationChanged() 明确先调用 handleWindowingModeChangeIfNeeded(),再更新 Resources 并调用 Activity.onConfigurationChanged()。前者比较旧、新 windowingMode 是否跨越多窗口边界;只有 WindowConfiguration.inMultiWindowMode(old) != ... (new) 才会分发 onMultiWindowModeChanged(boolean, Configuration)。也就是说,bounds 改变会影响尺寸、资源限定符和布局,却不保证有多窗口模式回调。
关键源码
节点一:WCT 标记 mode 与 bounds
- 类/方法:
WindowContainerTransaction.setBounds()、setWindowingMode() - 路径:
frameworks/base/core/java/android/window/WindowContainerTransaction.java - 进程/线程:调用方进程;随后通过 Binder 进入
system_server。
阅读问题:bounds 为什么是配置变更,而 mode 又为何单独存储?
// 教程注释:AOSP r75 节选;bounds 同时带上配置和 WindowConfiguration 掩码。
public WindowContainerTransaction setBounds(WindowContainerToken container, Rect bounds) {
Change chg = getOrCreateChange(container.asBinder());
chg.mConfiguration.windowConfiguration.setBounds(bounds);
chg.mConfigSetMask |= ActivityInfo.CONFIG_WINDOW_CONFIGURATION;
chg.mWindowSetMask |= WindowConfiguration.WINDOW_CONFIG_BOUNDS;
return this;
}
public WindowContainerTransaction setWindowingMode(WindowContainerToken container, int mode) {
Change chg = getOrCreateChange(container.asBinder());
chg.mWindowingMode = mode;
return this;
}结论:前者是 Configuration 内的几何 override,后者是待由控制器应用的模式请求;两者可以同属一次事务,但语义不同。
节点二:ConfigurationContainer 让变化向下传播
- 类/方法:
ConfigurationContainer.onRequestedOverrideConfigurationChanged()、setBounds() - 路径:
frameworks/base/services/core/java/com/android/server/wm/ConfigurationContainer.java - 进程/线程:
system_server,由WindowOrganizerController的全局锁路径调用。
// 教程注释:AOSP r75 节选;更新 requested override 后立即重算 full config。
public void onRequestedOverrideConfigurationChanged(Configuration overrideConfiguration) {
updateRequestedOverrideConfiguration(overrideConfiguration);
final ConfigurationContainer parent = getParent();
onConfigurationChanged(parent != null ? parent.getConfiguration() : Configuration.EMPTY);
}
public int setBounds(Rect bounds) {
// 省略无关代码
mRequestsTmpConfig.windowConfiguration.setBounds(bounds);
onRequestedOverrideConfigurationChanged(mRequestsTmpConfig);
return boundsChange;
}结论:Task/ActivityRecord 不必各自“猜测半屏”;它们从父容器配置树取得重算后的结果。接下来 ActivityRecord 决定上报配置还是重启。
节点三:客户端的回调顺序与重建边界
- 类/方法:
ActivityThread.performActivityConfigurationChanged()、handleWindowingModeChangeIfNeeded() - 路径:
frameworks/base/core/java/android/app/ActivityThread.java - 进程/线程:应用进程主线程,
ActivityConfigurationChangeItem执行后进入。
// 教程注释:AOSP r75 节选;模式回调先于 Configuration 回调。
handleWindowingModeChangeIfNeeded(r, newConfig);
// 省略 ResourcesManager 更新和差异判断
activity.mCurrentConfig = new Configuration(newConfig);
activity.onConfigurationChanged(configToReport);
if (wasInMultiWindowMode != nowInMultiWindowMode) {
activity.dispatchMultiWindowModeChanged(nowInMultiWindowMode, newConfiguration);
}结论:dispatchMultiWindowModeChanged() 最终调用 Activity.onMultiWindowModeChanged(boolean, Configuration);它是模式边界通知,不是“窗口每次变宽都通知”的 API。Activity 已被服务端决定 relaunch 时,本段则属于新实例启动路径,不能期待旧实例靠该回调保存状态。
验证与排障
目标:在可分屏的 Android 14 设备上观察 Task bounds、windowing mode 与 Activity 回调的区别。
环境与权限:主机已连接 adb 设备;设备和目标 Activity 支持分屏。dumpsys activity 可由 shell 使用;读取详细 WindowManager 状态在量产设备上可能受版本或权限限制。
# 主机终端执行:先进入分屏,再拖动分隔条,记录目标包名。
adb shell dumpsys activity activities | grep -E 'mResumedActivity|windowingMode|bounds'
adb logcat -c
adb logcat -v time ActivityTaskManager:V WindowManager:V *:S预期:dumpsys 中目标 Task/Activity 的 bounds 或 windowingMode 会随分屏状态变化。若应用在两个模式之间进入或离开多窗口,可在应用自己的 onMultiWindowModeChanged 日志看到一次布尔值转换;只拖分隔条时,更可靠的观察是 bounds、onConfigurationChanged 与 View 尺寸变化。
异常方向:没有配置变化先核对目标是否真的在分屏 Task;没有回调检查是否只改了 bounds、Activity 是否被重建以及 manifest 的 configChanges;布局未更新则在应用主线程检查 onConfigurationChanged 后的资源/根 View 尺寸,而不是把问题归因于 Surface transition。
限制:dumpsys 证明当前服务端状态,不能单独证明客户端回调顺序;该顺序以本篇列出的 ActivityThread 源码为准。厂商分屏策略和 Shell 动画日志可能不同。
常见误区
- 误区:进入半屏只等于
bounds变小。- 问题:忽略了 mode 与 bounds 是不同字段;某些 resize 不跨越多窗口模式。
- 正确理解:分别读取
windowConfiguration.getWindowingMode()与 bounds,并按是否跨模式判断回调。
- 误区:收到
onMultiWindowModeChanged()就代表动画和 Surface 已完成。- 问题:客户端配置分发、View relayout、BLAST/transition 的 Surface 提交是不同阶段。
- 验证方式:同时对照应用回调日志、
dumpsys与 WindowManager/transition 观察点。
- 误区:所有 Configuration 变化都会重建 Activity。
- 正确理解:
ActivityRecord.shouldRelaunchLocked()根据差异和 manifest 决策;客户端处理时走回调,未处理时走 relaunch。
- 正确理解:
延伸阅读
- 多窗口层级篇:理解 DisplayArea、Root Task、Task 与 ActivityRecord 如何承载这里的配置树。
- WindowOrganizer 篇:继续追踪谁有权限组装和提交
WindowContainerTransaction。 - 分屏主教程:从用户分屏动作、Shell 组织到本文的配置分发建立全链路地图。
源码入口
| 文件 | 关键位置 | 作用 |
|---|---|---|
| 见本章“关键源码”各节点 | 类名 / 方法名 | 证明主流程的关键动作 |
| Android 14 r75 对应源码文件 | 使用 rg -n 核对 | 发布前记录真实行号,避免跨 tag 漂移 |