WMS之addView流程

adb shell dumpsys windowadb shell dumpsys window windows可以用来dump 相应的Window信息。

问题

读WMS 代码的时候,或许会想回答下面的几个问题(问题的回答不在本文💨)。

  1. Window出错的提示
  2. 常用的状态栏修改的理想方式
  3. Window属性的设置是怎么起作用的
  4. 如何比较好的处理DialogPopupWindow。为什么Dialog不能使用ApplicationContext
  5. Window到底是什么,系统是怎么去识别一个Window的?多个Window重叠的时候是怎么展示的?层级关系是怎么解决的?像mac os、Windows(微软)都是有窗口的概念
  6. Window的展示区域是怎么计算的

流程分析

这里整体的流程是startActivity以后的操作。这里为了流程更加的清晰(更加简单😄),所以对打开的Activity设置<item name="android:windowDisablePreview">true</item>取消了startWindow。

先画一个整体的时序图。
WMS中addView流程

第2步,方法参数是int

第3步,执行以后会生成mDecor(代表的是顶层的Window中的顶层View)和mContentParent(代表的是R.id.content中的内容)。最后调用mLayoutInflater.inflate(layoutResID, mContentParent);将设置的layoutResID加载进mContentParent

第4步,generateDecor生成的是DecorView

第5步,generateLayout生成的是R.id.content节点的View。这里会去获取Window的属性。aosp/frameworks/base/core/res/res/value/attrs.xml中定义了Window 的属性,而Window 的属性则是设置的Window 的主题

总结:上面3-6步过程结束以后就相当于创建完PhoneWindow以及构造完里面的View(此时还没有加入到Window)。

这里介绍一个类android.view.Display。代表的是展示的逻辑大小(相当于是一个虚拟的大小),有两种不同的描述。

  1. 应用显示:代表的是除了系统装饰(statusBar、navigationBar等)以后的展示。
  2. 实际显示:代表的是包含了系统装饰的展示。

但是实际的物理展示大小跟Display代表的意思不一样,Display可能会比设备的屏幕要小。

第7步,WindowManagerImpl#addViewActivity#makeVisible调用的。

第8步,adjustLayoutParamsForSubWindow不是每次都执行。比较重要的是会赋值token。

第9步,addView,首先WindowManagerGlobal是一个单例。看代码10、11、12行会将ViewViewRootImplWindowManager.LayoutParams分别保存在WindowManagerGlobal3个ArrayList中。由于WindowManagerGlobal是一个单例,所以在一个进程中所有Window的信息都会保存在这3个ArrayList中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
//省略代码...
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
//省略代码...
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
}
// do this last because it fires off messages to start doing things
try {
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
//省略代码...
}
}

第11步,com.android.server.wm.Session,是通过进程间调用以后在服务端的处理。此时客户端挂起。

1
2
3
4
//IWindowSession.aidl 中的定义
int addToDisplay(IWindow window, int seq, in WindowManager.LayoutParams attrs,
in int viewVisibility, in int layerStackId, out Rect outContentInsets,
out Rect outStableInsets, out Rect outOutsets, out InputChannel outInputChannel);

com.android.server.wm.Session代表的是什么,仅仅是用来跟WMS 交互吗?这里看相关代码的时对Session很迷糊,Session是IPC 的服务端,而每一个客户端的ViewRootImpl都有一个IWindowSession$Stub$Proxy,客户端和WMS 不直接进行IPC 通信,而是客户端与Session通信,Session直接与WMS 交互。

第12步,WindowManagerService#addWindow,这个方法比较复杂,后面会分成几个步骤分析。

这里有一些迷惑的内容:ActivityRecord$TokenWindowTokenAppWindowTokenWindowState

  • ActivityRecord$TokenActivityManagerService标识的一个Activity。(即服务端表示的一个客户端的Activity)
  • WindowStateWindowManagerService标识的一个Window。(Window在客户端是一个抽象的概念,在服务端使用WindowState表示)
  • WindowTokenAppWindowToken:一个WindowToken里面保存了一个IBinder对应的所有WindowState信息(比如一个Activity和里面的Dialog对应的是同一个WindowToken)。AppWindowToken是继承WindowToken,代表记录的是一个ActivityRecord$Token对应的所有的WindowState信息。不是用来表示一个Window的标识的。
1
2
HashMap<IBinder, WindowState> mWindowMap
HashMap<IBinder, WindowToken> mTokenMap

上面是ActivityManagerService中的两个重要的成员变量。

  • mWindowMap的key 对应的是ViewRootImpl$W(直接在system_process 进程做setView操作)或IWindow$Stub$Proxy(整个过程setView中从客户端传递过来的ViewRootImpl$W对象)
  • mTokenMap的key 对应的则是ActvityRecord$Token(这里是一个IBinder对象,不一定都是ActvityRecord$Token,对Activity来讲就是一个ActvityRecord$Token;对PopupWindow来讲是ViewRootImpl$W)。

第13步,checkAddPermission权限检查。

第15步,addWindowToListInOrderLocked是将WindowState加入到WMS 中的流程:涉及到Window层级计算。打开一个Activity的时候有可能会执行两次addWindowToListInOrderLocked,原因是打开Activity的时默认有一个预览界面的,不过这里的流程是手动去掉了startWindow。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//ActivityManagerSevice
private void addWindowToListInOrderLocked(final WindowState win, boolean addToToken) { //win 代表的是需要加入的Window,新打开Activity 的时候addToToken 是true
// 省略代码...
if (win.mAttachedWindow == null) { //应用Window 的mAttachedWindow 为null
final WindowToken token = win.mToken; //获取到token
int tokenWindowsPos = 0;
if (token.appWindowToken != null) {//对于Activity 来说,这里appWindowToken != null 成立
tokenWindowsPos = addAppWindowToListLocked(win);//1.
} else {
addFreeWindowToListLocked(win);
}
if (addToToken) {//这里为true,所以会把数据WindowState 加入到指定的位置tokenWindowsPos
if (DEBUG_ADD_REMOVE) Slog.v(TAG_WM, "Adding " + win + " to " + token);
token.windows.add(tokenWindowsPos, win);//2.
}
} else {
addAttachedWindowToListLocked(win, addToToken);
}
final AppWindowToken appToken = win.mAppToken;
if (appToken != null) {//因为是Activity,所以这里appToken != null成立
if (addToToken) {//addToken 成立
appToken.addWindow(win);//3.
}
}
}

主要有3个步骤:

  1. 第8行的addAppWindowToListLocked会计算出position,后面会分析。
  2. 第14行将当前的WindowState加入到指定的位置(tokenWindowsPos)。(这里可以举一个例子,一般新Activity的时候tokenWindowsPos 为0,而在Activity中打开Dailog的时候tokenWindowsPos 为1)
  3. 第23行,因为是Activity,所以还需要将WindowState加入到AppWindowToken中。

这里的流程只是分析了startActivity的,但是这里还有几个关键的判断需要注意下。第4行win.mAttachedWindow不为null 代表添加进来的是子Window会直接执行到addAttachedWindowToListLocked代表的是添加子Window。第10行addFreeWindowToListLocked则代表添加的是系统Window。(这里的逻辑可以分别使用PopupWindowToast来分析)

第16步,addAppWindowToListLocked是将应用Window(常见的是ActivityDialog)加入到DisplayContent#mWindows(按照z-order 进行排序的)的流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
//WindowManagerService 中加入应用Window 的流程(注意这里强调是加入应用Window 的流程)
private int addAppWindowToListLocked(final WindowState win) {
final DisplayContent displayContent = win.getDisplayContent(); //displayContent 相当于展示的界面信息(里面有多少个WindowState 等等)
final IWindow client = win.mClient; //对应的客户端
final WindowToken token = win.mToken;
final WindowList windows = displayContent.getWindowList(); //获取到的是当前展示屏幕的所有Window。
WindowList tokenWindowList = getTokenWindowsOnDisplay(token, displayContent); //获取某个token 对应的WindowList
int tokenWindowsPos = 0;
if (!tokenWindowList.isEmpty()) { //打开Activity 的时候这里没有值。
return addAppWindowToTokenListLocked(win, token, windows, tokenWindowList); //这里展开了预览界面之后打开Activity 或则打开的是Dialog 会执行这里
}
// 省略代码...
WindowState pos = null;
final ArrayList<Task> tasks = displayContent.getTasks(); //这里的Task 和TaskRecord 比较类似。在Activity 的启动流程中就会将相应的信息加入到Task 中
int taskNdx;
int tokenNdx = -1;
for (taskNdx = tasks.size() - 1; taskNdx >= 0; --taskNdx) { //1.现在需要找到一个参考的WindowState,将win 插入到参考点后面或者前面
AppTokenList tokens = tasks.get(taskNdx).mAppTokens; //找到对应的Task(其实相当于是Application),然后再找到是否有一致的Token(相当于是Window)
for (tokenNdx = tokens.size() - 1; tokenNdx >= 0; --tokenNdx) { //tokens 里面记录的数据不是最终展示的WindowList
final AppWindowToken t = tokens.get(tokenNdx); //找到相应的Token
if (t == token) { //从Task 里面遍历找里面的Token,获取到的Token 相同,相当于找到了Token 和Task 的值。
--tokenNdx; //减1代表的是参考WindowState对应的WindowState
if (tokenNdx < 0) { //小于0代表的是这个Task 已经结束,应该跳到上一个Task 寻找
--taskNdx; //taskNdx减1导致可能跳到上一个Task 里面
if (taskNdx >= 0) {
tokenNdx = tasks.get(taskNdx).mAppTokens.size() - 1;//tokenNdx< 0的情况下,没办法计算出应该加入到哪一个里面,所以计算出当前token 所在Token 的值
}
}
break;
}
//省略代码...
}
if (tokenNdx >= 0) {
// early exit
break;
}
}
//省略代码...
// Continue looking down until we find the first
// token that has windows on this display.
for ( ; taskNdx >= 0; --taskNdx) { //2.上面已经计算出了对应的是taskNdx 和tokenNdx,然后这里计算出来的就是taskNdx 和tokenNdx 对应的WindowState。以后的值需要根据这个WindowState 来计算
AppTokenList tokens = tasks.get(taskNdx).mAppTokens;
for ( ; tokenNdx >= 0; --tokenNdx) {
final AppWindowToken t = tokens.get(tokenNdx);
tokenWindowList = getTokenWindowsOnDisplay(t, displayContent);
final int NW = tokenWindowList.size();
if (NW > 0) {
pos = tokenWindowList.get(NW-1);
break;
}
}
if (tokenNdx >= 0) {
// found
break;
}
}
if (pos != null) {
// Move in front of any windows attached to this
// one.
WindowToken atoken = mTokenMap.get(pos.mClient.asBinder());
if (atoken != null) {
final int NC = atoken.windows.size();
if (NC > 0) {
WindowState top = atoken.windows.get(NC-1);
if (top.mSubLayer >= 0) {
pos = top;
}
}
}
placeWindowAfter(pos, win); //3.直接将windowState 加入进去。
return tokenWindowsPos;
}
//省略代码...
}

主要有3个步骤:

  1. 第17行开始的两个循环遍历,是用来查找将要加入的WindowState的参考的WindowState所在的Task 以及所对应的Token。(上面的代码注释的比较详细,但是还是总结一下,这里的每个Activity会分到不同的Task,每个Task里面也包含了很多的Token,会从后向前遍历Task寻找,对每一个Task里面的Token也是从后向前遍历,这样最终找到的餐参考值可能是当前WindowState对应的Task的上一个Task)
  2. 第42行开始的两个循环遍历,计算参考的WindowState。(这里比较关键的是每次都尽量取下面的值,第49行)
  3. 第73行将WindowState加入进去。

第3行的DisplayContent和第14行的Task,需要解释一下。DisplayContent记录的是一个显示屏里的内容(比如DisplayContent#mWindows记录的是屏幕中的所有WindowState)。Task比较类似TaskRecord

第17步,addAppWindowToTokenListLocked一般是已经加入然后这里是实际加入的Activity(startActivity先展示预览界面然后才展示Activity或者是打开Dialog的流程)。实现的功能和第16步是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//WindowManagerService
private int addAppWindowToTokenListLocked(WindowState win, WindowToken token,
WindowList windows, WindowList tokenWindowList) { //win代表的是加入的Window,windows 代表当前屏展示的Window 列表,tokenWindowList 则是当前WindowToken 对应的Window 列表
int tokenWindowsPos;
//省略代码...
if (win.mAttrs.type == TYPE_BASE_APPLICATION) { // 代表的是Activity
// Base windows go behind everything else.
WindowState lowestWindow = tokenWindowList.get(0); //计算出tokenWindowList 的最上面那个lowestWindow
placeWindowBefore(lowestWindow, win); // 将win 放在lowestWindow 的前面
tokenWindowsPos = indexOfWinInWindowList(lowestWindow, token.windows); //计算出lowestWindow 在tokenWindowList 中的位置
} else { //这里大部分就是Dialog 了
AppWindowToken atoken = win.mAppToken;
final int windowListPos = tokenWindowList.size();
WindowState lastWindow = tokenWindowList.get(windowListPos - 1);
if (atoken != null && lastWindow == atoken.startingWindow) {
placeWindowBefore(lastWindow, win);
tokenWindowsPos = indexOfWinInWindowList(lastWindow, token.windows);
} else {
int newIdx = findIdxBasedOnAppTokens(win);
//省略代码...
windows.add(newIdx + 1, win);
if (newIdx < 0) {
// No window from token found on win's display.
tokenWindowsPos = 0;
} else {
tokenWindowsPos = indexOfWinInWindowList(
windows.get(newIdx), token.windows) + 1;
}
mWindowsChanged = true;
}
}
return tokenWindowsPos;
}

主要分2种情况:

  1. 第6行,代表的是Activity
  2. 第11行,代表的是Dialog

总结:第15-17是关键步骤,主要是将需要展示Window加入到WindowManagerService的过程。完成以后所有的Window会按照z-order 升序保存在DisplayContent#mWindows中。

第19步,getInsetHintLw计算展示区域的大小。计算出来的值都是Inset(边衬区),里面的值实际上是距离展示区域的上下左右的值。这里计算出来的值可能跟最终实际展示的数据关系不大。

第20步,assignLayersLocked计算出WindowState的layer,用于最终Window的展示层级。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//WindowLayersController
final void assignLayersLocked(WindowList windows) { //用来计算Window 的展示层级。这里的输入已经按照z-order 排序了
//省略代码...
int curBaseLayer = 0;
int curLayer = 0;
boolean anyLayerChanged = false;
for (int i = 0, windowCount = windows.size(); i < windowCount; i++) {
final WindowState w = windows.get(i);
boolean layerChanged = false;
int oldLayer = w.mLayer;
if (w.mBaseLayer == curBaseLayer || w.mIsImWindow || (i > 0 && w.mIsWallpaper)) { //1.如果是相同类型、或者是输入法Window、或者是墙纸Window(并且i>0)
curLayer += WINDOW_LAYER_MULTIPLIER; //如果满足条件只是+5
} else { //2.
curBaseLayer = curLayer = w.mBaseLayer;
}
assignAnimLayer(w, curLayer); //3.给w 分配layer,mLayer 赋值
// TODO: Preserved old behavior of code here but not sure comparing
// oldLayer to mAnimLayer and mLayer makes sense...though the
// worst case would be unintentionalp layer reassignment.
if (w.mLayer != oldLayer || w.mWinAnimator.mAnimLayer != oldLayer) {
layerChanged = true;
anyLayerChanged = true;
}
if (w.mAppToken != null) {
mHighestApplicationLayer = Math.max(mHighestApplicationLayer,
w.mWinAnimator.mAnimLayer);
}
collectSpecialWindows(w);
if (layerChanged) {
w.scheduleAnimationIfDimming();
}
}
//省略代码...
}

可以重点看看第12行判断语句,看第一个判断逻辑w.mBaseLayer == curBaseLayer,意思是当前的baseLayer 和上一个WindowState相同的时候就在上一个WindowState#mLayer的基础上+5,否则就会重新一次新的WindowState#mLayer计算。

总结:到此为止完成了Activity#setContentView的第一个流程。从代码的逻辑上看,是将WMS 中的mWindowMap的数据更新到DisplayContent#mWindows中,同时计算了DisplayContent#mWindows的layer 的值。此时对系统(WMS)来说,已经知道了展示WindowState以及每一个WindowState的层级关系。(后续的逻辑就是需要将这些WindowState绘制出来了)

代码总结

  1. PhoneWindow:在应用层代表的一个Window
  2. WindowState:在WMS 代表的是一个Window
  3. DisplayContent:代表显示屏幕的信息
  4. Task:WMS 里面中的一个Task 的概念,Task 对应的id 还与Activity 的TaskRecord 对应的信息是一致的。主要是用来计算
  5. Session:上面有介绍
  6. WindowToken、AppWindowToke:上面有介绍

WMS addView uml简图

入口是WindowManagerervice

参考

还是老罗分析的棒