前言
创建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分析
2.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.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显示,是这样的。
最底的那个绿色的WindowState,就是Dialog的窗口。
把Dialog的Window.type指定为SYSTEM_WINDOW之后,是这样的:
右边最底的那个WindowState就是SYSTEM_WINDOW类型的Dialog窗口,在层级关系上,跟隔壁的ActivityRecord是相等的。
Dialog窗口所在容器,就是刚刚说到的那个即时创建的WindowToken。
其实其他系统级别的窗口也是放置在这个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.ge