0. 理解图层
(图片来自 www.jianshu.com/p/b0ef7c044...
在很多的图形相关的软件中都有图层的概念,那什么是图层呢?
简单的说,可以将每个图层理解为一张透明的纸,将图像的各部分绘制在不同的透明纸(图层)上。透过这层纸,可以看到纸后面的东西,而且每层纸都是独立的,无论在这层纸上如何涂画,都不会影响到其他图层中的图像。也就是说,每个图层可以独立编辑或修改,最后将透明纸叠加起来,从上向下俯瞰,即可得到并实时改变最终的合成效果。
1. 显示界面的组成与描述
在 Android 中,一个显示界面由多个窗口(Window)组成。
从应用侧看:
- statusbar 占据一个窗口
- navigationbar 占据一个窗口
- Activity 占据一个窗口
- Wallpaper,也就是壁纸占据一个窗口
- ......
窗口都有一个 Z 轴高度的属性,高的窗口会盖在低的窗口之上,WallPaper 的高度最低,Activity 其次,statusbar 和 navigationbar 最高。
实际的窗口会多一些,我们可以通过 adb shell dumpsys window w
命令来查看窗口信息。
bash
Window #0 Window{c5e43 u0 ScreenDecorOverlayBottom}:
# ......
Window #1 Window{bc07d71 u0 ScreenDecorOverlay}:
# ......
Window #2 Window{7162bcd u0 NavigationBar0}:
# ......
Window #3 Window{166a760 u0 NotificationShade}:
# ......
Window #4 Window{59378d5 u0 StatusBar}:
# ......
Window #5 Window{3ade79d u0 ShellDropTarget}:
# ......
Window #6 Window{b3af08a u0 InputMethod}:
# ......
Window #7 Window{70a1ff6 u0 com.android.launcher3/com.android.launcher3.uioverrides.QuickstepLauncher}:
# ......
Window #8 Window{3dc41e3 u0 com.android.systemui.wallpapers.ImageWallpaper}:
# ......
从系统侧看
Android11 以后,WMS/AMS 中使用 WindowContainer 对象来描述一块显示区域,使用 WindowContainer 组成的树来描述整个显示界面。为方便叙述,本文称这棵树为窗口容器树
仅从便于理解的角度来看,树的结构大致如下:
(注意,这是根据代码分析分析拼凑出的一张图,不准确,仅用于学习理解)
图片看着有点抽象,接下来我们一步步分析每个节点的类型即可看懂这张图了。
2. 窗口容器树节点分析
窗口容器树的每一个节点都是窗口容器(WindowContainor)的子类。
我们可以通过 adb shell dumpsys activity containers
命令查看整个窗口容器树的描述。(太长了,就不贴出来了)
2.1 WindowContainor ------ 树节点的公共父类
我们先看看 WindowContainor 类的定义:
java
// frameworks/base/services/core/java/com/android/server/wm/WindowContainer.java
/**
* Defines common functionality for classes that can hold windows directly or through their
* children in a hierarchy form.
* The test class is {@link WindowContainerTests} which must be kept up-to-date and ran anytime
* changes are made to this class.
*/
class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<E>
implements Comparable<WindowContainer>, Animatable, SurfaceFreezer.Freezable {
// ......
}
类的注释直接翻译过来是:WindowContainer 定义了能够直接或者间接以层级结构的形式持有窗口的类的通用功能。
这个定义可以说是相当抽象了,用人话说就是,窗口容器树的所有节点都是 WindowContainer 子类。WindowContainer 中定义了这些节点通用的成员变量和成员方法。
我们需要重点关注的是 WindowContainer 的以下两个成员:
java
/**
* The parent of this window container.
* For removing or setting new parent {@link #setParent} should be used, because it also
* performs configuration updates based on new parent's settings.
*/
private WindowContainer<WindowContainer> mParent = null;
// ......
// List of children for this window container. List is in z-order as the children appear on
// screen with the top-most window container at the tail of the list.
protected final WindowList<E> mChildren = new WindowList<E>();
- mParent 成员变量的类型是 WindowContainer,保存的是当前 WindowContainer 的父节点的引用
- WindowList 是 ArrayList 的子类, mChildren 成员变量保存的是当前 WindowContainer 持有的所有子节点,列表的顺序同时也是子节点出现在屏幕上的顺序,最顶层的子容器位于队尾
2.2 WindowState ------ 窗口类
在 WMS/AMS 中,一个 WindowState 对象就代表了一个窗口,通常位于树的最底层。可以将屏幕理解为一张画布,WindowState 就是其中的一个图层。
java
/** A window in the window manager. */
class WindowState extends WindowContainer<WindowState> implements WindowManagerPolicy.WindowState,
InsetsControlTarget, InputTarget {
// ......
}
我们可以从 adb shell dumpsys window w
命令的输出结构中找到 WindowState 的描述:
less
# NavigationBar 的描述
#0 7162bcd NavigationBar0 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1280]
# StatusBar 的描述
#0 59378d5 StatusBar type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1280]
# Launcher 页面的描述
#0 70a1ff6 com.android.launcher3/com.android.launcher3.uioverrides.QuickstepLauncher type=home mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1280]
# ......
2.3 WindowToken、ActivityRecord 和 WallpaperWindowToken ------ WindowState 的父节点
WindowState 的父节点有三类 WindowToken、ActivityRecord 和 WallpaperWindowToken。
WindowToken
java
// frameworks/base/services/core/java/com/android/server/wm/WindowToken.java
/**
* Container of a set of related windows in the window manager. Often this is an AppWindowToken,
* which is the handle for an Activity that it uses to display windows. For nested windows, there is
* a WindowToken created for the parent window to manage its children.
*/
class WindowToken extends WindowContainer<WindowState> {
// ......
}
WindowToken 是 WMS 中用于保存一组相关窗口(WindowState)的容器类。
在 WMS/AMS 中,WindowToken 继承自 WindowContainer<WindowState>, 是 WindowState 的容器,其父类内部成员 WindowList<E> mChildren
保存了 WindowToken 的 WindowState 子节点们。
我们称 WindowToken 是 WindowState 的父容器或者父节点。
我们可以从 adb shell dumpsys window w
命令的输出结构中找到 WindowToken 的描述:
less
#0 WindowToken{77ed98c type=2000 android.os.BinderProxy@2667cde} type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1280]
#0 59378d5 StatusBar type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1280]
# ......
ActivityRecord
java
/**
* An entry in the history task, representing an activity.
*/
final class ActivityRecord extends WindowToken implements WindowManagerService.AppFreezeListener {
// ......
}
在 WMS/AMS 中一个 ActivityRecord 对象用于描述 App 端的一个 Activity 对象。同时 ActivityRecord 继承自 WindowToken,和 WindowToken 一样, ActivityRecord 是 WindowState 的容器,其内部成员 WindowList<E> mChildren
保存了 WindowToken 的 WindowState 子节点们。
ActivityRecord 和 WindowToken 的子节点都是 WindowState,那它们的区别是什么呢?
在了解两者的区别之前,我们先了解一下窗口的类型。根据窗口的添加方式可以将窗口分为 Activity 窗口和非 Activity 窗口:
- Activity 窗口由系统自动创建,不需要 App 主动去调用 ViewManager.addView 去添加一个窗口,比如写一个Activity 或者 Dialog,系统就会在合适的时机为 Activity 或者 Dialog 调用 ViewManager.addView 去向 WindowManager 添加一个窗口。这类窗口在创建的时候,其父节点为 ActivityRecord。
- 非 Activity 窗口,这类窗口需要 App 主动去调用 ViewManager.addView 来添加一个窗口,比如 NavigationBar 窗口的添加,需要 SystemUI 主动去调用 ViewManager.addView 来为 NavigationBar 创建一个新的窗口。这类窗口在创建的时候,其父节点为 WindowToken。
我们可以从 adb shell dumpsys window w
命令的输出结构中找到 ActivityRecord 的描述:
less
#0 ActivityRecord{8076c53 u0 com.android.launcher3/.uioverrides.QuickstepLauncher t7} type=home mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1280]
#0 70a1ff6 com.android.launcher3/com.android.launcher3.uioverrides.QuickstepLauncher type=home mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1280]
WallpaperWindowToken
java
/**
* A token that represents a set of wallpaper windows.
*/
class WallpaperWindowToken extends WindowToken {
// ......
}
WallpaperWindowToke 继承自 WindowToken,是 Wallpaper 相关的窗口的父节点。
上面讨论 ActivityRecord 的时候,我们将窗口划分为 Activity 窗口和非 Activity 窗口。在引入了 WallpaperWindowToken 后,我们继续将非 Activity 窗口划分为两类,Wallpaper 窗口和非 Wallpaper 窗口。Wallpaper 窗口的父节点都是 WallpaperWindowToken 类型的。
Wallpaper 窗口的层级是比 Activity 窗口的层级低的,因此这里我们可以按照层级这一角度将窗口划分为:
Activity 之上的窗口,父节点为 WindowToken,如 StatusBar 和 NavigationBar。 Activity 窗口,父节点为 ActivityRecord,如 Launcher。 Activity 之下的窗口,父节点为 WallpaperWindowToken,如 ImageWallpaper 窗口。
我们可以从 adb shell dumpsys window w
命令的输出结构中找到 WallpaperWindowToken 的描述:
less
#0 WallpaperWindowToken{b8d832d token=android.os.Binder@43bb644} type=undefined mode=fullscreen override-mode=fullscreen requested-bounds=[0,0][0,0] bounds=[0,0][720,1280]
#0 3dc41e3 com.android.systemui.wallpapers.ImageWallpaper type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1280]
2.4 Task ------ ActivityRecord 的父节点
java
class Task extends TaskFragment {
}
class TaskFragment extends WindowContainer<WindowContainer> {
}
一个 Task 对象就代表了一个任务栈,内部保存了一组相同 affinities 属性的相关 Activity,这些 Activity 用于执行一个特定的功能。比如发送短信,拍摄照片等。
关于任务栈的内容可以参考官方文档任务和返回堆栈。
Taks 继承自 TaskFragment,TaskFragment 继承自 WindowContainer。除了上述的功能,在我们描述的窗口容器树中, Task 是 ActivityRecord 的父节点,内部管理有多个 ActivityRecord 对象。
我们可以从 adb shell dumpsys window w
命令的输出结构中找到 Task 的描述:
less
#0 Task=7 type=home mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1280]
#0 ActivityRecord{8076c53 u0 com.android.launcher3/.uioverrides.QuickstepLauncher t7} type=home mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1280]
#0 70a1ff6 com.android.launcher3/com.android.launcher3.uioverrides.QuickstepLauncher type=home mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1280]
2.5 DisplayArea ------ 一块显示区域
java
/**
* Container for grouping WindowContainer below DisplayContent.
*
* DisplayAreas are managed by a {@link DisplayAreaPolicy}, and can override configurations and
* can be leashed.
*
* DisplayAreas can contain nested DisplayAreas.
*
* DisplayAreas come in three flavors, to ensure that windows have the right Z-Order:
* - BELOW_TASKS: Can only contain BELOW_TASK DisplayAreas and WindowTokens that go below tasks.
* - ABOVE_TASKS: Can only contain ABOVE_TASK DisplayAreas and WindowTokens that go above tasks.
* - ANY: Can contain any kind of DisplayArea, and any kind of WindowToken or the Task container.
*
* @param <T> type of the children of the DisplayArea.
*/
public class DisplayArea<T extends WindowContainer> extends WindowContainer<T> {
// ......
private final String mName;
// ......
}
DisplayArea 代表了屏幕上的一块区域。
DisplayArea 中有一个字符串成员 mName,表示 DisplayArea 对象的名字,其内容由三部分组成 name + ":" + mMinLayer + ":" + mMaxLayer
。其中:
- name:用于指定 DisplayArea 的特殊功能,如:name 的值为 "WindowedMagnification" 表示 DisplayArea 代表的屏幕区域支持窗口放大。如果没有特殊功能且是叶子节点,name 的值为 "leaf"
- mMinLayer 和 mMaxLayer,指定当前 DisplayArea 的图层高度范围,WMS 将 Z 轴上的纵向空间分成了 0 到 36 一共 37 个区间,值越大代表图层高度越高,这里两个值,指定了图层高度的范围区间。
实际上我们可以把屏幕显示的内容看做一个三维空间:
注意这里的 x y z 轴的位置,不同于一般的三维图。
这里的每一个矩形就是一个窗口(WindowState),我们可以将其理解为一个图层。
我们将 Z 轴分为 0 到 36, 一共 37 个连续的区间。
DisplayArea 指定了一块显示区域,这里的显示区域包含了 x y z 三个方向上的区域。Z 轴的区域通过内部 mName 成员中保存的两个整形变量 mMinLayer 和 mMaxLayer 指定。比如 mMinLayer = 1, mMaxLayer =3
,那么当前 DisplayArea 对象在 Z 轴方向上占据了 1 到 3,3 个区间的纵向空间。
DisplayArea 有三个子类 TaskDisplayArea,DisplayArea.Tokens 和 DisplayArea.Dimmable。DisplayArea.Tokens 有一个子类 DisplayContent.ImeContainer。DisplayArea.Dimmable 有一个子类 RootDisplayArea
更清晰的继承关系可以参考以下的类图:
接下来,我们来看看 DisplayArea 的每一个子类:
TaskDisplayArea
java
/**
* {@link DisplayArea} that represents a section of a screen that contains app window containers.
*
* The children can be either {@link Task} or {@link TaskDisplayArea}.
*/
final class TaskDisplayArea extends DisplayArea<WindowContainer> {
// ......
}
TaskDisplayArea,继承自 DisplayArea,代表了屏幕上一块专门用来存放 App 窗口的区域。它的子容器通常是 Task。
我们可以从 adb shell dumpsys window w
命令的输出结构中找到 TaskDisplayArea 的描述:
less
#1 DefaultTaskDisplayArea type=undefined mode=fullscreen override-mode=fullscreen requested-bounds=[0,0][0,0] bounds=[0,0][720,1280]
#1 Task=1 type=home mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1280]
#0 Task=7 type=home mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1280]
#0 ActivityRecord{8076c53 u0 com.android.launcher3/.uioverrides.QuickstepLauncher t7} type=home mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1280]
#0 70a1ff6 com.android.launcher3/com.android.launcher3.uioverrides.QuickstepLauncher type=home mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1280]
DisplayArea.Tokens
java
/**
* DisplayArea that contains WindowTokens, and orders them according to their type.
*/
public static class Tokens extends DisplayArea<WindowToken> {
}
Tokens 是 DisplayArea 的内部类,继承自 DisplayArea,代表了屏幕上一块专门用来存放非 App 窗口的区域。它的子容器通常是 WindowToken。
我们可以从 adb shell dumpsys window w
命令的输出结构中找到 DisplayArea.Tokens 的描述:
less
#0 Leaf:15:15 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1280]
#0 WindowToken{77ed98c type=2000 android.os.BinderProxy@2667cde} type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1280]
#0 59378d5 StatusBar type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1280]
这里的 Leaf 的类型就是 DisplayArea.Tokens
DisplayContent.ImeContainer
java
/**
* Container for IME windows.
*
* This has some special behaviors:
* - layers assignment is ignored except if setNeedsLayer() has been called before (and no
* layer has been assigned since), to facilitate assigning the layer from the IME target, or
* fall back if there is no target.
* - the container doesn't always participate in window traversal, according to
* {@link #skipImeWindowsDuringTraversal()}
*/
private static class ImeContainer extends DisplayArea.Tokens {
}
ImeContainer 是 DisplayContent 类的内部类,继承自 DisplayArea.Tokens,是存放输入法窗口的容器。
我们可以从 adb shell dumpsys window w
命令的输出结构中找到 ImeContainer 的描述:
less
#0 ImeContainer type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1280]
#0 WindowToken{82b005b type=2011 android.os.Binder@a85536a} type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1280]
#0 b3af08a InputMethod type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1280]
DisplayArea.Dimmable
java
/**
* DisplayArea that can be dimmed.
*/
static class Dimmable extends DisplayArea<DisplayArea> {
// ......
}
Dimmable 也是 DisplayArea 的内部类,从名字可以看出,这类的 DisplayArea 可以添加模糊效果。
它内部有一个Dimmer对象:
java
private final Dimmer mDimmer = new Dimmer(this);
可以通过 Dimmer 对象施加模糊效果,模糊图层可以插入到以该 Dimmable 对象为根节点的层级结构之下的任意两个图层之间。
RootDisplayArea
java
/**
* Root of a {@link DisplayArea} hierarchy. It can be either the {@link DisplayContent} as the root
* of the whole logical display, or a {@link DisplayAreaGroup} as the root of a partition of the
* logical display.
*/
class RootDisplayArea extends DisplayArea.Dimmable {
// ......
}
RootDisplayArea,是一个DisplayArea 层级结构的根节点。
它可以是:
DisplayContent,作为整个屏幕的 DisplayArea 层级结构根节点。 DisplayAreaGroup,作为屏幕上部分区域对应的 DisplayArea 层级结构的根节点。
这又引申出了 RootDisplayArea 的两个子类,DisplayContent 和 DisplayAreaGroup。
DisplayContent
java
/**
* Utility class for keeping track of the WindowStates and other pertinent contents of a
* particular Display.
*/
@TctAccess(scope = TctScope.PUBLIC)
class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.DisplayContentInfo {
// ......
}
DisplayContent,代表了一个实际的屏幕,作为一个屏幕上的 DisplayArea 层级结构的根节点。
DisplayAreaGroup
java
/** The root of a partition of the logical display. */
class DisplayAreaGroup extends RootDisplayArea {
}
DisplayAreaGroup,屏幕上的部分区域对应的 DisplayArea 层级结构的根节点。什么意思呢?
(图片来自 blog.csdn.net/shensky711/...
我们可以创建两个 DisplayAreaGroup 并将屏幕一分为二分别放置这两个,这两个区域都是可以作为应用容器的,和分屏不一样的是,这两块区域可以有不同的 Feature 规则以及其他特性。这个用得比较少,了解一下即可
3. 总结
在了解了这些类以后,在看文章开头给出的窗口层级图,就容易不少了: