一文洞彻:Application为啥不能作为Dialog的context?

大家好,相信大家在使用Dialog时,都有一个非常基本的认知:就是Dialog的context只能是Activity,而不能是Application,不然会导致弹窗崩溃:

这个Exception几乎属于是每个Android开发初学者都会碰到的,但是。

前几天研究项目代码发现 Application作为 Dialog context竟然不会崩溃?!!这句话说出来和本篇文章标题严重不符哈,这不是赤裸裸的打脸了吗。先别急,请大家跟着我的脚步,相信阅读完本篇文章就可以解答目前你心目中最大的两个疑惑:

  1. 如标题所言,为啥Application无法作为Dialog的context并导致崩溃?
  2. 项目中为啥又发现,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。

  1. 如果当前为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?

  1. 如果当前为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);
}

上面的代码是经过精简后的。

  1. 前面有提到,Dialog的窗口类型为2,所以不满足if的条件,自然parentWindow无法赋值,即为null;

  2. 这里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处标记的条件分支中,窗口校验通过。

  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的跳转等其他未知行为,估计就会出现其他的幺蛾子了哈。

五. 总结

本篇文章涉及到的源码有点多,重点在于以下几个地方:

  1. Activity和Application获取WindowManager在应用侧服务的区别;
  2. 将窗口添加到WMS侧,Activity和Application下WindowManagerImpl传参token的区别;
  3. WMS中对应窗口类型以及传入的token是否为null进行的一番检验,已经检验不通过导致应用侧发生BadTokenException异常;

希望本篇文章能对你有所帮助,有什么需要交流的也欢迎下评论中留言,感谢阅读。

参考文章

Android高工面试(难度:四星):为什么不能使用 Application Context 显示 Dialog?

相关推荐
zhangphil35 分钟前
Android简洁缩放Matrix实现图像马赛克,Kotlin
android·kotlin
m0_5127446436 分钟前
极客大挑战2024-web-wp(详细)
android·前端
lw向北.1 小时前
Qt For Android之环境搭建(Qt 5.12.11 Qt下载SDK的处理方案)
android·开发语言·qt
不爱学习的啊Biao1 小时前
【13】MySQL如何选择合适的索引?
android·数据库·mysql
Clockwiseee1 小时前
PHP伪协议总结
android·开发语言·php
mmsx8 小时前
android sqlite 数据库简单封装示例(java)
android·java·数据库
众拾达人11 小时前
Android自动化测试实战 Java篇 主流工具 框架 脚本
android·java·开发语言
吃着火锅x唱着歌12 小时前
PHP7内核剖析 学习笔记 第四章 内存管理(1)
android·笔记·学习
_Shirley13 小时前
鸿蒙设置app更新跳转华为市场
android·华为·kotlin·harmonyos·鸿蒙
hedalei15 小时前
RK3576 Android14编译OTA包提示java.lang.UnsupportedClassVersionError问题
android·android14·rk3576