大家好,相信大家在使用Dialog时,都有一个非常基本的认知:就是Dialog的context只能是Activity,而不能是Application,不然会导致弹窗崩溃:
这个Exception几乎属于是每个Android开发初学者都会碰到的,但是。
前几天研究项目代码发现 , Application
作为 Dialog
的 context
竟然不会崩溃?!!这句话说出来和本篇文章标题严重不符哈,这不是赤裸裸的打脸了吗。先别急,请大家跟着我的脚步,相信阅读完本篇文章就可以解答目前你心目中最大的两个疑惑:
- 如标题所言,为啥Application无法作为Dialog的context并导致崩溃?
- 项目中为啥又发现,Application作为Dialog的context可以正常显示弹窗?
一. 窗口(包括Activity和Dialog)如何显示的?
这里怕有些童鞋不了解窗口(包括Activity和Dialog的)的显示流程,先简单的介绍下:
不管是Activity界面的显示还是DIalog的窗口显示,都会调用到WindowManagerImpl#addView()
方法,这个方法经过一连续调用,会走到ViewRootImpl#setView()
方法中。
在这个方法中,我们最终会调用到IWindowSession#addToDisplayAsUser()
方法,这个方法是一个跨进程的调用,经过一番折腾,最终会执行到WMS的addWindow()
方法。
在这个方法中会将窗口的信息进行保存管理,并且对于窗口的信息进行校验,比如上面的崩溃信息:"BadTokenException: Unable to add window"就是由于在这个方法中检验失败导致的;另外也是在这个方法中将窗口和Surface、Layer绘制建立起了连接(这句话说的可能不标准,主要对这块了解不多,懂得大佬可以评论分享下)。
接着开始在ViewRootImpl#setView()
执行requestLayout()方法,开始进行渲染绘制等。
有了上面的简单介绍,接下来我们就开始先分析为啥Application作为Dialog的context会异常。
二. 窗口离不开的WindowManagerImpl
上面也说了,窗口只要显示,就得借助WindowManagerImpl#addView()
方法,而WindowManagerImpl
创建流程在 Application
和 Activity
的差异,就是 Application
作为 Dialog
的 context
会异常的核心原因。
我们就从下面方法作为入口进行分析:
scss
context.getSystemService(WINDOW_SERVICE)
1. Application下WindowManagerImpl
的创建
对于Application而言,getSystemService()
方法的调用,最终会走到父类ContextWrapper
中:
而这个mBase
属性对应的类为ContextImpl
对象,对应ContextImpl#getSystemService()
:
对应SystemServiceRegistry#getSystemService
:
SYSTEM_SERVICE_FETCHERS
是一个Map集合,对应的key为服务的名称,value为服务的实现方式:
Android会在SystemServiceRegistry初始化的时候将各种服务以及服务的实现方法注册到这个集合中:
接下来看下咱们关心的WindowManager
服务的注册方式:
到了这里,咱们就明白了,调用context.getSystemService(WINDOW_SERVICE)
会返回一个WindowManagerImpl对象,核心点就在于WindowManagerImpl的构造函数,可以看到构造函数只传入了一个ContextImpl对象,我们看下其构造方法:
本篇文章重要的地方来了:通过这种方法创建的WindowManagerImpl对象,其 mParentWindow
属性是null的。
2. Activity下WindowManagerImpl
的创建
Activity重写了getSystemService()
方法:
而mWindowManager属性的赋值是发生在Activity#attach()
方法中:
这个mWindow属性对应的类型为Window类型(其唯一实现类为大家耳熟能详的PhoneWindow,其创建时机和Activity创建的时机是一起的),走进去看下:
经过一层层的调用,最终咱们的WindowManager是通过WindowManagerImpl#createLocalWindowManager
创建的,并且参数传入的是当前的Window对象,即PhoneWindow。
可以看到,该方法最终帮助咱们创建了WindowManagerImpl对象,关键点是其mParentWindow属性的值为上面传入的PhoneWindow,不为null。
小结:
Activity获取到的WindManager服务,即WindowManagerImpl的mParentWindow属性不为空,而Application获取的mParentWindow属性为null。
文章开头我们简单介绍了窗口的显示流程,同时又知道实现窗口添加的关键类WindowManagerImpl的来头,有了这些铺垫,接下来我们就对窗口的显示进行一个比较深入的分析。
三. 深入探究窗口的显示流程
这里我们就从WindowManagerGlobal#addView()
方法说起,它是WindowManagerImpl#addView()
方法的真正实现者。
WindowManagerImpl#addView():
WindowManagerGlobal#addView():
这一分析,就进入到了本篇文章最重要的一个方法的分析,如上面红框所示。
前面我们有讲过,对于Application获取的WindowManagerImpl,其mParentWindow属性为null,而Activity对应的mParentWindow不为null。
- 如果当前为Activity的窗口,或者借助Activity作为Context显示的Dialog窗口,其会走入到方法
adjustLayoutParamsForSubWindow()
中,对应的实现类为Window
:
type
为窗口的类型,对于Activity的窗口还是对于Dialog的窗口,其对应类型为都为2(TYPE_APPLICATION),所以最终都会走到红框中的位置,最终给window对应的layoutparam对象的token属性赋值为mAppToken。
这个mAppToken可以简单理解为窗口的一种凭证,它是AMS在startActivity流程的时候被初始化的,然后传递给应用侧,最终再用来WMS进行窗口检验的 。其中在AMS的startActivity流程中,会将这个AppToken作为key,并构造一个WindowToken对象作为value,写入到 DisplayContent#mTokenMap集合中 ,这部分详细的源码分析可以参考文章:Android高工面试(难度:四星):为什么不能使用 Application Context 显示 Dialog?
- 如果当前为application作为context显示的Dialog,mParentWindow为null,那就走不到
adjustLayoutParamsForSubWindow()
方法中,自然其Window#LayoutParam#token属性就是null。
咱们再次回到WindowManagerGlobal#addView()方法中,接下来会走到ViewRootImpl#setView()方法中,这个方法里最终会调用下面方法完成窗口真正的添加:
其中这个mWindowSession
对应是一个Binder对象,对应类型为IWindowSession
,其真正的实现位于system_server侧的Session类,所以这里会发生跨进程通信,并将window的LayoutParam类型参数进行传入,我们继续看下Session#addToDiaplayAsUser
方法:
mService对应的实现类WindowManagerService,所以我们看下该类的addWindow方法:
java
# WindowManagerService
final HashMap<IBinder, WindowState> mWindowMap = new HashMap<>();
public int addWindow(Session session, IWindow client, LayoutParams attrs, int viewVisibility,
int displayId, int requestUserId, InsetsVisibilities requestedVisibilities,
InputChannel outInputChannel, InsetsState outInsetsState,
InsetsSourceControl[] outActiveControls) {
WindowState parentWindow = null;
final int type = attrs.type;
//1.
if (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW) {
parentWindow = windowForClientLocked(null, attrs.token, false);
//...
}
//2.
final boolean hasParent = parentWindow != null;
WindowToken token = displayContent.getWindowToken(
hasParent ? parentWindow.mAttrs.token : attrs.token);
//3.
if (token == null) {
if (!unprivilegedAppCanCreateTokenWith(parentWindow, callingUid, type,
rootType, attrs.token, attrs.packageName)) {
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
}
final WindowState win = new WindowState(this, session, client, token, parentWindow,
appOp[0], attrs, viewVisibility, session.mUid, userId,
session.mCanAddInternalSystemWindow);
}
# DiaplayConent
private final HashMap<IBinder, WindowToken> mTokenMap = new HashMap();
WindowToken getWindowToken(IBinder binder) {
return mTokenMap.get(binder);
}
上面的代码是经过精简后的。
-
前面有提到,Dialog的窗口类型为2,所以不满足if的条件,自然parentWindow无法赋值,即为null;
-
这里hasParent自然就是false,调用方法
getWindowToken()
传入的参数就是应用侧Window#LayoutParam#token属性,其中借助前面分析,如果Application作为Dialog的context,这个token值是null;看下
getWindowToken()
方法,它会将上面的传入token作为key,从DisplayContent#mTokenMap
这个集合中获取值,什么时候写入值呢:前面有提到过,在startActivity的流程中,会向这个集合中写入值。而这个传入的token就是之前startActivity流程中,写入到DisplayContent#mTokenMap
这个集合中的key,所以自然是能够获取到对应的value,即WindowToken类型属性token不为null,自然走不到3处标记的条件分支中,窗口校验通过。 -
而Application作为Dialog的context时,传入的token是null,自然是无法获取到值,WindowToken 类型属性token为null,走到if分支中,会返回
WindowManagerGlobal.ADD_BAD_APP_TOKEN
,当应用侧检测到返回值为这个时,就会出现文章一开头说的BadTokenException异常。
到了这里,相信你就明白了,为啥Application作为Dialog的context会导致崩溃,关键的分析就是上面的内容;
四. 不让Application作为Dialog的context崩溃?
根据上面的分析结果,Application作为Dialog的context崩溃的真正原因就是应用侧传过来的LayoutParam#token对象是null的,既然这样,那我们在应用侧给Dialog的Window#LayoutParam#token属性赋值为Activity的Window#LayoutParam#token属性,就可以避免这场悲剧发生了,可以看到下面能正常显示弹窗:
但是还是不建议大家这样做哈,毕竟如果在Dialog中使用到了这个Application的context进行Activity的跳转等其他未知行为,估计就会出现其他的幺蛾子了哈。
五. 总结
本篇文章涉及到的源码有点多,重点在于以下几个地方:
- Activity和Application获取WindowManager在应用侧服务的区别;
- 将窗口添加到WMS侧,Activity和Application下WindowManagerImpl传参token的区别;
- WMS中对应窗口类型以及传入的token是否为null进行的一番检验,已经检验不通过导致应用侧发生BadTokenException异常;
希望本篇文章能对你有所帮助,有什么需要交流的也欢迎下评论中留言,感谢阅读。