18、多窗口与分屏模式:AMS对多窗口、自由窗口、画中画模式的管理

多窗口,这个功能现在大家用得挺多的。但说实话,在Android早期版本里,压根没这玩意儿。我记得Android 7.0刚引入分屏时,我还在做系统定制,客户第一句话就是:“这玩意儿稳定吗?”嗯,当时确实有不少坑。

今天我们就来聊聊,AMS到底是怎么管理这些窗口模式的。分屏、自由窗口、画中画,它们背后的逻辑是什么。

18.1 多窗口的三种形态

先理清楚概念。Android的多窗口,说白了就是三种形态:

  • 分屏模式:屏幕一分为二,两个App各占一半。最常见。
  • 自由窗口模式:窗口可以拖拽、缩放,像PC桌面一样。多见于平板或大屏设备。
  • 画中画模式:一个小窗口悬浮在其他应用之上。视频播放器最爱。

这三种模式,AMS是怎么区分的?其实核心就一个东西——TaskDisplayArea

核心概念:每个窗口模式,本质上对应一个不同的TaskDisplayArea。AMS通过管理这些Area,来控制窗口的布局和堆叠。

18.2 AMS中的窗口管理结构

我们来看一下AMS内部的数据结构。我画了一张图,帮你理清关系。

RootWindowContainer DisplayContent TaskDisplayArea (分屏) TaskDisplayArea (自由窗口) TaskDisplayArea (画中画) Task (App A) Task (App B) Task (自由窗口) Task (视频App) 图:AMS多窗口容器层级结构

你看,每个TaskDisplayArea管理一组Task。分屏模式下,两个Task并排显示。自由窗口模式下,Task可以任意移动。画中画模式下,Task被压缩成一个小窗口。

18.3 分屏模式的启动流程

分屏是怎么触发的?用户长按最近任务键,或者从Overview拖拽App。AMS收到请求后,会做这几件事:

  1. 检查设备是否支持:不是所有设备都能分屏。平板、折叠屏通常支持,但有些手机厂商会限制。
  2. 创建新的TaskDisplayArea:如果当前没有分屏Area,AMS会创建一个。
  3. 移动Task:把当前App的Task移到分屏Area中。
  4. 调整窗口大小:计算两个窗口的尺寸,通常是50:50,但用户可以拖动分割线调整。

核心代码在ActivityManagerServicestartActivity方法里。我简化一下关键逻辑:

// 伪代码:分屏启动流程
if (request.isSplitScreen()) {
    TaskDisplayArea splitArea = mRootWindowContainer
        .getDefaultDisplay()
        .getOrCreateSplitScreenPrimaryTaskDisplayArea();
    
    // 把目标Activity的Task移到分屏Area
    task.reparent(splitArea, ON_TOP);
    
    // 调整两个窗口的边界
    splitArea.setSplitScreenRatio(0.5f);
}

嗯,这里有个坑。我曾经遇到过一个Bug:分屏后,其中一个App的Activity生命周期乱了。原因是reparent时没有正确处理onPauseonResume。后来加了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状态流转图,帮你理解:

正常全屏 enterPiP 画中画模式 用户点击 恢复全屏 PiP销毁 用户关闭/App退出 图:画中画模式状态流转

你想想看,PiP模式下最麻烦的是什么?是焦点管理。PiP窗口虽然小,但它仍然是一个Activity。如果用户点击了PiP窗口,焦点要从下层App切换到PiP。AMS需要处理好这个切换,否则会出现点击无响应的情况。

18.6 多窗口下的生命周期管理

多窗口模式下,Activity的生命周期会变得复杂。举个例子:分屏时,两个App都可见,但只有一个处于活跃状态(有焦点)。另一个虽然可见,但处于onPause状态。

AMS是怎么判断哪个Activity该pause的?核心逻辑在ActivityStackresumeTopActivityInnerLocked方法里。它会遍历所有栈,找到最顶层的、可见的、有焦点的Activity,然后resume它。其他的都pause。

我记得有一次,客户反馈说分屏后视频App声音还在播放。这其实是正常的——因为onPause不代表App停止播放。但用户不理解。后来我们在系统设置里加了一个选项,允许用户选择“分屏时暂停后台播放”。

注意:多窗口模式下,不要依赖onStart/onStop来做资源释放。因为窗口可见但不一定活跃。建议使用onWindowFocusChanged来判断用户是否正在与你的App交互。

18.7 总结一下

多窗口管理,说白了就是AMS对TaskDisplayAreaTask的调度。分屏、自由窗口、画中画,本质上是三种不同的布局策略。AMS负责创建Area、移动Task、调整窗口大小、管理生命周期。

我个人觉得,理解多窗口的关键在于两点:一是容器层级结构,二是生命周期回调。把这两点搞清楚了,遇到多窗口相关的Bug,你就能快速定位问题。

嗯,今天就聊到这里。如果你在实际项目中遇到多窗口的坑,欢迎来交流。


公众号:蓝海资料掘金营,微信deep3321