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

0. 理解图层

20240822111043

(图片来自 www.jianshu.com/p/b0ef7c044…

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

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

1. 显示界面的组成与描述

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

20240822122626

从应用侧看

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

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

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

从系统侧看

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

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

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

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

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

Window 

Window 

Window 

Window 

Window 

Window 

Window 

Window 

Window 

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

2. 窗口容器树节点分析

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

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

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

我们先看看 WindowContainor 类的定义:



class WindowContainerextends WindowContainer> extends ConfigurationContainer
        implements Comparable, Animatable, SurfaceFreezer.Freezable {

    
}

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

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

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

    
    private WindowContainer mParent = null;

	

    
    
    protected final WindowList mChildren = new WindowList();

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

2.2 WindowState —— 窗口类

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


class WindowState extends WindowContainer implements WindowManagerPolicy.WindowState,
        InsetsControlTarget, InputTarget {
            
}

我们可以从 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



class WindowToken extends WindowContainer {
    
}

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

ActivityRecord


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


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 {

}

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


public class DisplayAreaextends WindowContainer> extends WindowContainer {
    
    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 个连续的区间。

20240222155917

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类图转存失败,建议直接上传图片文件`

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

TaskDisplayArea


final class TaskDisplayArea extends DisplayArea {
    
}

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

    
    public static class Tokens extends DisplayArea {

    }

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

    
    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

    
    static class Dimmable extends DisplayArea {
        
    }

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

它内部有一个Dimmer对象:

private final Dimmer mDimmer = new Dimmer(this);

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

RootDisplayArea


class RootDisplayArea extends DisplayArea.Dimmable {
    
}

RootDisplayArea,是一个DisplayArea 层级结构的根节点。

它可以是:

DisplayContent,作为整个屏幕的 DisplayArea 层级结构根节点。
DisplayAreaGroup,作为屏幕上部分区域对应的 DisplayArea 层级结构的根节点。

这又引申出了 RootDisplayArea 的两个子类,DisplayContent 和 DisplayAreaGroup。

DisplayContent


@TctAccess(scope = TctScope.PUBLIC)
class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.DisplayContentInfo {
    
}

DisplayContent,代表了一个实际的屏幕,作为一个屏幕上的 DisplayArea 层级结构的根节点。

DisplayAreaGroup


class DisplayAreaGroup extends RootDisplayArea {
}

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

20240222121912

(图片来自 blog.csdn.net/shensky711/…

我们可以创建两个 DisplayAreaGroup 并将屏幕一分为二分别放置这两个,这两个区域都是可以作为应用容器的,和分屏不一样的是,这两块区域可以有不同的 Feature 规则以及其他特性。,AOSP 虽然引入了这个概念和代码,但其实并未使用,我们只能从测试代码 DualDisplayAreaGroupPolicyTest 中略窥一二了。

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]
  
   
    
     
    
     
   
    
     
      
    
     
    
     
   
    
     
      
       
      
      
       
    
     
      
     
      
    
     
      
       
    
     
      
       
        
    
     
      
       
    
     
      
       
        
    
     
      
       
        
         
      
       
        
         
       
        
         
          
           
        
         
          
        
         
         
       
        
         

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

ROOT type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1080,2400]
  

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

我们接着往下看:


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


 
 
 

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

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

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

4. 窗口容器树的可视化

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

窗口容器树转存失败,建议直接上传图片文件

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

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

参考资料

阅读全文
下载说明:
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.dandroid.cn/archives/22350,转载请注明出处。
0

评论0

显示验证码
没有账号?注册  忘记密码?