0%

Android-Window机制原理之Token验证

概述

注:本文基于Android 10源码,为了文章的简洁性,引用源码的地方可能有所删减。

今天在掘金上看到一篇解析为什么不能使用 Application Context 显示 Dialog的文章,看完之后感觉作者忽略了一个很重要的对象–parentWindow,因此讲解的时候无法完整地把源码逻辑串起来。在参考了之前对Android-Window机制原理的解析,重新阅读了源码,决定借助这个问题记录一下关于 Android WMS 在 addWindow 的时候Token验证的逻辑,借此也可以说明为什么不能使用 Application Context 显示 Dialog。

Android 不允许使用 Activity 之外的 Context 来显示普通的 Dialog(非 System Dialog 等)。当运行如下代码的时候,会报错:

1
2
3
4
5
6
7
8
9
10
val dialog = Dialog(applicationContext)
dialog.show()

// ------- error -------
android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?
at android.view.ViewRootImpl.setView(ViewRootImpl.java:840)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:356)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:94)
at android.app.Dialog.show(Dialog.java:329)
// ...

如果添加一行代码,发现可以show成功(注意要在window?.attributes?.token被赋值后调用,可以使用延时或者View.post调用dialog.show):

1
2
3
val dialog = Dialog(applicationContext)
dialog.window?.attributes?.token = window?.attributes?.token
dialog.show()

接下来从源码角度来解析Token验证相关的逻辑。在开始这部分的内容之前,最好对 startActivity 启动源码和 Window 机制的原理有一定的理解,这里先将相关的流程做个梳理。从Android-Activity启动流程中可知,startActivity 的流程中与 Token 相关的步骤简要描述如下:

App进程调用 Context.startActivity 方法,然后交由 AMS(system_server进程) 做一些处理,下面的 Token 创建便是在这里完成的,此外,如果需要启动的 Activity 是一个新的进程,那么 system_server 会向 zygote 发起创建新进程的请求,在目标进程创建成功后,逻辑就由AMS转到了目标Activity进程,目标进程的 ActivityThread 会调用 performLaunchActivity 方法来创建目标 Activity 实例,然后调用 Activity.attach 方法,下面的 WindowManager 对象便是在这里创建的,后面会陆续回调 Activity 的生命周期方法,其中在 onCreate 的 setContentView 中创建了一个 DecorView 对象,然后在 onResume 回调完成后,会通过 WindowManager.addView 添加 DecorView 对象(见Android-Window机制原理,通过Binder调用,借助WMS完成)。

在大致了解了这个流程后(上面的流程是简化的,如果不想阅读 startActivity 相关源码的话可以先记住上面的流程,接下来的解析会用到),接下来看看Token是怎么在各个阶段去工作的。关于Binder相关的可以参考这里:Android-Binder原理系列,简言之就是 Binder IPC 方式是一个 C/S 架构,服务端和客户端进程分别持有 Binder 的引用与代理,二者之间可以跨进程调用。

最后的总结部分会将这个流程输出为一个流程图,如有问题,欢迎留言指正!

Token创建

根据上面的流程,我们先从AMS开始看一看Token是怎么创建的,如果阅读过 Activity 启动的源码的话,可以知道在 ActivityStarter.startActivity 方法中有如下代码(此时处于system_server进程的AMS线程,与WMS同进程不同线程,可以参考Android-init-zygote):

1
2
3
4
5
6
7
8
9
// ActivityStarter
private int startActivity(/*...*/) {
// ...
ActivityRecord r = new ActivityRecord(mService, callerApp, callingPid, callingUid,
callingPackage, intent, resolvedType, aInfo, mService.getGlobalConfiguration(),
resultRecord, resultWho, requestCode, componentSpecified, voiceSession != null,
mSupervisor, checkedOptions, sourceRecord);
// ...
}

然后我们看看 ActivityRecord 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
final class ActivityRecord extends ConfigurationContainer implements AppWindowContainerListener {

// Binder 服务端对象
static class Token extends IApplicationToken.Stub {
// 持有外部 ActivityRecord 的弱引用
private final WeakReference<ActivityRecord> weakActivity;
private final String name;

Token(ActivityRecord activity, Intent intent) {
weakActivity = new WeakReference<>(activity);
name = intent.getComponent().flattenToShortString();
}
// ...
}

ActivityRecord(/*...*/) {
appToken = new Token(this, _intent);
// ...
}
}

因此在 startActivity 过程中,ActivityRecord 对象中的 appToken 被实例化了。接着再往下走,来到了 ActivityStack.startActivityLocked 方法:

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
void startActivityLocked(ActivityRecord r, ActivityRecord focusedTopActivity,
boolean newTask, boolean keepCurTransition, ActivityOptions options) {
// ...
r.createWindowContainer();
// ...
}

// ActivityRecord
void createWindowContainer() {
mWindowContainerController = new AppWindowContainerController(taskController, appToken, /*...*/);
// ...
}

// AppWindowContainerController
public AppWindowContainerController(TaskWindowContainerController taskController, IApplicationToken token, /*...*/) {
atoken = createAppWindow(mService, token, /*...*/);
}

AppWindowToken createAppWindow(WindowManagerService service, IApplicationToken token, /*...*/) {
return new AppWindowToken(service, token, /*...*/);
}

// AppWindowToken --> WindowToken
AppWindowToken(WindowManagerService service, IApplicationToken token, /*...*/) {
super(service, token != null ? token.asBinder() : null, TYPE_APPLICATION, /*...*/);
appToken = token;
mVoiceInteraction = voiceInteraction;
mFillsParent = fillsParent;
mInputApplicationHandle = new InputApplicationHandle(this);
}

// WindowToken
WindowToken(WindowManagerService service, IBinder _token, int type, /*...*/) {
super(service);
token = _token;
windowType = type;
mPersistOnEmpty = persistOnEmpty;
mOwnerCanManageAppTokens = ownerCanManageAppTokens;
mRoundedCornerOverlay = roundedCornerOverlay;
onDisplayChanged(dc);
}

void onDisplayChanged(DisplayContent dc) {
dc.reParentWindowToken(this);
// ...
}

// DisplayContent
void reParentWindowToken(WindowToken token) {
// ...
addWindowToken(token.token, token);
}

private void addWindowToken(IBinder binder, WindowToken token) {
// HashMap<IBinder, WindowToken> mTokenMap
// key--ActivityRecord.Token(IApplicationToken.Stub); value--WindowToken
mTokenMap.put(binder, token);
// ...
}

上面的代码只给出了关键的步骤,可以清楚地看到,客户端进程调用 startActivity 去启动一个 Activity,然后在AMS(system_server进程的AMS线程)的处理流程中,创建了一个 IApplicationToken.Stub 的对象,这是一个 Binder 服务端,然后又创建了一个 AppWindowToken 对象,并将其存入 DisplayContent.mTokenMap 中。这里 AMS 和 WMS 都处于 system_server 进程中,后续 WMS.addWindow 中会使用到 mTokenMap 来检验 Token(此处关于 mTokenMap 是否存在线程安全问题,有兴趣可以深入看看细节)。

WindowManager对象获取

然后看一下使用 Activity 的 Context 调用 getSystemService 方法和使用 Application 的 Context 调用 getSystemService 方法的区别(只针对WMS服务):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Activity
public Object getSystemService(@ServiceName @NonNull String name) {
if (WINDOW_SERVICE.equals(name)) {
return mWindowManager;
}
// ...
}

// Application 调用的是父类 ContextImpl 的方法
// ContextImpl
public Object getSystemService(String name) {
return SystemServiceRegistry.getSystemService(this, name);
}

// SystemServiceRegistry
registerService(Context.WINDOW_SERVICE, WindowManager.class, new CachedServiceFetcher<WindowManager>() {
@Override
public WindowManager createService(ContextImpl ctx) {
return new WindowManagerImpl(ctx);
}});

Activity 中获取的是 mWindowManager 对象,它在 Activity.attach 方法中赋值,从Android-Activity启动原理可知该方法是在 startActivity 过程中回调的(AMS处理后,通过Binder调用目标Activity的方法):

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
// Activity
final void attach(/*...*/) {
attachBaseContext(context);
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowManager((WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(), (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
mWindowManager = mWindow.getWindowManager();
// ...
}

// Window
public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
boolean hardwareAccelerated) {
mAppToken = appToken;
mAppName = appName;
if (wm == null) {
wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
}
mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}

// WindowManagerImpl
public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
return new WindowManagerImpl(mContext, parentWindow);
}

public WindowManagerImpl(Context context) {
this(context, null);
}

private WindowManagerImpl(Context context, Window parentWindow) {
mContext = context;
mParentWindow = parentWindow;
}

从上面的源码可以看出使用 Activity 的 Context 调用 getSystemService 方法和使用 Application 的 Context 调用 getSystemService 方法的区别在于: Activity 中的 WindowManager 对象中 parentWindow 为 Activity 中的 PhoneWindow 对象,而 Application 中的 WindowManager 对象中 parentWindow 为 null。

至于上面的 mToken 对象(这是客户端进程的mToken,与上面AMS端创建的Token对象不一样!)从何而来,可以看看 Activity.attach 的调用:

1
2
3
4
// ActivityThread
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
activity.attach(appContext, this, getInstrumentation(), r.token, /*...*/);
}

可以看到 mToken 对象是 ActivityClientRecord.token,注意到这时我们所处的是目标Activity所在的进程,直接从Activity启动源码解析可以知道,这个 ActivityClientRecord.token 是 AMS 中 ActivityRecord.token 的 Binder 代理,具体的对象传递代码不贴了,贴多了代码看着无聊,这里想看的可以直接参考之前的博客。

总而言之就是,目标Activity进程中的 mAppToken 是一个 Binder 代理对象,其Binder服务端是 AMS 的 ActivityRecord 中的 Token 对象(IApplicationToken.Stub)。

WindowManager.addView

接着我们就到了 WindowManager.addView 添加 DecorView 的流程了,此时 Activity 才刚启动,界面还没有可见。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// WindowManagerImpl
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();

@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}

// WindowManagerGlobal.java
public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
if (parentWindow != null) {
// 由上面可知在Activity中的WindowManager里,parentWindow是PhoneWindow对象
parentWindow.adjustLayoutParamsForSubWindow(wparams);
}
// ...
ViewRootImpl root = new ViewRootImpl(view.getContext(), display);
// ...
root.setView(view, wparams, panelParentView);
}

上面的代码处于Activity所在的客户端进程,由于 parentWindow 不为空,是PhoneWindow对象,因此看看 Window.adjustLayoutParamsForSubWindow 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Window
void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) {
if (wp.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
wp.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
// ...
} else if (wp.type >= WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW &&
wp.type <= WindowManager.LayoutParams.LAST_SYSTEM_WINDOW) {
// ...
} else {
// 由于这是Application级别的window,因此走这个流程
if (wp.token == null) {
wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;
}
}
}

可以看到 LayoutParams.token 取的是上面 Token 对象在客户端的 Binder 代理!记住这里,下面要用的。接下来看一下 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
26
27
28
29
30
31
32
33
// ViewRootImpl
public ViewRootImpl(Context context, Display display) {
mContext = context;
// 继承于IWindow.Stub的W对象
mWindow = new W(this);
mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this, context);
// ...
}

// View.AttachInfo
AttachInfo(IWindowSession session, IWindow window, Display display,
ViewRootImpl viewRootImpl, Handler handler, Callbacks effectPlayer,
Context context) {
mWindow = window;
mWindowToken = window.asBinder();
mViewRootImpl = viewRootImpl;
// ...
}

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
mWindowAttributes.copyFrom(attrs);
// 通过mWindowSession会调用到WMS.addWindow
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), mWinFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel);
// ...
}
}
}

上面 View.AttachInfo 构造方法可以注意一下,下面也要用,这个类表示 View 的 attach 信息。接下来就到了 WMS 的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
public int addWindow(Session session, IWindow client, int seq, LayoutParams attrs, /*...*/) {
synchronized(mWindowMap) {
AppWindowToken atoken = null;
final boolean hasParent = parentWindow != null;
// 这个逻辑先不看,在后面Dialog添加再说
WindowToken token = displayContent.getWindowToken(hasParent ? parentWindow.mAttrs.token : attrs.token);
// 创建WindowState实例
final WindowState win = new WindowState(this, session, client, token, parentWindow,
appOp[0], seq, attrs, viewVisibility, session.mUid, session.mCanAddInternalSystemWindow);
mWindowMap.put(client.asBinder(), win);
// ...
}
}

这里的client是上面 ViewRootImpl 中的 Binder 服务端–mWindow。上述代码只贴了相关的逻辑,startActivity 流程中添加 Window 的过程可以只看到这里。WMS.addWindow方法中的WindowToken token对象便是用来做检验的,后面要讲的 Dialog 崩溃便是在这里!

Dialog.show

在上面大致讲了一下 Activity 启动后,添加 DecorView 的过程。接着我们可以开始研究 Dialog 调用 show 方法后发生了什么,以及它与 Token 和 Activity/Application 的关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
final Window w = new PhoneWindow(mContext);
mWindow = w;
// ...
}

public void show() {
onStart();
// ...
mDecor = mWindow.getDecorView();
mWindowManager.addView(mDecor, l);
mShowing = true;
}

可知也是调用的 WM.addView 方法。于是可以接着看上面给出的 WindowManagerGlobal.addView 方法,这里分两种情况:

  • 传给 Dialog 的是 Activity 上下文,则 WindowManager 的 parentWindow 不为空
  • 传给 Dialog 的是 Application 上下文,则 WindowManager 的 parentWindow 为空

我们已经知道,根据 parentWindow 是否为空,会选择是否调用其 parentWindow.adjustLayoutParamsForSubWindow(wparams) 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Window
void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) {
if (wp.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
wp.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
// 这里是普通对话框,因此走这个流程
if (wp.token == null) {
View decor = peekDecorView(); // 通过PhoneWindow拿到DecorView对象
if (decor != null) {
wp.token = decor.getWindowToken();
}
}
}
// ...
}

// View
public IBinder getWindowToken() {
return mAttachInfo != null ? mAttachInfo.mWindowToken : null;
}

上面的 getWindowToken 方法返回的就是我们前面看到的 mAttachInfo.mWindowToken 对象!也就是之前 ViewRootImpl 中创建的 mWindow 对象(一个Binder服务端)。而如果这个 Context 是 Application,那么 wp.token 将会是 null。

WMS.addView

于是我们接着看展示 Dialog 过程中,WMS 的表演:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public int addWindow(Session session, IWindow client, int seq, LayoutParams attrs, /*...*/) {
WindowState parentWindow = null;
if (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW) {
// 普通Dialog会走这个流程,获取parentWindow对象
parentWindow = windowForClientLocked(null, attrs.token, false);
if (parentWindow == null) {
// parentWindow为null,返回bad
return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
}
if (parentWindow.mAttrs.type >= FIRST_SUB_WINDOW && parentWindow.mAttrs.type <= LAST_SUB_WINDOW) {
// parentWindow为普通window,返回bad
return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
}
}
// ...
}

final WindowState windowForClientLocked(Session session, IBinder client, boolean throwOnError) {
WindowState win = mWindowMap.get(client);
// ...
return win;
}

我们先看看 parentWindow 的逻辑,windowForClientLocked 方法中 client 参数即是上一节讲的 wp.token

  • Context是Application的情况,它为null,则WMS中返回的parentWindow也是null,那么添加 Window 失败,返回 bad code。
  • Context是Activity的情况,它是 ViewRootImpl 中 mWindow 对象的 Binder 代理,在上面解析 startActivity 添加 DecorView 的过程中我们看到,我们为 mWindowMap 添加了一个 key 为 mWindow 对象代理,value 为当时创建的 WindowState 对象,也将作为这次的 parentWindow 返回。

我们接着往下看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public int addWindow(Session session, IWindow client, int seq, LayoutParams attrs, /*...*/) {
// ...
AppWindowToken atoken = null;
final boolean hasParent = parentWindow != null;
// 取parentWindow.mAttrs.token
WindowToken token = displayContent.getWindowToken(hasParent ? parentWindow.mAttrs.token : attrs.token);
if (token == null) {
if (rootType >= FIRST_APPLICATION_WINDOW && rootType <= LAST_APPLICATION_WINDOW) {
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
if (rootType == TYPE_INPUT_METHOD) {
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
}
// ...
}

// DisplayContent
WindowToken getWindowToken(IBinder binder) {
// key--ActivityRecord.Token(IApplicationToken.Stub); value--WindowToken
return mTokenMap.get(binder);
}

对于传入是 Activity 的情况,因为 parentWindow 不为空,可知 hasParent = true,从上面可以知道,parentWindow 的 LayoutParams.token 取的是 AMS创建的 Token 对象在客户端的 Binder 代理,而 mTokenMap 中早就添加过这个 key 的元素了!因此这里如果是 Activity 的话,则返回的 token 是有值的,其值为 AMS 创建的 AppWindowToken 对象。

现在应该知道,为啥在最开始我们加了这行代码 dialog.window?.attributes?.token = window?.attributes?.token 后,Dialog就可以正常展示了!因为我们手动给 Dialog 自己设置了 token,token 值就是启动 Activity 时创建的 mAppToken(代理)。

异常抛出

上面WMS返回后,回到 ViewRootImpl.setView 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
res = mWindowSession.addToDisplay(/*...*/)
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?");
// ...
}
}
// ...
}
}
}

看到这里,我们也终于找到最开始看到的崩溃日志是怎么回事了。

总结

用一张图来总结一下 Token 验证的流程(手里来了个新需求,时间仓促,如果流程图有问题欢迎指正,之前的解析如有问题也请指出改正!):

Window-Token验证