概述 注:本文基于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() 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 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 { static class Token extends IApplicationToken .Stub { 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(); } void createWindowContainer () { mWindowContainerController = new AppWindowContainerController(taskController, appToken, ); } public AppWindowContainerController (TaskWindowContainerController taskController, IApplicationToken token, ) { atoken = createAppWindow(mService, token, ); } AppWindowToken createAppWindow (WindowManagerService service, IApplicationToken token, ) { return new AppWindowToken(service, token, ); } 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(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 ); } void reParentWindowToken (WindowToken token) { addWindowToken(token.token, token); } private void addWindowToken (IBinder binder, WindowToken token) { 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 public Object getSystemService (@ServiceName @NonNull String name) { if (WINDOW_SERVICE.equals(name)) { return mWindowManager; } } public Object getSystemService (String name) { return SystemServiceRegistry.getSystemService(this , name); } 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 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(); } 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 ); } 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 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 private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();@Override public void addView (@NonNull View view, @NonNull ViewGroup.LayoutParams params) { mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow); } public void addView (View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) { final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params; if (parentWindow != null ) { 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 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 { 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 public ViewRootImpl (Context context, Display display) { mContext = context; mWindow = new W(this ); mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this , mHandler, this , context); } 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); 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 ; WindowToken token = displayContent.getWindowToken(hasParent ? parentWindow.mAttrs.token : attrs.token); 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 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(); if (decor != null ) { wp.token = decor.getWindowToken(); } } } } 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) { parentWindow = windowForClientLocked(null , attrs.token, false ); if (parentWindow == null ) { return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN; } if (parentWindow.mAttrs.type >= FIRST_SUB_WINDOW && parentWindow.mAttrs.type <= LAST_SUB_WINDOW) { 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 ; 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; } } } WindowToken getWindowToken (IBinder binder) { 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 验证的流程(手里来了个新需求,时间仓促,如果流程图有问题欢迎指正,之前的解析如有问题也请指出改正!):