在安卓源码的设计中,将将屏幕分为了37层,不同的窗口将在不同的层级中显示。
对这一块的概念以及相关源码做了详细分析,整理出以下几篇。
【Android 13源码分析】WindowContainer窗口层级-1-初识窗口层级树
【Android 13源码分析】WindowContainer窗口层级-2-构建流程
【Android 13源码分析】WindowContainer窗口层级-3-实例分析
【Android 13源码分析】WindowContainer窗口层级-4-Surface树
当前为第一篇,主要是基础知识介绍。
打开"电话应用",然后按音量键出现一下界面:
提出2个问题:
- 为什么音量窗口会挡住应用窗口?
- 为什么不管打开哪个应用都能看到导航栏和状态栏?
再看下面这个图:
左边是将第一张截图的每一个窗口提出来画了的模拟图, 想象一下每个窗口其实都是全屏的,那么他的前后顺序若右图(注意颜色是对应的)。 如果说是按右边的这种层级排序,那么音量键的窗口和状态栏的窗口就会挡住Activity的窗口。
在安卓中窗口是有先后顺序的,越靠近用户的就越靠前,就能挡住底下的窗口。 目前说这么一个结论可能为时过早,后面的内容将详细介绍。
1. 基础知识介绍
1.1 三维空间概念
1.1.1 三维空间概念--游戏3D世界
下面2个图来自Unity3d官网开发文档:
们玩的王者荣耀,原神等游戏的开发,都是类似在这么一个3D场景下进行的,开发者将使用3D建模工具来创建地形、建筑、植被等地图元素,并通过材质和贴图来增强地图的视觉效果。
比如这张图片里放了3个颜色的柱子。 除了这些物体外,可以看到还有一个摄像机(Camera),它是用来捕捉画面的, 毕竟手机屏幕是2D的,简单来说这个摄像机能捕捉到的画面就是我们手机屏幕上显示的内容,比如这张图片捕捉到的画面是右下角的内容。
像我们玩游戏移动角色,其实就是通过转动这个摄像机(Camera)来完成的。
下面这种图会更加像一个游戏的画面一些。
通过看游戏地图的3D开发场景,就是希望能狗更加生动的理解到在2D的手机屏幕下,其实有一个3D的空间,虽然我们的安卓开发不会像游戏开发那么复杂,但是原理也是一样的。
1.1.2 三维空间概念--Android的三维坐标系
Android坐标系其实就是一个三维坐标,Z轴向上,X轴向右,Y轴向下。
经常听到的 Z-Order 也就是指窗口在Z轴的排序,离用户越近,Z值越大。并且能够遮挡住后面的窗口。
为了再加深一下安卓窗口的层级关系,下面看一张应用开发的View层级的3D视角。
注意,下面的图片是View,不是本次要讲的窗口,只是为了加深一下层级印象
可以看到在布局了写了37层约束布局,其实第3层加了一个按钮。 右上角我们手机屏幕上只是一个按钮的界面,但是右下角通过Android Studio自带的工具可以发现,整个View树是有很多层的(代码写了37层)。
这里看到的View层级其实和后面要讲就窗口分层,是有相似之处的。 将这里是37层想象层安卓在窗口的分层也是可以的。
1.2 ViewTree
为了后续方便理解窗口树, 先介绍一下安卓开发都知道的View树
XML里如图写下2个简单的红绿布局,显示的UI效果绿色的会挡住一部分红色的。但是用工具其实可以发送红色控件其实也是完成绘制的。
也就是说在同一层级下,在ViewGropu孩子View数组这个集合中,下标越大的View会挡住后面的View。也可以理解层 层级越靠前,就会当初后面的View。
继续增加几个View
-
控件B下新增控件D,D下面放了一个文本控件G
-
控件C下面新增了一个文本控件F
左边看到对View做了层级处理,这个就是ViewTree,可以将其画成树图
这个就是View层的View树,能构建出View树的原因是因为在代码中定义了ViewGroup这个类
# View
// 父容器
protected ViewParent mParent;
# ViewGroup
// 所有子View
private View[] mChildren;
public void addView(View child, int index) {
......
// 内部实现是通过addInArray 将View添加到数组mChildren中
addView(child, index, params);
}
@Override
public void removeView(View view) {
......// 本质还是从mChildren移除
}
应用布局用的场景的几个类,他们都有一个共同的父类--ViewGroup。
ViewGroup继承了View,所以有父亲(mParent),自身内部又维护了一个View数组(mChildren)表示它的孩子们。 上有父亲下有一群孩子,所以能构建出一个ViewTree。
如果只有一个孩子,那是线性结构,因此孩子必须是多个,所以ViewGroup也是一个View的容器类。 有这么一个类的存在,开发者就可以通过嵌套来行程一个非常复杂的ViewTree结构
这也是应用开发者能写出各种丰富UI的基础。
在窗口这一级别的开发中,也有窗口树,也有对应的容器结构。 后面会详细解释窗口容器类,在介绍之前先了解一下什么是窗口。
ViewTree在代码中也是真实存在的,在Activity中通过以下代码对应ViewTree的变化做监听
View.getViewTreeObserver().addOnGlobalLayoutListener(
......
)
1.3 什么是窗口
从视觉上,用户在手机屏幕上看到的"一块区域"就是一个窗口,比如前面看到的这张图,每一块都是一个窗口
从代码上来说,应用端的窗口指的是Window, framework层的窗口指的是WindowState
为什么会用不一样的类来表示"窗口"呢?
比如说有一个人,他是唯一的,身份证号是他的唯一表示,但是他在不同的系统中,保存的他的数据是不一样的,比如在公司,公司系统对这个人保存的个人基本信息,在交警系统,保存的是这个人的车辆信息和违章信息。
在生活中,不同系统对一个人关注保存的数据是不一样,在代码中也是一样的。应用开发者为了降低模块的依赖,也会有这种设计。
2. 初识窗口层级树
2.1 窗口容器类介绍
前面看到了一些View树构建的类,也就是我们说的常见布局,现在列举构建窗口树用的的几个容器类。
WindowContainer:
类似ViewGroup的存在,是窗口容器的基类,后面介绍的其他窗口容器类都是它的子类。
有泛型限制,说明容器的内容是有限制的
mParent: 保存当前容器的父窗口引用。
mChildren :保存当前窗口的所有孩子窗口容器集合(有泛型)。根据注释,列表后面的子容器,z-order 越大,离屏幕越近。
父类为ConfigurationContainer,封装了配置的处理,当前类封装了容器的操作。
RootWindowContainer:*
根窗口容器,也是窗口层级树的根,管理DisplayContent。
DisplayContent:
代表一个屏幕,Android是支持多屏幕的。
继承关系为:
DisplayContent->RootDisplayArea->DisplayArea.Dimmable->DisplayArea->WindowContainer
孩子为DisplayArea(这个规则定义在DisplayArea的子类Dimmable中)
WindowState:
代表一个窗口,本身也是一个容器,比如子窗口就是它的孩子(Popupwindow场景)
DisplayArea:
注释:DisplayContent下的窗口容器集合。
说人话:表示一块显示区域,是DisplayContent的子容器。DisplayContent下安卓目前设计为分了37层,每一层都是一个DisplayArea。
有三个直接子类,TaskDisplayArea,DisplayArea.Tokens和DisplayArea.Tokens。
TaskDisplayArea:
注释:孩子可以是Task,或者是TaskDisplayArea。(不过目前看到的孩子都是Task类型)。
对应层级树的第二层,专门用来存放应用的窗口图层,非常重要,APP的窗口都在这。也是窗口层级树看的重点区域。
DisplayArea.Token:
DisplayArea的子类,并且是其内部类,表示WindowToken的容器。WindowToken的子类是WindowState
DisplayArea.Dimmable:
DisplayArea的子类,并且是其内部类,DisplayContent的父类,带模糊效果。孩子是DisplayArea。
ImeContainer:
输入法容器,父类是DisplayArea.Token,那么孩子也是WindowToken。输入法专用
Task:
父类TaskFragment继承WindowContainer,泛型没有限制孩子的类型。但是实际情况下孩子是Task和ActivityRecord类型。
开发过程中经常见到的类,也是应用开发,多窗口开发经常会遇到的。
WindowToken:
理器中一组相关窗口的容器,是窗口的Token,而窗口的定义WindowState。一般在窗口层级树中WindowToken下面就会挂载一个WindowState。
壁纸用到的WallpaperWindowToken也是其子类。
ActivityRecord:
对应着一个Activity。
是WindowToken的子类,所以孩子也是WindowState。 从应用开发角度,一个Activity下也有一个Window。并且一般作为是Task的孩子。
2.2 Feature介绍
为什么有这个Feature(特征)呢?
AOSP既然将屏幕分了37层,那说明图层之间是有区别的,有不一样的特性,这个就是Feature,比如这一层是不是支持单手操作。
这里列举5个常见的Feature,已经它们所在的层级。
WindowedMagnification
拥有特征的层级: 0-31
特征描述: 支持窗口缩放的一块区域,一般是通过辅助服务进行缩小或放大
HideDisplayCutout
拥有特征的层级: 0-14 16 18-23 26-35
特征描述:隐藏剪切区域,即在默认显示设备上隐藏不规则形状的屏幕区域,比如在代码中打开这个功能后,有这个功能的图层就不会延伸到刘海屏区域。
OneHanded
拥有特征的层级:0-23 26-32 34-35
特征描述:表示支持单手操作的图层,这个功能在手机上还是挺常见的
FullscreenMagnification
拥有特征的层级:0-12 15-23 26-27 29-31 33-35
特征描述:支持全屏幕缩放的图层,和上面的不同,这个是全屏缩放,前面那个可以局部
ImePlaceholder
拥有特征的层级: 13-14
特征描述:输入法相关
3 WMS的层级结构树
可以通过以下命令来看获取到设备当前的层级结构树
adb shell dumpsys activity containers
在开完机后的launcher就执行了dump命令,然后就能得到下面这么一段输出,乍一看很容易劝退,但是实际上这些东西都是有规律,而且很简单。目前可以先不看,稍后再详细解释。
ACTIVITY MANAGER CONTAINERS (dumpsys activity containers)
ROOT type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 Display 0 name="Built-in Screen" type=undefined mode=fullscreen override-mode=fullscreen requested-bounds=[0,0][720,1600] bounds=[0,0][720,1600]
#2 Leaf:36:36 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#1 WindowToken{451b2bd type=2024 android.os.BinderProxy@4526826} type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 47e1803 ScreenDecorOverlayBottom type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 WindowToken{69b9325 type=2024 android.os.BinderProxy@3a8ab1c} type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 799b2ab ScreenDecorOverlay type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#1 HideDisplayCutout:32:35 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#2 OneHanded:34:35 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 FullscreenMagnification:34:35 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 Leaf:34:35 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#1 FullscreenMagnification:33:33 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 Leaf:33:33 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 OneHanded:32:32 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 Leaf:32:32 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 WindowedMagnification:0:31 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#6 HideDisplayCutout:26:31 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 OneHanded:26:31 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#2 FullscreenMagnification:29:31 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 Leaf:29:31 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#1 Leaf:28:28 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 FullscreenMagnification:26:27 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 Leaf:26:27 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#5 Leaf:24:25 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#1 WindowToken{922c2bc type=2024 android.os.BinderProxy@d50168e} type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 48a6245 pip-dismiss-overlay type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 WindowToken{1a3a19a type=2019 android.os.BinderProxy@1ec36bc} type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 50a3d66 NavigationBar0 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#4 HideDisplayCutout:18:23 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 OneHanded:18:23 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 FullscreenMagnification:18:23 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 Leaf:18:23 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#3 OneHanded:17:17 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 FullscreenMagnification:17:17 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 Leaf:17:17 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 WindowToken{7472fe2 type=2040 android.os.BinderProxy@1bfb9c4} type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 4b26f73 NotificationShade type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#2 HideDisplayCutout:16:16 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 OneHanded:16:16 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 FullscreenMagnification:16:16 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 Leaf:16:16 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#1 OneHanded:15:15 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 FullscreenMagnification:15:15 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 Leaf:15:15 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 WindowToken{3da7d5c type=2000 android.os.BinderProxy@e2c682e} type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 7619865 StatusBar type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 HideDisplayCutout:0:14 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 OneHanded:0:14 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#1 ImePlaceholder:13:14 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 ImeContainer type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 WindowToken{1397896 type=2011 android.os.Binder@23bebb1} type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 d0c3c51 InputMethod type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 FullscreenMagnification:0:12 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#2 Leaf:3:12 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 WindowToken{82fa61a type=2038 android.os.BinderProxy@2f86adc} type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 33a873c ShellDropTarget type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#1 DefaultTaskDisplayArea type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#2 Task=1 type=home mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 Task=7 type=home mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 ActivityRecord{bd2b1d4 u0 com.android.launcher3/.uioverrides.QuickstepLauncher} t7} type=home mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#1 ae1df9b 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,1600]
#0 b9fa2f0 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,1600]
#1 Task=2 type=undefined mode=fullscreen override-mode=fullscreen requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 Task=3 type=undefined mode=fullscreen override-mode=fullscreen requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#1 Task=6 type=undefined mode=multi-window override-mode=multi-window requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 Task=5 type=undefined mode=multi-window override-mode=multi-window requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 Leaf:0:1 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 WallpaperWindowToken{4b4c99a token=android.os.Binder@9258e45} type=undefined mode=fullscreen override-mode=fullscreen requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 f3495ce com.android.systemui.ImageWallpaper type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
先看看点击桌面的"电话"进入电话界面后,再执行这个dump命令有什么区别
tips: com.google.android.dialer 这个包名是"电话"这个应用
这个区别就比较好看出来了,在#1 DefaultTaskDisplayArea下多了一些东西。看到里面也知道这个是增加一些与"电话"这个应用的Activity相关的东西。
然后再按一下音量键盘,看一下区别
在其他的场景比如按power出现的弹窗, 出现toast的时候,或者进入分屏都可以进行dump,对比一下差异。
现在可以有以下信息:
- 开完机层级结构树就存在
- 界面上有相关的Window的操作,都会在层级结构树上体现
- 不同的window会被挂在到对应的位置,这个其实就是层级结构树的关键。
当然哪个Window应该挂在到那一层,怎么个先后顺序,这个我们其实无需过于在意,这个是产品设计。
tips: 可以试试不同Activity启动模式后的区别
3.1 简单分析层级结构树
现在来分析上面那一团输出信息怎么看。(从上到下,从左往右)
ROOT type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 Display 0 name="Built-in Screen" type=undefined mode=fullscreen override-mode=fullscreen requested-bounds=[0,0][720,1600] bounds=[0,0][720,1600]
上面的ROOT 表示根节点,暂时可以忽略。下面的 #0 Display 0 name="Built-in Screen"表示当前的手机的第0个屏幕,目前也可忽略,主要是下面那一部分内容。
下面的内容虽然很长,但是我们不要看全部,抓住几个点就够了,以前面这段为例
#2 Leaf:36:36 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
1. 看所在位置 #X
每一行最前面都是 "#+数字"的形式打头,比如现在看的是 "#2 Leaf:36:36"这里的数字表示这个图层在当前父容器的位置,从0开始。其实和ViewGrop或许childView的一样的。
当前这个为#2 所以他的父容器一共有3个子容器,当前这个处于第三个,也就是最上面。那么和他同级的另外2个怎么找呢?
需要往下看,找到和当前 #2 前面空格一样多的#1 和#0 就是他同级的2个容器了,按照规则能能找下面这2个与他同级的。
需要注意这里说的同级并不是在同一图层,而是在层级树这个树的结构是同级关系
#1 HideDisplayCutout:32:35
#0 WindowedMagnification:0:31
2. 层级name 名字+起始层级:结束层级
指的是 "Leaf:36:36" 这一段信息,这个的格式为"容器名 起始层级:结束层级"
体现在当前就是这个 #2 的容器叫 "Leaf",表示一个叶子节点,比较特殊,主要看后面的频繁出现的HideDisplayCutout,ImePlaceholder,OneHanded等这些,都有具体的意义,我们知道所有的东西在源码中都能找到对应的代码, 像提到的HideDisplayCutout,ImePlaceholder,OneHanded在源码中称之为Feature(特征),即表示当前这个容器有具有这个特征,暂时知道就可以,后面会详细介绍源码对这些Feature的具体定义。
然后就是后面的起始层级:结束层级,因为虽然一共是分为37层,但是并不是说有37个Feature,比如"#1 ImePlaceholder:13:14 " ImePlaceholder看着就是和输入法相关,那就代表着13,14都是和输入法相关的window。
android 13目前一共也只有5个Feature。
另外提一下这里的 "Leaf"代表的不是Feature,而且说当前是某个叶子节点,下面是要挂着具体Window的。
3. 看其他属性,比如type,mode
知道上面这4点基本上就能看到层级结构树的信息了,内容虽然很多,但是我们其实主要关心的还是下面"#1 DefaultTaskDisplayArea"的部分,因为这里放的才是应用相关的窗口,其他的一般都是系统窗口。像应用操作,分屏,小窗,自由窗口操作导致层级改变都体现在这一层,
另外可以留意一下如果是 WindowToken +WindowState的都是系统窗口,比如下面这种形式:
#0 WindowToken{1397896
#0 d0c3c51 InputMethod
d0c3c51 这个应该是WindowState的对象名,后面的InputMethod是具体的窗口名
而 ActivityRecord+WindowState就是应用了比如:
#0 ActivityRecord{91c971c
#0 9c20028 com.google.android.dialer
这些有个印象就行,不需要硬背,以后看的多了自然就有感觉了。
3.2 层级结构树可视化
单看层级树可能过于枯燥,在刚开始学习的时候一般都会根据信息画出一个层级结构图。
首先根据前面看dump内容的方式,先画出一部分内容如下:
这里是DisplayContent下的3个孩子,可以看到已经覆盖了 0-36层。
然后按照这种方式将所有的内容都画出来就可以得到下面这完整的树图:
强烈建议想学这一块的同学一定要手动画出这么一个图
android版本相同,画出来的图基本上都是一样的,只会根据出现不同的Window在响应的Leaf,也就是叶子节点会有不同。
这里将叶子节点的颜色涂上了,发现叶子节点下要么为空,要么就是 WindowToken,除了最底层的壁纸,其实壁纸叶子节点下的WallpaperWindowToken这个类,也是继承的WindowToken。
可能之前通过文本的形式还有点陌生,但是转换成图片后,一些常见的东西就都清楚了,比如launcher,StatusBar,NavigationBar,Wallpaper
窗口树和View树还是有差距的,View树上都是View,而窗口树上只有叶子节点上挂着窗口,其他大都都是一些容器和一些。
窗口树是固定37层的, 然后各个图层都有自己支持的Feature这些都是代码中固定好的。 实际开发中,开发中再将自己的窗口根据需求挂到对应的叶子节点上,这个和View树是有区别的。
3.3 为什么这么设计
方便管理
在写应用的时候也会这样定义,这样如果说某个业务的View无论怎么写,他就只能在自己所在的"层级"上显示,不会影响到其他。
窗口这样设计的原因可能还有其他的考虑,在远古时期窗口的顺序好像是经过规则计算的,那么这种计算很麻烦出了问题也不好定位。
现在的这种设计就很合理,符合"单一原则",各个类型的窗口在自己的层级上,不会影响到其他。
方便功能开发
另外一个优点我认为是这种设计为小窗,分屏这种功能提供了开发的便利,因为只需要将对应的窗口移动到所在的Task就可以了。
像现在的Activity启动,在system_service进程的处理的很多逻辑都是围绕着这个层级树来做的。
比如看一眼"电话"和"短信"2个应用进行分屏操作的前后对比:
通过对比可以发现
- Task=5,6 这2个Task是分屏用到的Task,它们有共同的父亲-- Task =4。 这3个Task 在开机的时候就创建好了,默认在DefaultTaskDisplayArea孩子里是最后面。
- 启动分屏后其实对应窗口容器这边做了2件事
- 将Task=4 移到栈顶
- 将2个分屏应用的Task分别移到到Task5,6 下
这样分屏就完成了。