18、多窗口与分屏模式:AMS对多窗口、自由窗口、画中画模式的管理
多窗口,这个功能现在大家用得挺多的。但说实话,在Android早期版本里,压根没这玩意儿。我记得Android 7.0刚引入分屏时,我还在做系统定制,客户第一句话就是:“这玩意儿稳定吗?”嗯,当时确实有不少坑。
今天我们就来聊聊,AMS到底是怎么管理这些窗口模式的。分屏、自由窗口、画中画,它们背后的逻辑是什么。
18.1 多窗口的三种形态
先理清楚概念。Android的多窗口,说白了就是三种形态:
- 分屏模式:屏幕一分为二,两个App各占一半。最常见。
- 自由窗口模式:窗口可以拖拽、缩放,像PC桌面一样。多见于平板或大屏设备。
- 画中画模式:一个小窗口悬浮在其他应用之上。视频播放器最爱。
这三种模式,AMS是怎么区分的?其实核心就一个东西——TaskDisplayArea。
核心概念:每个窗口模式,本质上对应一个不同的TaskDisplayArea。AMS通过管理这些Area,来控制窗口的布局和堆叠。
18.2 AMS中的窗口管理结构
我们来看一下AMS内部的数据结构。我画了一张图,帮你理清关系。
你看,每个TaskDisplayArea管理一组Task。分屏模式下,两个Task并排显示。自由窗口模式下,Task可以任意移动。画中画模式下,Task被压缩成一个小窗口。
18.3 分屏模式的启动流程
分屏是怎么触发的?用户长按最近任务键,或者从Overview拖拽App。AMS收到请求后,会做这几件事:
- 检查设备是否支持:不是所有设备都能分屏。平板、折叠屏通常支持,但有些手机厂商会限制。
- 创建新的TaskDisplayArea:如果当前没有分屏Area,AMS会创建一个。
- 移动Task:把当前App的Task移到分屏Area中。
- 调整窗口大小:计算两个窗口的尺寸,通常是50:50,但用户可以拖动分割线调整。
核心代码在ActivityManagerService的startActivity方法里。我简化一下关键逻辑:
// 伪代码:分屏启动流程
if (request.isSplitScreen()) {
TaskDisplayArea splitArea = mRootWindowContainer
.getDefaultDisplay()
.getOrCreateSplitScreenPrimaryTaskDisplayArea();
// 把目标Activity的Task移到分屏Area
task.reparent(splitArea, ON_TOP);
// 调整两个窗口的边界
splitArea.setSplitScreenRatio(0.5f);
}
嗯,这里有个坑。我曾经遇到过一个Bug:分屏后,其中一个App的Activity生命周期乱了。原因是reparent时没有正确处理onPause和onResume。后来加了moveTaskToFront的调用才解决。
18.4 自由窗口模式
自由窗口,说白了就是让App像桌面软件一样悬浮。这个模式在Android 10之后才正式开放给普通应用。之前只有系统应用能用。
自由窗口的核心是窗口边界管理。AMS需要跟踪每个窗口的位置、大小、堆叠顺序。这些信息存在WindowState里。
| 属性 | 说明 | 示例值 |
|---|---|---|
| mFrame | 窗口在屏幕上的位置和大小 | Rect(100, 200, 600, 800) |
| mStackId | 所属的栈ID | FREEFORM_WORKSPACE_STACK_ID |
| mDragResizing | 是否正在拖拽缩放 | true/false |
自由窗口的拖拽,其实是AMS和WMS(WindowManagerService)配合完成的。用户拖拽时,WMS负责更新窗口位置,AMS负责调整Activity的配置。我建议你重点关注resizeStack这个方法,它是自由窗口的核心。
避坑指南:自由窗口模式下,App的onConfigurationChanged会被频繁调用。如果你的App没有处理好配置变化,很容易出现界面错乱。我曾经见过一个App,在自由窗口下拖拽时,界面直接白屏了——因为它没有实现onConfigurationChanged,导致Activity被重建了无数次。
18.5 画中画模式
画中画(PiP)是视频类App的标配。它的实现和分屏、自由窗口不太一样。PiP窗口其实是一个特殊的Activity,它被放在一个独立的Task里,这个Task的栈ID是PIP_STACK_ID。
AMS对PiP的管理,有几个关键点:
- 进入PiP:App调用
enterPictureInPictureMode,AMS把当前Activity的Task移到PiP栈。 - 窗口大小:PiP窗口通常很小,比如240x135。AMS会强制设置这个尺寸。
- 触摸事件:PiP窗口可以响应点击、拖拽。但触摸事件需要穿透到下层App。
- 生命周期:进入PiP后,原Activity会调用
onPictureInPictureModeChanged,但不会销毁。
我画了一张PiP状态流转图,帮你理解:
你想想看,PiP模式下最麻烦的是什么?是焦点管理。PiP窗口虽然小,但它仍然是一个Activity。如果用户点击了PiP窗口,焦点要从下层App切换到PiP。AMS需要处理好这个切换,否则会出现点击无响应的情况。
18.6 多窗口下的生命周期管理
多窗口模式下,Activity的生命周期会变得复杂。举个例子:分屏时,两个App都可见,但只有一个处于活跃状态(有焦点)。另一个虽然可见,但处于onPause状态。
AMS是怎么判断哪个Activity该pause的?核心逻辑在ActivityStack的resumeTopActivityInnerLocked方法里。它会遍历所有栈,找到最顶层的、可见的、有焦点的Activity,然后resume它。其他的都pause。
我记得有一次,客户反馈说分屏后视频App声音还在播放。这其实是正常的——因为onPause不代表App停止播放。但用户不理解。后来我们在系统设置里加了一个选项,允许用户选择“分屏时暂停后台播放”。
注意:多窗口模式下,不要依赖onStart/onStop来做资源释放。因为窗口可见但不一定活跃。建议使用onWindowFocusChanged来判断用户是否正在与你的App交互。
18.7 总结一下
多窗口管理,说白了就是AMS对TaskDisplayArea和Task的调度。分屏、自由窗口、画中画,本质上是三种不同的布局策略。AMS负责创建Area、移动Task、调整窗口大小、管理生命周期。
我个人觉得,理解多窗口的关键在于两点:一是容器层级结构,二是生命周期回调。把这两点搞清楚了,遇到多窗口相关的Bug,你就能快速定位问题。
嗯,今天就聊到这里。如果你在实际项目中遇到多窗口的坑,欢迎来交流。
公众号:蓝海资料掘金营,微信deep3321