创建Dialog的时候知道在Dialog的构造方法中需要一个上下文环境,而对这个“上下文”没有具体的概念结果导致程序报错,于是发现Dialog需要的上下文环境只能是activity。
前言
创建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");  
相关推荐
0评论