专栏名称: 鸿洋
你好,欢迎关注鸿洋的公众号,每天为您推送高质量文章,让你每天都能涨知识。点击历史消息,查看所有已推送的文章,喜欢可以置顶本公众号。此外,本公众号支持投稿,如果你有原创的文章,希望通过本公众号发布,欢迎投稿。
目录
相关文章推荐
stormzhang  ·  时间不多了 ·  14 小时前  
鸿洋  ·  Android14 WMS/AMS ... ·  23 小时前  
stormzhang  ·  真正该刺激的是收入 ·  昨天  
鸿洋  ·  一款高效的HarmonyOS工具包 ·  昨天  
鸿洋  ·  Android多渠道打包指南 ·  6 天前  
51好读  ›  专栏  ›  鸿洋

Android14 WMS/AMS 窗口层级结构解析

鸿洋  · 公众号  · android  · 2024-09-19 08:35

正文

理解图层

 

(图片来自 https://www.jianshu.com/p/b0ef7c04486d)

在很多的图形相关的软件中都有图层的概念,那什么是图层呢?

简单的说,可以将每个图层理解为一张透明的纸,将图像的各部分绘制在不同的透明纸(图层)上。透过这层纸,可以看到纸后面的东西,而且每层纸都是独立的,无论在这层纸上如何涂画,都不会影响到其他图层中的图像。也就是说,每个图层可以独立编辑或修改,最后将透明纸叠加起来,从上向下俯瞰,即可得到并实时改变最终的合成效果。

1
显示界面的组成与描述

在 Android 中,一个显示界面由多个窗口(Window)组成。

 

从应用侧看

  • statusbar 占据一个窗口
  • navigationbar 占据一个窗口
  • Activity 占据一个窗口
  • Wallpaper,也就是壁纸占据一个窗口
  • ......

如果屏幕是一张画布,那么一个个窗口,就是一个个图层,不同的图层有不同的高度。系统将不同的界面绘制到不同的图层上,最后将图层叠加起来,从上向下俯瞰,就是最终显示到屏幕上的样子。

窗口都有一个 Z 轴高度的属性,高的窗口会盖在低的窗口之上,WallPaper 的高度最低,Activity 其次,statusbar 和 navigationbar 最高。

从系统侧看

Android11 以后,WMS/AMS 中使用 WindowContainer 对象来描述一块显示区域,使用 WindowContainer 组成的树来描述整个显示界面。为方便叙述,本文称这棵树为窗口容器树。

仅从便于理解的角度来看,树的结构大致如下:

(注意,这是根据代码分析拼凑出的一张图,不准确,仅用于学习理解)

这颗树的叶子节点 WindowState 就对应了 App 端的窗口。

实际的 WindowState/窗口 会多一些,我们可以通过 adb shell dumpsys window w 命令来查看到相关信息。

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}:
# ......

图片看着有点抽象,接下来我们一步步分析每个节点的类型即可看懂这张图了。

2
窗口容器树节点分析

窗口容器树的每一个节点都是窗口容器(WindowContainor)的子类。

我们可以通过 adb shell dumpsys activity containers 命令查看整个窗口容器树的描述。(太长了,就不贴出来了)

2.1 WindowContainor —— 树节点的公共父类

我们先看看 WindowContainor 类的定义:

// 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 WindowContainerextends ConfigurationContainer<E>
        implements Comparable<WindowContainer>, AnimatableSurfaceFreezer.Freezable 
{

    // ......
}

类的注释直接翻译过来是:WindowContainer 定义了能够直接或者间接以层级结构的形式持有窗口的类的通用功能。

这个定义可以说是相当抽象了,用人话说就是,窗口容器树的所有节点都是 WindowContainer 子类。WindowContainer 中定义了这些节点通用的成员变量和成员方法。

我们需要重点关注的是 WindowContainer 的以下两个成员:

    /**
     * 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 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 mChildren = new WindowList();

  • mParent 成员变量的类型是 WindowContainer,保存的是当前 WindowContainer 的父节点的引用。
  • WindowList 是 ArrayList 的子类, mChildren 成员变量保存的是当前 WindowContainer 持有的所有子节点,列表的顺序同时也是子节点出现在屏幕上的顺序,最顶层的子容器位于队尾。

2.2 WindowState —— 窗口类

在 WMS/AMS 中,一个 WindowState 对象对应一个应用侧的窗口,通常位于树的最底层。可以将屏幕理解为一张画布,WindowState 就是其中的一个图层。

/** A window in the window manager. */
class WindowState extends WindowContainer<WindowStateimplements WindowManagerPolicy.WindowState,
        InsetsControlTargetInputTarget 
{
            // ......
}

我们可以从 adb shell dumpsys window w 命令的输出结构中找到 WindowState 的描述:

# 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

// 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 的容器,其父类内部成员 WindowList mChildren 保存了 WindowToken 的 WindowState 子节点们。

在窗口容器树中,WindowToken 是 WindowState 的父容器或者父节点。

我们可以从 adb shell dumpsys window w 命令的输出结构中找到 WindowToken 的描述:

       #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]
        # 下面就是 WindowToken 的 WindowToken 子节点们
        #0 59378d5 StatusBar type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1280]
    
        # ......

ActivityRecord

/**
 * 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 mChildren 保存了 WindowToken 的 WindowState 子节点们。

ActivityRecord 和 WindowToken 的子节点都是 WindowState,那它们的区别是什么呢?

在了解两者的区别之前,我们先了解一下窗口的类型。根据窗口的添加方式可以将窗口分为 Activity 窗口和非 Activity 窗口:

  • Activity 窗口由系统自动创建,不需要 App 主动去调用 ViewManager.addView 去添加一个窗口,比如写一个Activity 或者 Dialog,系统就会在合适的时机为 Activity 或者 Dialog 调用 ViewManager.addView 去向 WindowManager 添加一个窗口。这类 WindowState 在创建的时候,其父节点为 ActivityRecord。
  • 非 Activity 窗口,这类窗口需要 App 主动去调用 ViewManager.addView 来添加一个窗口,比如 NavigationBar 窗口的添加,需要 SystemUI 主动去调用 ViewManager.addView 来为 NavigationBar 创建一个新的窗口。这类 WindowState 在创建的时候,其父节点为 WindowToken。

我们可以从 adb shell dumpsys window w 命令的输出结构中找到 ActivityRecord 的描述:

          #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]
           # 后面就是 ActivityRecord 的 WindowState 子节点们了
           #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

/**
 * A token that represents a set of wallpaper windows.
 */

class WallpaperWindowToken extends WindowToken {
    // ......
}

WallpaperWindowToke 继承自 WindowToken,是 Wallpaper 相关的 WindowState 的父节点。

上面讨论 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 的描述:

        #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 的父节点

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 的描述:

         #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 —— 一块显示区域

/**
 * 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  type of the children of the DisplayArea.
 */

public class DisplayArea<T extends WindowContainerextends WindowContainer<T{
    // ......
    private final String mName;
    // ......
}

DisplayArea 代表了屏幕上的一块区域。

DisplayArea 中有一个字符串成员 mName,表示 DisplayArea 对象的名字,其内容由三部分组成 name + ":" + mMinLayer + ":" + mMaxLayer。其中:

  • name:用于指定 DisplayArea 的特殊功能(Feature),如:name 的值为 "WindowedMagnification" 表示 DisplayArea 代表的屏幕区域支持窗口放大。如果没有特殊功能且是叶子节点,name 的值为 "leaf"
  • mMinLayer 和 mMaxLayer,指定当前 DisplayArea 的图层高度范围,WMS 将 Z 轴上的纵向空间分成了 0 到 36 一共 37 个区间,值越大代表图层高度越高,这里两个值,指定了图层高度的范围区间。

实际上我们可以把屏幕显示的内容看做一个三维空间:

窗口立体结构

注意这里的 x y z 轴的位置,不同于一般的三维图。

这里的每一个矩形就是一个窗口(WindowState),我们可以将其理解为一个图层。

我们将 Z 轴分为 0 到 36, 一共 37 个连续的区间。

 

DisplayArea 是 WindowState 的上级节点,指定了一块显示区域,这里的显示区域包含了 x y z 三个方向上的区域。Z 轴的区域通过内部 mName 成员中保存的两个整形变量 mMinLayer 和 mMaxLayer 指定。比如 mMinLayer = 1, mMaxLayer =3,那么当前 DisplayArea 对象在 Z 轴方向上占据了 1 到 3,3 个区间的纵向空间,其子节点只能存在于这个区域。

在 Android 中, 通常一个 DisplayArea 在 Z 轴方向上占据一些层,这些层都会有自己的 feature,也就是上面我们说的 name。目前 Android 中主要有以下几种 feature:

WindowedMagnification

  • 拥有特征的层级:0-31。
  • 特征描述:支持窗口缩放的一块区域,一般是通过辅助服务进行缩小或放大。

HideDisplayCutout

  • 拥有特征的层级:0-14 16 18-23 26-31。
  • 特征描述:隐藏剪切区域,即在默认显示设备上隐藏不规则形状的屏幕区域,比如在代码中打开这个功能后,有这个功能的图层就不会延伸到刘海屏区域。

OneHanded

  • 拥有特征的层级:0-23 26-32 34-35。
  • 特征描述:表示支持单手操作的图层。

FullscreenMagnification

  • 拥有特征的层级:0-12 15-23 26-27 29-31 33-35。
  • 特征描述:支持全屏幕缩放的图层,和上面的不同,这个是全屏缩放,前面那个可以局部。

ImePlaceholder

  • 拥有特征的层级:13-14。
  • 特征描述:输入法相关。

DisplayArea 有三个子类 TaskDisplayArea,DisplayArea.Tokens 和 DisplayArea.Dimmable。DisplayArea.Tokens 有一个子类 DisplayContent.ImeContainer。DisplayArea.Dimmable 有一个子类 RootDisplayArea。

更清晰的继承关系可以参考以下的类图:

`

接下来,我们来看看 DisplayArea 的每一个子类:

TaskDisplayArea

/**
 * {@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 的描述:

       #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

    /**
     * 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 的描述:

      #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

    /**
     * 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 的描述:

       #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

    /**
     * DisplayArea that can be dimmed.
     */

    static class Dimmable extends DisplayArea<DisplayArea{
        // ......
    }

Dimmable 也是 DisplayArea 的内部类,从名字可以看出,这类的 DisplayArea 可以添加模糊效果。

它内部有一个Dimmer对象:

private final Dimmer mDimmer = new Dimmer(this);

可以通过 Dimmer 对象施加模糊效果,模糊图层可以插入到以该 Dimmable 对象为根节点的层级结构之下的任意两个图层之间。

RootDisplayArea

/**
 * 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

/**
 * 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

/** The root of a partition of the logical display. */
class DisplayAreaGroup extends RootDisplayArea {
}

DisplayAreaGroup,屏幕上的部分区域对应的 DisplayArea 层级结构的根节点。什么意思呢?

 

(图片来自 https://blog.csdn.net/shensky711/article/details/121530510)

我们可以创建两个 DisplayAreaGroup 并将屏幕一分为二分别放置这两个,这两个区域都是可以作为应用容器的,和分屏不一样的是,这两块区域可以有不同的 Feature 规则以及其他特性。这个用得比较少,了解一下即可。

3
如何查看窗口容器树?

我们可以通过:

adb shell dumpsys activity containers

命令,来查看窗口容器树,在 launcher 界面下,其输出如下:

ACTIVITY MANAGER CONTAINERS (dumpsys activity containers)
ROOT type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
  #0 Display 0 name="内置屏幕" type=undefined mode=fullscreen override-mode=fullscreen requested-bounds=[0,0][1080,2400] bounds=[0,0][1080,2400]
   #2 Leaf:36:36 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
    #1 WindowToken{397dfce type=2024 android.os.BinderProxy@1b30cc9} type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
     #0 52f3eef ScreenDecorOverlayBottom type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
    #0 WindowToken{a4f1664 type=2024 android.os.BinderProxy@d7d47f7} type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
     #0 1d730cd ScreenDecorOverlay type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
   #1 HideDisplayCutout:32:35 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
    #2 OneHanded:34:35 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
     #0 FullscreenMagnification:34:35 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
      #0 Leaf:34:35 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
    #1 FullscreenMagnification:33:33 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
     #0 Leaf:33:33 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
    #0 OneHanded:32:32 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
     #0 Leaf:32:32 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
   #0 WindowedMagnification:0:31 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
    #6 HideDisplayCutout:26:31 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
     #0 OneHanded:26:31 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
      #2 FullscreenMagnification:29:31 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
       #0 Leaf:29:31 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
      #1 Leaf:28:28 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
      #0 FullscreenMagnification:26:27 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
       #0 Leaf:26:27 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
    #5 Leaf:24:25 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
     #1 WindowToken{81514f0 type=2024 android.os.BinderProxy@8713933} type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
      #0 efb3a69 EdgeBackGestureHandler0 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
     #0 WindowToken{4ead5b8 type=2019 android.os.BinderProxy@f44da2a} type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
      #0 96b3f91 NavigationBar0 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
    #4 HideDisplayCutout:18:23 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
     #0 OneHanded:18:23 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
      #0 FullscreenMagnification:18:23 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
       #0 Leaf:18:23 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
    #3 OneHanded:17:17 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
     #0 FullscreenMagnification:17:17 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
      #0 Leaf:17:17 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
       #0 WindowToken{590030 type=2040 android.os.BinderProxy@8feb1e2} type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
        #0 913e4a9 NotificationShade type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
    #2 HideDisplayCutout:16:16 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
     #0 OneHanded:16:16 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
      #0 FullscreenMagnification:16:16 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
       #0 Leaf:16:16 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
    #1 OneHanded:15:15 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
     #0 FullscreenMagnification:15:15 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
      #0 Leaf:15:15 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
       #0 WindowToken{9c27fe1 type=2000 android.os.BinderProxy@569f3eb} type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
        #0 3f1e3c7 StatusBar type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
    #0 HideDisplayCutout:0:14 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
     #0 OneHanded:0:14 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
      #1 ImePlaceholder:13:14 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
       #0 ImeContainer type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
        #0 WindowToken{cd892cd type=2011 android.os.Binder@8d0c064} type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
         #0 49a6d60 InputMethod type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
      #0 FullscreenMagnification:0:12 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
       #2 Leaf:3:12 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
        #0 WindowToken{a847160 type=2038 android.os.BinderProxy@c8c148} type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
         #0 a24e51d ShellDropTarget type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
       #1 DefaultTaskDisplayArea type=undefined mode=fullscreen override-mode=fullscreen requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
        #2 Task=1 type=home mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
         #0 Task=68 type=home mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
          #0 ActivityRecord{501cf9a u0 com.android.launcher3/.uioverrides.QuickstepLauncher t68} type=home mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
           #0 2b9a038 com.android.launcher3/com.android.launcher3.uioverrides.QuickstepLauncher type=home mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
        #1 Task=77 type=standard mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
         #0 ActivityRecord{fd7a035 u0 com.android.settings/.Settings t77} type=standard mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
          #0 4b8b032 com.android.settings/com.android.settings.Settings type=standard mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
        #0 Task=2 type=undefined mode=fullscreen override-mode=fullscreen requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
         #1 Task=4 type=undefined mode=multi-window override-mode=multi-window requested-bounds=[0,2400][1080,3600] bounds=[0,2400][1080,3600]
         #0 Task=3 type=undefined mode=multi-window override-mode=multi-window requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
       #0 Leaf:0:1 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
        #0 WallpaperWindowToken{76e16e0 token=android.os.Binder@1c64ce3} type=undefined mode=fullscreen override-mode=fullscreen requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
         #0 4f077a5 com.android.systemui.wallpapers.ImageWallpaper type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]

看着很多,实际还是比较简单的:

ROOT type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
  #0 Display 0 name="内置屏幕" type=undefined mode=fullscreen override-mode=fullscreen requested-bounds=[0,0][1080,2400] bounds=[0,0][1080,2400]

第一行的 ROOT 表示根节点,第二行的 #0 Display 0 name="内置屏幕" 表示当前手机的第 0 个屏幕。

我们接着往下看:

#2 Leaf:36:36 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]

每一行开头的 # + 数字表示当前节点在父节点的位置。稍微往下看看,找到和当前 #2 前面空格一样多的 #1 和 #0,可以看出,Display 0 一共有 3 个子节点:

#0 Display 0 
 #2 Leaf:36:36
 #1 HideDisplayCutout:32:35
 #0 WindowedMagnification:0:31

# + 数字 后面紧接着的是当前节点所代表的窗口容器的 Feature 以及容器所占据的层级。

在 Feature 和层级的后面就是窗口/窗口容器的一些属性,基本从名字我们就能知道起含义了。

对于应用层,主要关注 DefaultTaskDisplayArea,因为这里放的才是应用相关的窗口,其他的一般都是系统窗口。像应用操作,分屏,小窗,自由窗口操作导致层级改变都体现在这一层。

4
窗口容器树的可视化

前面我们已经说过了,在代码中窗口/窗口容器通过树的形式联系在一起,我们可以通过上一节 adb shell dumpsys activity containers 命令打印的数据画出这颗树:

窗口容器树

图片如果看不清,可以通过下面的链接查看原图:https://boardmix.cn/app/share/CAE.CMLx1wwgASoQf8YHVeXOvtNYoW4GlhcAhjAGQAE/hxtUlY

实际只有 WindowState 节点才是最终显示的窗口,其他节点都是窗口容器。

参考资料

  • 【Android 12】WindowContainer类
  • android 13 WMS/AMS系统开发-窗口层级相关DisplayArea,WindowContainer
  • Android 12 - WMS 层级结构 && DisplayAreaGroup 引入
  • 安卓12窗口层的排序方式,层级值越高在Z轴上的位置也就越高。但是层级值的大小和窗口类型对应的次: DisplayArea树
  • Android T 窗口层级其二 —— 层级结构树的构建
  • 透视Android WindowManagerService
  • Android窗口系统第一篇---Window的类型与Z-Order确定
  • Android12 窗口组织方式(对比Android10)
  • 【Android 13源码分析】WindowContainer窗口层级-1-初识窗口层级树


最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!


推荐阅读

一款高效的HarmonyOS工具包
Android主线程锁监控的一种方案
Android多渠道打包指南


扫一扫 关注我的公众号

如果你想要跟大家分享你的文章,欢迎投稿~


┏(^0^)┛明天见!