Application 作为 Dialog 的 Context?小心踩坑!

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

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

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

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

  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创建流程在ApplicationActivity的差异,就是Application作为Dialogcontext会异常的核心原因

我们就从下面方法作为入口进行分析:

context.getSystemService(WINDOW_SERVICE)


1. Application 下WindowManagerImpl的创建

对于 Application 而言,getSystemService()方法的调用,最终会走到父类ContextWrapper中:


而这个mBase属性对应的类为ContextImpl对象,对应ContextImpl#getSystemService():

对应SystemServiceRegistry#getSystemService

SYSTEM_SERVICE_FETCHERS是一个 Map 集合,对应的 key 为服务的名称,value 为服务的实现方式:

SYSTEM_SERVICE_FETCHERS是一个 Map 集合,对应的 key 为服务的名称,value 为服务的实现方式:

接下来看下咱们关心的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]

2. 如果当前为 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 方法:

# WindowManagerServicefinal 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);}
# DiaplayConentprivate 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、ios、java、php,或者你是产品经理、老板,都可以免费入群接单或者发布项目,全程不收取任何费用。

    2025年02月21日 15点27分
  • Application 作为 Dialog 的 Context?小心踩坑!

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

    2025年02月25日 15点25分
  • Android 应用的线程世界:最少需要几个线程才能启动?

    这篇文章主要介绍了 Android App 中的多种线程,包括守护线程(如 Signal Catcher 等)、渲染线程、主线程、三方线程(如 OkHttp、Glide、ARouter 相关线程)等,还提及三方库中线程池的情况及可能存在的问题。

    2025年02月25日 15点13分
  • Android 复杂项目崩溃率收敛至0.01%实践

    在我们的项目中,每个版本发布之后,我们会创建一个opt分支,用于修复线上崩溃以及业务逻辑BUG。

    2025年02月25日 15点11分
  • Android 能悄悄知道用户截屏?这里有你想要的答案

    很多应用在当你截屏的时候能够感知到,并提示你是否要发送截屏等等。

    2025年02月25日 15点07分
  • 科大讯飞讯飞星火API能力免费开放,引领大模型商业化新篇章

    合肥本土科技巨头科大讯飞传来振奋人心的消息:讯飞星火API能力正式向公众免费开放,其中,讯飞星火Lite API更是实现了永久免费。这一举措无疑为整个行业注入了新的活力,也彰显了科大讯飞在大模型商业化进程中的坚定决心。

    2025年02月21日 15点57分
  • 一木林接入AI大模型,实现智能体功能

    一木林——全能型AI电子工具箱。这是一款集多功能于一体的工具类应用,凭借小巧的体积与全面且强大的功能,赢得了极高的口碑。应用内汇聚了上百款实用工具,诸如指南针、计算器、分贝仪等,一应俱全。如今,一木林已携手星火AI大模型,实现了AI对话与智能体的创新功能。

    2025年02月21日 15点42分
  • PrivacySentry:隐私政策守护利器

    近年来,工信部对APP个人隐私要求越来越多,之后各大应用市场也开始要求,有违规情况的会导致APP下架或者无法上架。这不,我的app就因为三方SDK频繁获取Android ID 导致无法上架,等SDK商场更新也很浪费时间,所以只能想办法去处理这件事,好在找到了PrivacySentry这个神器,可规避应用市场上架合规检测的大部分问题

    2025年02月18日 23点20分
  • Kotlin协程:MutableSharedFlow的实现原理

    在Koltin协程:异步热数据流的设计与使用中,提到了可以通过MutableSharedFlow方法创建一个MutableSharedFlow接口指向的对象,代码如下:

    2025年02月18日 11点03分
  • 探索Markdown之父John Gruber对Markdown的看法

    Markdown的缔造者——约翰·格鲁伯,其形象正如上方头像所示。不禁让人遐想,国外的大师们是否都偏爱蓄须? 约翰·格鲁伯,1973年出生于美国宾夕法尼亚州,他是一位才华横溢的作家、博主、UI设计师,更是Markdown的创始人。他毕业于Drexel University(卓克索大学),并获得了计算机科学学士学位。在职业生涯中,他曾在Joyent公司任职,为大型企业提供了基础架构和平台服务。自2002年起,他开始撰写备受欢迎的科技博客《Daring Fireball》,并主持了知名的播客节目The Talk Show。2013年,他与两位友人共同创立了Q Branch,并开发了Vesper笔记应用。

    2025年02月18日 10点52分
  • 浅聊一下JVM内存结构

    2025年02月17日 14点29分
  • Android屏幕适配(6) — 今日头条屏幕适配

    在之前的文章中,我们讲到了Android屏幕适配的一些知识,大家感兴趣的话可参考 Android屏幕适配(1) — 概念解释 Android屏幕适配(2) — drawable与mipmap Android屏幕适配(3) — 资源文件夹命名与匹配规则 Android屏幕适配(4) — 宽高限定符 Android屏幕适配(5) — 最小宽度smallWidth适配 这节我们讲讲今日头条屏幕适配方案。

    2025年02月15日 11点19分
  • Android屏幕适配(4) — 宽高限定符

    在之前的文章中,我们讲到了Android屏幕适配的一些知识,大家感兴趣的话可参考 Android屏幕适配(1) — 概念解释 Android屏幕适配(2) — drawable与mipmap Android屏幕适配(3) — 资源文件夹命名与匹配规则 今天就让我们来学习下Android屏幕适配的宽高限定符相关知识吧。

    2025年02月15日 11点14分
  • Android屏幕适配(3) — 资源文件夹命名与匹配规则

    在之前的文章中,我们已经讲到了Android屏幕适配的一些知识,大家感兴趣的话,可参考以下文章 Android屏幕适配(1) — 概念解释 Android屏幕适配(2) — drawable与mipmap 大家都知道在Android资源文件夹res/下,我们经常能看到layout-sw720,drawable-xhdpi,values-w480等字样文件夹,那么res/下到底有哪些文件夹可以命名,命名规则如何?这节我们来讲讲关于Android资源文件夹res/下各文件夹的命名。

    2025年02月15日 11点14分
  • Android屏幕适配(2) — drawable与mipmap

    上节我们讲到了屏幕适配的几个基本概念。大家感兴趣的可参考 Android屏幕适配(1) — 概念解释 这节我们讲讲屏幕适配中drawable与mipmap相关知识。

    2025年02月15日 11点08分
  • Android屏幕适配(1) — 概念解释

    2025年02月15日 11点05分

发表回复

评论列表

点击查看更多

    联系我们

    在线咨询: QQ交谈

    微信:dxmcpjl

    邮件:1529097251#qq.com

    工作时间:周一至周五,9:30-18:30,节假日休息

    微信