前言
创建Dialog的时候知道在Dialog的构造方法中需要一个上下文环境,而对这个“上下文”没有具体的概念结果导致程序报错,
于是发现Dialog需要的上下文环境只能是activity。
所以接下来这篇文章将会从源码的角度来彻底的理顺这个问题;
一、Dialog创建失败
在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这个到底是什么,怎么产生的;
二、Token分析
1、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上下文则不会
2、为什么非要一个Token
因为在WMS那边需要根据这个Token来确定Window的位置(不是说坐标),如果没有Token的话,就不知道这个窗口应该放到哪个容器上了;
因为非Activity的Context它的WindowManger没有ParentWindow,导致在WMS那边找不到对应的容器,也就是不知道要把Dialog的Window放置在何处。
还有一个原因是没有SYSTEM_ALERT_WINDOW权限(当然要加权限啦,DisplayArea.Tokens的子容器,级别比普通应用的Window高,也就是会显示在普通应用Window的前面,如果不加权限控制的话,被滥用还得了)。
在获得SYSTEM_ALERT_WINDOW权限并将Dialog的Window.type指定为SYSTEM_WINDOW之后能正常显示,是因为WMS会为SYSTEM_WINDOW类型的窗口专门创建一个WindowToken(这下就有容器了),并放置在DisplayArea.Tokens里面(这下知道放在哪里了);
三、创建dialog流程分析
1、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;
- }
- }
- //2.如果WindowManagerImpl是在activity的方法中被创建则不为空
- if (parentWindow != null) {
- parentWindow.adjustLayoutParamsForSubWindow(wparams);
- } else {
- ......
- }
2、这里会首先判断一个类型为Window的parentWindow 是否为空,如果不为空会通过Window的adjustLayoutParamsForSubWindow方法调整一个类型为WindowManager.LayoutParams的变量wparams的一些属性值。应用程序请求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是什么,我们待会再来分析。
3、看WindowManagerGlobal的addView方法的,会调用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);
- }
- ......
- }
- }
- }
这段代码有两处需要注意:
- 会通过一个mWindowSession的binder对象请求WindowManagerService服务,传递一个类型为WindowManager.LayoutParams的变量mWindowAttributes到WindowManagerService,mWindowAttributes里面装有代表当前activity的token对象。然后通过WindowManagerService服务创建屏幕视图。
- 会根据请求WindowManagerService服务的返回结果判断是否请求成功,如果请求失败会抛出异常,注释的地方就是在文章开头示例抛出的异常。此时attrs.token为空。如果创建dialog的上下文环境改为activity的为什么就不为空呢?
四、分析创建Dialog的上下文Activity为何与众不同
1、上文的分析中可以看出attrs.token的赋值在Window的adjustLayoutParamsForSubWindow方法中。而Dialog默认的WindowManager.LayoutParams.type是应用级别的,因此,如果能进入这个方法内,attrs.token肯定能被赋值。现在只有一种情况,如果不是activity的上下文环境就没有进入到这个方法内。这时我们再看WindowManagerGlobal的addView方法的:
- 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的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);
- ......
- }
2、我们再看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不为空。
3、Application的getSystemService方法:
由于Application是Context的子类,所以Application的getSystemService最终会调到ContextImpl的getSystemService方法
- @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时,如果传入构造方法不是一个activity类型的上下文,则导致WindowManagerImpl类型为Window的变量mParentWindow,从而导致WindowManagerGlobal的addView不会调用Window的adjustLayoutParamsForSubWindow方法,从而不会给attr.token赋值,导致在WindowManagerService服务中的身份验证失败,抛出BadTokenException异常;
- Show一个普通的Dialog需要的并不是Activity本身,而是一个容器的token,我们平时会传Activity,只不过是Activity刚好对应WMS那边的一个WindowState的容器而已;
本文转载自微信公众号「Android开发编程」