token报错分析

为什么要再读Activity的启动呢,因为在启动弹窗的时候,出现了一个这个报错:

1
2
3
4
Process: com.librty.crashsolution, PID: 7037 android.view.WindowManager$BadTokenException:   Unable to add window -- token null is not valid; is your activity running? 
at android.view.ViewRootImpl.setView(ViewRootImpl.java:1189)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:400)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:138)

里面个讲到一个window.token值。由于这个值的不正确导致的。
首先需要找到抛出异常的位置:在ViewRootImpl的setView阶段:

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
class ViewRootImpl{
public void setView(View view,WindowManager.LayoutParams attrs,View panelParentView,int userId){
...
int res = mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), userId,
mInsetsController.getRequestedVisibilities(), inputChannel, mTempInsets,
mTempControls);

if (res < WindowManagerGlobal.ADD_OKAY) {
switch (res) {
case WindowManagerGlobal.ADD_BAD_APP_TOKEN:
case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN:
throw new WindowManager.BadTokenException(
"Unable to add window -- token " + attrs.token
+ " is not valid; is your activity running?");
case WindowManagerGlobal.ADD_NOT_APP_TOKEN:
throw new WindowManager.BadTokenException(
"Unable to add window -- token " + attrs.token
+ " is not for an application");

}
}
...
}
}

可以看到res的值是通过mWindowSession.addToDisplayerAsUser方法获取到的。
获取过程如下:
第一步:搞明白mWindowSession对象的获取过程

1
2
3
4
5
6
7
8
9
10
class ViewRootImpl{
final IWindowSession mWindowSession;
public ViewRootImpl(Context context, Display display) {
this(context, display, WindowManagerGlobal.getWindowSession(),
false);
}
public ViewRootImpl(Context cotext,Display display,IWindowSession session,boolean useSfChoreographer){
mWindowSession = sesssion;
}
}

可以看到mWindowSession是通过WindowManagerGlobal.getWindowSession()方法获取的。可以看到这里是通过Binder通信去获取WindowManagerService服务。

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
public static IWindowSession getWindowSession() {
synchronized (WindowManagerGlobal.class) {
if (sWindowSession == null) {
try {
@UnsupportedAppUsage InputMethodManager.ensureDefaultInstanceForDefaultDisplayIfNecessary();
IWindowManager windowManager = getWindowManagerService();
sWindowSession = windowManager.openSession(
new IWindowSessionCallback.Stub() {
@Override
public void onAnimatorScaleChanged(float scale) {
ValueAnimator.setDurationScale(scale);
}
});
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
return sWindowSession;
}
}

class WindowManagerService extends IWindowManager.Stub
implements Watchdog.Monitor, WindowManagerPolicy.WindowManagerFuncs{
public IWindowSession openSession(IWindowSessionCallback callback) {
return new Session(this, callback);
}
}

可以看到iWindowSession其实就是Session对象。

第二步调用addToDisplayAsUser方法

1
2
3
4
5
6
7
8
9
10
class Session{
final WindowManagerService mService;
public int addToDisplayAsUser(IWindow window, WindowManager.LayoutParams attrs,
int viewVisibility, int displayId, int userId, InsetsVisibilities requestedVisibilities,
InputChannel outInputChannel, InsetsState outInsetsState,
InsetsSourceControl[] outActiveControls) {
return mService.addWindow(this, window, attrs, viewVisibility, displayId, userId,
requestedVisibilities, outInputChannel, outInsetsState, outActiveControls);
}
}

可以看到又调用了WindowManagerService的addWindow方法:

1
2
3
4
5
6
7
8
class WindowManagerService{
public int addWindow(Session session, IWindow client, LayoutParams attrs, int viewVisibility,
int displayId, int requestUserId, InsetsVisibilities requestedVisibilities,
InputChannel outInputChannel, InsetsState outInsetsState,
InsetsSourceControl[] outActiveControls) {
}
}

这里就引出了很重要的一个服务WindowManagerService,它管理了当前系统中的所有window窗口,他主管了设备产生的touch事件,因为windowmanagerservice知道当前哪个window最适合处理事件。

来看下WindowmanagerService里面所有的窗口是如何管理的:

5.jpg

现在继续看addWindow整个过程干了什么:

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
class WindowManagerService{
public static final int FIRST_SUB_WINDOW = 1000;
public static final int FIRST_SYSTEM_WINDOW = 2000;
public int addWindow(Session session, IWindow client, LayoutParams attrs, int viewVisibility,
int displayId, int requestUserId, InsetsVisibilities requestedVisibilities,
InputChannel outInputChannel, InsetsState outInsetsState,
InsetsSourceControl[] outActiveControls) {
Arrays.fill(outActiveControls, null);
int[] appOp = new int[1];
final boolean isRoundedCornerOverlay = (attrs.privateFlags
& PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY) != 0;
// 第一步 : 进行权限检查
int res = mPolicy.checkAddPermission(attrs.type, isRoundedCornerOverlay, attrs.packageName,
appOp);
if (res != ADD_OKAY) {
return res;
}
// 第二步:获取 Dispay
final DisplayContent displayContent = getDisplayContentOrCreate(displayId, attrs.token);
if (displayContent == null) {
ProtoLog.w(WM_ERROR, "Attempted to add window to a display that does "
+ "not exist: %d. Aborting.", displayId);
return WindowManagerGlobal.ADD_INVALID_DISPLAY;
}
// 获取窗口类型
final int type = attrs.type;
//第三步: 1000 2000
if (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW) {
parentWindow = windowForClientLocked(null, attrs.token, false);
if (parentWindow == null) {
ProtoLog.w(WM_ERROR, "Attempted to add window with token that is not a window: "
+ "%s. Aborting.", attrs.token);
return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
}
if (parentWindow.mAttrs.type >= FIRST_SUB_WINDOW
&& parentWindow.mAttrs.type <= LAST_SUB_WINDOW) {
ProtoLog.w(WM_ERROR, "Attempted to add window with token that is a sub-window: "
+ "%s. Aborting.", attrs.token);
return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
}

// 避免窗口的重复添加
if (mWindowMap.containsKey(client.asBinder())) {
ProtoLog.w(WM_ERROR, "Window %s is already added", client);
return WindowManagerGlobal.ADD_DUPLICATE_ADD;
}
if (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW) {
parentWindow = windowForClientLocked(null, attrs.token, false);
if (parentWindow == null) {
ProtoLog.w(WM_ERROR, "Attempted to add window with token that is not a window: "
+ "%s. Aborting.", attrs.token);
return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
}
if (parentWindow.mAttrs.type >= FIRST_SUB_WINDOW
&& parentWindow.mAttrs.type <= LAST_SUB_WINDOW) {
ProtoLog.w(WM_ERROR, "Attempted to add window with token that is a sub-window: "
+ "%s. Aborting.", attrs.token);
return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
}
}

}
}
}

第一步: 进行权限检查。看你这个类型的window能够进行创建。
第二步: 获取Display。Display就是屏幕的意思,手机的可显示区域会通过displayId进行标记,每个displayId对应一个display,然后display会被保存在DisplayContent里面。到了这里就要讲到上面的关于token产生的报错问题了。

dd.jpg

在图上可以看到通过token值从DisplayContent里面获取WindowToken值。windowToken是一组窗口的编号,比如我们在activity里面弹出了一个dialog,那么这个时候dialog的windowtoken和activity的token的值就是一样的,因为dialog是activity的子窗口,他们属于一组窗口,所以windowtoken的值是一样的。

1669356626268.jpg这个token是activity的。
1669356765007.jpg这个token是activity上弹的dialog弹窗。

从我们的实验结果能发现,属于一组窗口的时候他们的windowToken的值是一样的。什么叫一组窗口,就是存在父子关系的,比如activity和他的dialog。两个activity间就不是一组窗口,因此他们的windowtoken也不一样。

lQDPJxbn5v0At5XNAtvNBRuwT8cA5lJ9efEDfQPYo8A2AA_1307_731.jpg
在WindowManagerservice.addWindow()里面会为窗口每次去创建WindowState对象,每个windowSate都保存了windowToken值,所以间接的,通过windowToken管理了一组windowState,是怎么实现的呢?
WindowToken继承自WindowContainer,然后在WindowContainer里面有一个WindowList数组,这个数组就存下了相同token下的所有窗口。

captrue.png
lQDPJxbqOR7xeyDNAbrNAxqwqPAI_ypMD0EDgNFGKkDOAA_794_442.jpg

captrue.png

captrue.png
可以看到一组相同的windowToken会通过windowList保存所有的windowState。
5.jpg
经过这么一通分析,上面这张图已经搞懂了WindowToken和WidowState的关系了。但是我现在还有一个疑问这个WindowToken的值是从哪来的?继续分析…..

lQDPJxbqOv9bUtnNAnDNA16ws9drjwKi7p8DgNRZXMBwAA_862_624.jpg

captrue.png
WindowToken是通过IBinder值从DisplayContent获取到的。然后IBinder值是通过保存在WindowManager.LayoutParams里的token值来获取的。那现在就一步步往上追,这个WindowManager.LayoutParams.token的赋值时机:

captrue.png

① 调用者Session:

captrue.png

② 调用者ViewRootImpm.setView

captrue.png

③ 调用者WindowManagerGlobal.addView:

captrue.png

④ 调用者WindowManagerImpl.addView:

captrue.png

⑤ 调用者ActivityThread.handleResumeActivity:

captrue.png

现在分析每步干了什么:
⑤的位置: 首先获取到一个空的LayoutParams,因为在我们每次创建Window对象的时候,都会立马创建一个空的WindManager.LayoutParams对象。然后在⑤位置给我们的window设置了type值WindowManager.LayoutParams.TYPE_BASE_APPLICATION这个常量值是1也就代表这是一个应用窗口。

④的位置: 没干任何事情。直接就掉用了WindowManagerGlobal.addView方法。

③的位置: 对token进行赋值

captrue.png
这里的parentWindow并不为空,因为在Activity的attach阶段,创建了我们的windowManagetImpl对象

103.png

captrue.png

现在看回adjustLayoutParamsForSubWindow:

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
class Window{

void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) {
CharSequence curTitle = wp.getTitle();
// 不符合这里1000 - 1999
if (wp.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
wp.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
if (wp.token == null) {
View decor = peekDecorView();
if (decor != null) {
wp.token = decor.getWindowToken();
}
}
if (curTitle == null || curTitle.length() == 0) {
final StringBuilder title = new StringBuilder(32);
if (wp.type == WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA) {
title.append("Media");
} else if (wp.type == WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA_OVERLAY) {
title.append("MediaOvr");
} else if (wp.type == WindowManager.LayoutParams.TYPE_APPLICATION_PANEL) {
title.append("Panel");
} else if (wp.type == WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL) {
title.append("SubPanel");
} else if (wp.type == WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL) {
title.append("AboveSubPanel");
} else if (wp.type == WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG) {
title.append("AtchDlg");
} else {
title.append(wp.type);
}
if (mAppName != null) {
title.append(":").append(mAppName);
}
wp.setTitle(title);
}
} //这也不符合2000 - 2999
else if (wp.type >= WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW &&
wp.type <= WindowManager.LayoutParams.LAST_SYSTEM_WINDOW) {
if (curTitle == null || curTitle.length() == 0) {
final StringBuilder title = new StringBuilder(32);
title.append("Sys").append(wp.type);
if (mAppName != null) {
title.append(":").append(mAppName);
}
wp.setTitle(title);
}
} //进到这里因为应用级别的type 1 - 999
else {
if (wp.token == null) {
//对于我们的activity这个mContainer是null
wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;
}
if ((curTitle == null || curTitle.length() == 0)
&& mAppName != null) {
wp.setTitle(mAppName);
}
}
if (wp.packageName == null) {
wp.packageName = mContext.getPackageName();
}
if (mHardwareAccelerated ||
(mWindowAttributes.flags & FLAG_HARDWARE_ACCELERATED) != 0) {
wp.flags |= FLAG_HARDWARE_ACCELERATED;
}
}
}

到这里就结束了,LayoutParams该设置的值都设置结束了。

标记1的地方可以看到WindowManagerService有一个成员变量RootWindowContainer。我们都知道WindowManagerService可以管理所有窗口,这就是答案所在。在RootWindowContainer里面有一个数组专门用来保存创建的窗口。

1
2
3
class RootWindowContainer{
protected final WindowList<E> mChildren = new WindowList<E>();
}

标记2的地方,通过token值从RootWindowContainer里面获取对应存在的DisplayContent。在我们创建一个activity的时候,我可以给你讲,这个token的值是null。token的赋值操作可以通过获取Widow的WindowManager.LayoutParams直接进行赋值的:

1
2
3
4
5
6
val p = WindowManager.LayoutParams()
p.flags = WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
p.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL
p.token = applicationContext.getActivityToken()
var mWindowManager = getSystemService(WINDOW_SERVICE) as WindowManager
mWindowManager.addView(view, p)

标记3:通过token获取不到值的时候,就通过displayId来获取,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class RootWindowContainer{
DisplayContent getDisplayContentOrCreate(int displayId) {
// 从WindowList里面取,看能不能取到
DisplayContent displayContent = getDisplayContent(displayId);
if (displayContent != null) {
return displayContent;
}
if (mDisplayManager == null) {
return null;
}
//先获取display
final Display display = mDisplayManager.getDisplay(displayId);
if (display == null) {
return null;
}
//创建一个和display对应的displaycontent对象。
displayContent = new DisplayContent(display, this);
//保存到windowlist里面
addChild(displayContent, POSITION_BOTTOM);
return displayContent;
}
}

其实看到这里我又不懂了,这个displayId是什么,Display又是什么?我没搞懂 (2022年11月24日,今天我搞懂了。)

在android里面display就是一个可以展示的屏幕,displayid就是标记是哪个屏幕。surfaceflinger就是通过displayid将合成的图片分发给指定的屏幕。在我们手机只有一个屏幕的时候这个dispalyid就是0.如果有两个屏幕分别为0 和 1(就是这么个意思,也可能不是0 1。,反正就是两个不同的数字)。有一个屏幕就有一个display对象,有两个屏幕就有两个display对象。

5.jpg

第三步:
当type大于1000,小于1999,说明这个窗口是一个子窗口。如果是子窗口就会在第三步去找到他的父窗口也就是代码里面的parentWindow,activity是应用窗口所以在第三步不会去找他的父窗口。Dialog是一个子窗,这个时候会去找他的父窗口,现在来研究下是怎么找的。

1
2
3
class WindowManagerService{
parentWindow = windowForClientLocked(null, attrs.token, false);
}

可以看到在第二步的时候去判断了窗口类型。

看到这里总结一下:在创建ViewRootImpl对象的时候,会去获取WindowManagerService服务去创建Session对象。然后调用session对象的addToDisplayAsUser方法,他的返回值结果用来表示窗口添加结果情况情况。


现在来总结下全部过程,在ActivityThread的performLaunchActivity阶段会去创建Activity对象,并调用Activity的attach方法。在attach里面会去创建PhoneWindow对象。然后给当前创建的PhoneWindow创建对应的WindowManagerImpl对象,通过方法window.setWindowManager()。创建WindowManagerImpl对象。PhoneWindow持有WindowManagerImpl的引用。WindowManagerImpl也持有window的引用。在这个阶段还涉及了一个很重的参数token:IBinder。每个Activity都有一个对应的token:IBinder。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Activtiy{
public void attach(){
mWindow = PhoneWindow();
mWindow.setWindowManager((WindowManager)context.getSystemService(Context.WINDOW_SERVICE),mToken,mComponent.flattenToString(),(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED)!=0);
}
}
class Window{
public void setWindowManager(WindowManager wm,IBinder appToken,String appName,boolean hardwareAccelerated){
mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}
}
class WindowManagerImpl{
public WindowManagerImpl crateLocalWindowManager(Window parentWindow){
return new WindowManagerImpl(mContext,parentWindow,mWindowContextToken);
}
}

然后在ActivityThread的handleResumeActivity阶段,设置Activity对应的window属性type为应用窗口属性1,然后调用WindowManagerImpl的addView方法。在addView方法又再次调用了WindowManagerGlobal.addView方法。这里需要注意,WindowManagerGlobal对象是一个单例对象,一个进程只会存在一份,然后windowmanagerImpl对象是在每次创建窗口都会新建一个。
接着来到了windowmanagetGlobal.addView方法。在这个时候会进行很重要的一次操作,就是给window设置token:IBinder值。token值用来标记了哪些窗口是一组的,比如activity和他的dialog,他们的token值就是一样的,两个activity的token值就是不一样的。
设置完window的token值,接着创建了ViewRootImpl对象。然后调用了ViewRootImpl的setView方法。这里面干了很重的操作,不仅调用了requestlayout触发了整个view树的measure layout draw操作,还干了件大事就是完成了window窗口的添加。调用方法windowsession.addToDisplayAsUser方法。这里就涉及到了跨进程通信。windowSession是通过跨进程的方式从windowManagerService里面获取到的session对象。接着看addToDisplayToUser方法。这个时候就来到了system_server进程。
这个时候就来到了一个新的领域,android窗口添加过程了。在session调用addDisplayToUser的时候会调用到WindowManagerService.addWindow方法。然后通过displayid或者token值获取对应的displayContent。如果用户手机有多个屏幕的话就会有多个Displaycontent对象。如果只有一个屏幕。那只会有一个DisplayContent对象。通过token从DisplayContent里面获取windowtoken值。然后为窗口创建windowstate对象。通过windowtoken维护着一组windowstate。然后displaycontent维护着一组windowtoken。至此结束。