1.前言
记得以前在创建Dialog的时候知道在Dialog的构造方法中需要一个上下文环境,而对这个“上下文”没有具体的概念结果导致程序报错,后来在网上搜索答案,发现Dialog需要的上下文环境只能是activity。由于项目时间的原因没有深入的去研究为什么只能是activity,所以接下来这篇文章将会从源码的角度来彻底的理顺这个问题。
2.报错信息
先来看一个错误示例,在Dialog的构造方法中传入一个Application的上下文环境。看看程序是否报错:
Dialog dialog = new Dialog(getApplication());
TextView textView = new TextView(this);
textView.setText("使用Application创建Dialog");
dialog.setContentView(textView);
dialog.show();
运行程序,程序不出意外的崩溃了,我们来看下报错信息:
Caused by: android.view.WindowManager$BadTokenException: Unable to add window -- token null is not for an application
at android.view.ViewRootImpl.setView(ViewRootImpl.java:517)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:301)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:215)
at android.view.WindowManagerImpl$CompatModeWrapper.addView(WindowManagerImpl.java:140)
这段错误日志,有两点我们需要注意一下
- 程序报了一个BadTokenException异常
- 程序报错是在ViewRootImpl的setView方法中
我们一定很疑惑BadTokenException到底是个啥,在说明这个之前我们首先需要了解Token,在了解了Token的概念之后,再结合ViewRootImpl的setView方法,就能理解BadTokenException这个到底是什么,怎么产生的。
3.Token之旅
Token直译成中文是令牌的意思,android系统中将其作为一种安全机制,其本质是一个Binder对象,在跨进程的通行中充当验证码的作用。比如:在activity的启动过程及界面绘制的过程中会涉及到ActivityManagerService,应用程序,WindowManagerService三个进程间的通信,此时Token在这3个进程中充当一个身份验证的功能,ActivityManagerService与WindowManagerService通过应用程序的activity传过来的Token来分辨到底是控制应用程序的哪个activity。具体来说就是:
- 在启动Activity的流程当中,首先,ActivityManagerService会创建ActivityRecord由其本身来管理,同时会为这个ActivityRecord创建一个IApplication(本质上就是一个Binder)。
- ActivityManagerService将这个binder对象传递给WindowManagerService,让WindowManagerService记录下这个Binder。
- 当ActivityManagerService这边完成数据结构的添加之后,会返回给ActivityThread一个ActivityClientRecord数据结构,中间就包含了Token这个Binder对象。
- ActivityThread这边拿到这个Token的Binder对象之后,就需要让WindowManagerService去在界面上添加一个对应窗口,在添加窗口传给WindowManagerService的数据中WindowManager.LayoutParams这里面就包含了Token。
- 最终WindowManagerService在添加窗口的时候,就需要将这个Token的Binder和之前ActivityManagerService保存在里面的Binder做比较,验证通过说明是合法的,否则,就会抛出BadTokenException这个异常。
到这里,我们就知道BadTokenException是怎么回事了,然后接下来分析为什么使用Application上下文会报BadTokenException异常,而Activity上下文则不会。
4.流程分析
在Activity界面绘制流程这篇文章中曾经说到过,activity的界面最后是通过ViewRootImpl的setView方法连接WindowManagerService,从而让WindowManagerService将界面绘制到手机屏幕上。而从上面的异常日志中其实也可以看出,Dialog的界面也是通过ViewRootImpl的setView连接WindowManagerService,从而完成界面的绘制的。
我们首先来看Dialog的构造方法。不管一个参数的构造方法。两个参数的构造方法,最终都会调用到3个参数的构造方法:
Dialog(@NonNull Context context, @StyleRes int themeResId, boolean
createContextThemeWrapper) {
......
//1.创建一个WindowManagerImpl对象
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
//2.创建一个PhoneWindow对象
final Window w = new PhoneWindow(mContext);
mWindow = w;
//3.使dialog能够响应用户的事件
w.setCallback(this);
w.setOnWindowDismissedCallback(this);
//4.为window对象设置WindowManager
w.setWindowManager(mWindowManager, null, null);
w.setGravity(Gravity.CENTER);
mListenersHandler = new ListenersHandler(this);
}
这段代码可以看出dialog的创建实质上和activity界面的创建没什么两样,都需要完成一个应用窗口Window的创建,和一个应用窗口视图对象管理者WindowManagerImpl的创建。
然后Dialog同样有一个setContentView方法:
public void setContentView(@LayoutRes int layoutResID) {
mWindow.setContentView(layoutResID);
}
依然是调用PhoneWindow的setContentView方法。再接着我们来看下dialog的show方法:
public void show() {
......
//1.得到通过setView方法封装好的DecorView
mDecor = mWindow.getDecorView();
......
//2.得到创建PhoneWindow时已经初始化的成员变量WindowManager.LayoutParams
WindowManager.LayoutParams l = mWindow.getAttributes();
if ((l.softInputMode
& WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) == 0) {
WindowManager.LayoutParams nl = new WindowManager.LayoutParams();
nl.copyFrom(l);
nl.softInputMode |=
WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
l = nl;
}
try {
//3.通过WindowManagerImpl添加DecorView到屏幕
mWindowManager.addView(mDecor, l);
mShowing = true;
sendShowMessage();
} finally {
}
}
这段代码和activity的makeVisable方法类似,这里也不多说了,注释已经大概的写清楚了。然后调用WindowManagerImpl的addView方法:
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mDisplay, mParentWindow);
}
接着调用了WindowManagerGlobal的addView方法,也没什么好说的:
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
......
//1.将传进来的ViewGroup.LayoutParams类型的params转成
WindowManager.LayoutParams类型的wparams
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)
params;
//2.如果WindowManagerImpl是在activity的方法中被创建则不为空
if (parentWindow != null) {
parentWindow.adjustLayoutParamsForSubWindow(wparams);
} else {
......
}
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
......
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
//3.将视图对象view,ViewRootImpl以及wparams分别存入相应集合的对应位置
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
}
// do this last because it fires off messages to start doing things
try {
//4.通过ViewRootImpl联系WindowManagerService将view绘制到屏幕上
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
synchronized (mLock) {
final int index = findViewLocked(view, false);
if (index >= 0) {
removeViewLocked(index, true);
}
}
throw e;
}
}
这段代码在Activity界面绘制流程也已经详细分析了,这里关键说下注释2:
//2.如果WindowManagerImpl是在activity的方法中被创建则不为空
if (parentWindow != null) {
parentWindow.adjustLayoutParamsForSubWindow(wparams);
} else {
......
}
这里会首先判断一个类型为Window的parentWindow 是否为空,如果不为空会通过Window的adjustLayoutParamsForSubWindow方法调整一个类型为WindowManager.LayoutParams的变量wparams的一些属性值。还记得在Token之旅的小结从曾经说过,应用程序请求WindowManagerService服务时会传入一个Token,其实那个Token就会通过Window的adjustLayoutParamsForSubWindow方法存放在wparams的token变量中,也就是说如果没有调用Window的adjustLayoutParamsForSubWindow方法就会导致wparams的token变量为空。然后我们接下来看一下wparams的token变量是如何赋值的:
void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) {
CharSequence curTitle = wp.getTitle();
if (wp.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
wp.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
......
} else {
if (wp.token == null) {
wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;
}
......
}
if (wp.packageName == null) {
wp.packageName = mContext.getPackageName();
}
if (mHardwareAccelerated) {
wp.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
}
这里我们可以看到这段代码首先会做一个判断如果wp.type的值有没有位于WindowManager.LayoutParams.FIRST_SUB_WINDOW与WindowManager.LayoutParams.LAST_SUB_WINDOW之间,如果没有则会给wp.token赋值。wp.type代表窗口类型,有3种级别,分别为系统级,应用级以及子窗口级。而这里是判断是否为子窗口级级别。而Dialog的WindowManager.LayoutParams.type默认是应用级的,因此会走else分支,给wp.token赋值mAppToken。至于mAppToken是什么,我们待会再来分析。
再来回过头来看WindowManagerGlobal的addView方法的注释4,会调用ViewRootImpl的setView方法,我们来看一下,ViewRootImpl是如何连接WindowManagerService传递token的:
public void setView(View view, WindowManager.LayoutParams attrs, View
panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
try {
......
//1.通过binder对象mWindowSession调用WindowManagerService的接口请求
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mInputChannel);
} catch (RemoteException e) {
......
throw new RuntimeException("Adding window failed", e);
} finally {
if (restore) {
attrs.restore();
}
}
......
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?");
//2.如果请求失败(token验证失败)则抛出BadTokenException异常
case WindowManagerGlobal.ADD_NOT_APP_TOKEN:
throw new WindowManager.BadTokenException(
"Unable to add window -- token " + attrs.token
+ " is not for an application");
case WindowManagerGlobal.ADD_APP_EXITING:
throw new WindowManager.BadTokenException(
"Unable to add window -- app for token " +
attrs.token
+ " is exiting");
case WindowManagerGlobal.ADD_DUPLICATE_ADD:
throw new WindowManager.BadTokenException(
"Unable to add window -- window " + mWindow
+ " has already been added");
case WindowManagerGlobal.ADD_STARTING_NOT_NEEDED:
// Silently ignore -- we would have just removed it
// right away, anyway.
return;
case WindowManagerGlobal.ADD_MULTIPLE_SINGLETON:
throw new WindowManager.BadTokenException(
"Unable to add window " + mWindow +
" -- another window of this type already
exists");
case WindowManagerGlobal.ADD_PERMISSION_DENIED:
throw new WindowManager.BadTokenException(
"Unable to add window " + mWindow +
" -- permission denied for this window type");
case WindowManagerGlobal.ADD_INVALID_DISPLAY:
throw new WindowManager.InvalidDisplayException(
"Unable to add window " + mWindow +
" -- the specified display can not be found");
case WindowManagerGlobal.ADD_INVALID_TYPE:
throw new WindowManager.InvalidDisplayException(
"Unable to add window " + mWindow
+ " -- the specified window type is not valid");
}
throw new RuntimeException(
"Unable to add window -- unknown error code " + res);
}
......
}
}
}
这段代码有两处需要注意:
- 注释1会通过一个mWindowSession的binder对象请求WindowManagerService服务,传递一个类型为WindowManager.LayoutParams的变量mWindowAttributes到WindowManagerService,mWindowAttributes里面装有代表当前activity的token对象。然后通过WindowManagerService服务创建屏幕视图。
- 注释2 会根据请求WindowManagerService服务的返回结果判断是否请求成功,如果请求失败会抛出异常,注释的地方就是在文章开头示例抛出的异常。此时attrs.token为空。如果创建dialog的上下文环境改为activity的为什么就不为空呢?
5.创建Dialog的上下文Activity为何与众不同
从上文的分析中可以看出attrs.token的赋值在Window的adjustLayoutParamsForSubWindow方法中。而Dialog默认的WindowManager.LayoutParams.type是应用级别的,因此,如果能进入这个方法内,attrs.token肯定能被赋值。现在只有一种情况,如果不是activity的上下文环境就没有进入到这个方法内。这时我们再看WindowManagerGlobal的addView方法的注释2:
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
......
//2.如果WindowManagerImpl是在activity的方法中被创建则不为空
if (parentWindow != null) {
parentWindow.adjustLayoutParamsForSubWindow(wparams);
} else {
......
}
......
}
从这里看出如果Window类型的parentWindow为空,就不会进入adjustLayoutParamsForSubWindow方法。从而可以得出结论如果不是activity的上下文环境WindowManagerGlobal的第四个参数parentWindow为空。紧接着我们再来分析为什么其他的上下文会导致parentWindow为空。
WindowManagerGlobal调用addView方法在WindowManagerImpl的addView方法中
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mDisplay, mParentWindow);
}
WindowManagerImpl的addView方法在Dialog的首位方法中调用:
public void show() {
......
try {
mWindowManager.addView(mDecor, l);
mShowing = true;
sendShowMessage();
} finally {
}
}
对比这两个方法。可以看出WindowManagerImpl的addView方法调用WindowManagerGlobal的addView方法是多出来了两个参数mDisplay, mParentWindow,我们只看后一个,多了一个Window类型的mParentWindow,可以一mParentWindow并不是在Dialog的show方法中赋值的。那么它在哪赋值呢?在WindowManagerImpl类中搜索mParentWindow发现它在WindowManagerImpl的两个参数的构造方法中被赋值。从这里我们可以猜测,如果是使用的activity上下文,那么在创建WindowManagerImpl实例的时候用的是两个参数的构造方法,而其他的上下文是用的一个参数的构造方法。现在问题就集中到了WindowManagerImpl是如何被创建的了。
我们再回过头来看Dialog的构造方法中WindowManagerImpl是如何创建的:
Dialog(@NonNull Context context, @StyleRes int themeResId, boolean
createContextThemeWrapper) {
......
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
......
}
然后分别查看activity的getSystemService方法,和Application的getSystemService方法:
- activity的getSystemService方法
@Override
public Object getSystemService(@ServiceName @NonNull String name) {
......
if (WINDOW_SERVICE.equals(name)) {
return mWindowManager;
} else if (SEARCH_SERVICE.equals(name)) {
ensureSearchManager();
return mSearchManager;
}
return super.getSystemService(name);
}
在这个方法中直接返回了activity的mWindowManager对象,还记得在Activity界面绘制流程说到过activity的mWindowManager对象在activity的attach方法中:
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor) {
......
mWindow.setWindowManager((WindowManager)context.getSystemService(Context.WINDOW_SERVICE),mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
......
}
我们再看Window的setWindowManager方法:
public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
boolean hardwareAccelerated) {
//1.将ActivityManagerService传过来的Token保存到mAppToken中
mAppToken = appToken;
//2.创建WindowManagerImpl
mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}
这段代码两个地方需要注意,一是前ActivityManagerService传过来的Token赋值给Winow的mAppToken,这个token最后会保存到attr.token,具体操作在Window的adjustLayoutParamsForSubWindow方法中。二是调用WindowManagerImpl的createLocalWindowManager方法创建WindowManagerImpl:
public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
return new WindowManagerImpl(mDisplay, parentWindow);
}
到这里就可以看出如果创建Dialog的上下文是activity,则会调用WindowManagerImpl两个参数的构造方法,从而导致parentWindow不为空。
- Application的getSystemService方法:
由于Application是Context的子类,所以Application的getSystemService最终会调到ContextImpl的getSystemService方法(Application与ContextImpl的关系,以后会详细讨论):
@Override
public Object getSystemService(String name) {
return SystemServiceRegistry.getSystemService(this, name);
}
直接调用了SystemServiceRegistry的getSystemService方法,这个方法又会得到匿名内部类CachedServiceFetcher<WindowManager>的createService方法的返回值。
@Override
public WindowManager createService(ContextImpl ctx) {
return new WindowManagerImpl(ctx.getDisplay());
}});
从这个方法中可以看出上下文为Application时,调用的是WindowManagerImpl的一个参数的构造方法,从而parentWindow为空。
到这里在dialog的创建过程中上下文不同导致不同的结果基本上分析完了。下面我们再来总结一些。
6.总结
在创建dialog时,如果传入构造方法不是一个activity类型的上下文,则导致WindowManagerImpl类型为Window的变量mParentWindow,从而导致WindowManagerGlobal的addView不会调用Window的adjustLayoutParamsForSubWindow方法,从而不会给attr.token赋值,导致在WindowManagerService服务中的身份验证失败,抛出BadTokenException异常。