Android面试:Android-Window机制原理之Token验证(为什么Application的Context不能show dialog)
概述
注:本文基于Android 10源码,为了文章的简洁性,引用源码的地方可能有所删减。
今天在掘金上看到一篇解析为什么不能使用 Application Context 显示 Dialog
的文章,看完之后感觉作者忽略了一个很重要的对象–parentWindow,因此讲解的时候无法完整地把源码逻辑串起来。在参考了之前对Android-Window机制原理的解析,重新阅读了源码,决定借助这个问题记录一下关于 Android WMS 在 addWindow 的时候Token验证的逻辑,借此也可以说明为什么不能使用 Application Context 显示 Dialog。
Android 不允许使用 Activity 之外的 Context 来显示普通的 Dialog(非 System Dialog 等)。当运行如下代码的时候,会报错:
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):
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):
// 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 类:
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 方法:
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服务):
// 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的方法):
// 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 的调用:
// 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 才刚启动,界面还没有可见。
// 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 方法:
// 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 的相关逻辑:
// 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 的逻辑:
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 的关系。
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)
方法:
// 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 的表演:
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 返回。
我们接着往下看:
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 方法:
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 验证的流程(手里来了个新需求,时间仓促,如果流程图有问题欢迎指正,之前的解析如有问题也请指出改正!觉得不错的可以多点几个赞~):
面试复习笔记:
这份资料我从春招开始,就会将各博客、论坛。网站上等优质的Android开发中高级面试题收集起来,然后全网寻找最优的解答方案。每一道面试题都是百分百的大厂面经真题+最优解答。包知识脉络 + 诸多细节。
节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。
给文章留个小赞,就可以免费领取啦~
戳我领取:Android对线暴打面试指南、超硬核Android面试知识笔记、3000页Android开发者架构师核心知识笔记
《960页Android开发笔记》
《1307页Android开发面试宝典》
包含了腾讯、百度、小米、阿里、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率。
《507页Android开发相关源码解析》
只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。
真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。
听说一键三连的粉丝都面试成功了?如果本篇博客对你有帮助,请支持下小编哦
快速入手通道:(点这里)下载!诚意满满!!!
Android高级面试精选题、架构师进阶实战文档传送门:我的GitHub
整理不易,觉得有帮助的朋友可以帮忙点赞分享支持一下小编~
你的支持,我的动力;祝各位前程似锦,offer不断!!!
本文地址:https://blog.csdn.net/Androiddddd/article/details/110195969