在我们的印象里,如果构造一个 Dialog 传入一个非 Activiy 的 context,则可能会出现 bad token exception。
今天我们就来彻底搞清楚这一块,问题来了:
- 为什么传入一个非 Activity 的 context 会出现错误?
- 传入的 context 一定要是 Activity 吗?
更多问答 >>
-
2021-07-11 22:06
-
每日一问 | 我们经常说到的 Android 脱糖指的是什么?
2021-07-11 22:06 -
每日一问 | ViewModel 在什么情况下的「销毁重建」能够对数据进行无缝恢复?
2021-08-25 18:11 -
每日一问 | 关于 Activity 重建,值得探究的几个问题
2021-08-30 21:37 -
每日一问 | 好奇ActivityThread中为什么会有一个 Application的集合?
2021-08-30 21:36 -
2021-05-28 00:29
-
每日一问 | 已经有了 Intent,那为啥还要 PendingIntent?
2021-05-28 00:29 -
每日一问 | view.requestLayout如果在灭屏或者切home之后调用会怎么样?
2021-05-06 00:16 -
2021-05-06 00:16
-
每日一问 | 听说你做过内存优化 之 Bitmap内存占用到底在哪?
2021-04-19 23:40
先来看第二问:创建Dialog对象依赖的Context必须是Activity吗?
相信大家曾经都有遇到过需要在Application或者Service里弹出Dialog的情景,就算平时做的正式项目没有这种需求,那也应该在刚开始学习Android或者写Demo的时候试过。所以对于这个问题,回答肯定是:不是的。在创建Dialog对象时,
有经验的同学会说,想要通过非Activity对象创建并正常显示Dialog,首先必须拥有SYSTEM_ALERT_WINDOW权限,还有,在调用Dialog.context
参数传Activity和传Service或Application之类的非Activity的Context对象有什么区别呢?show
方法之前,必须把Dialog的Window的type
指定为SYSTEM_WINDOW类型,比如TYPE_SYSTEM_ALERT或TYPE_APPLICATION_OVERLAY。没有满足第一个条件的话,那肯定会报permission denied
啦。如果在show
之前没有指定Window的type
为SYSTEM_WINDOW类型,一样会发生BadTokenException的,message是token null is not valid; is your activity running?
。为什么会这样?
在上一个回答【Android中的子窗口到底指的是什么? 】中说到了常规的Dialog的容器是Activity,所以它窗口属性的token
引用的就是Activity的Token。到了WMS那边会根据这个Activity的Token来找到对应的ActivityRecord实例(其实是根据Token来查找对应的容器),然后把Dialog对应的WindowState添加到ActivityRecord里面。注意! 如果在查找容器这一步,没有找到对应实例的话,就会抛出一个BadTokenException(token null is not valid; is your activity running?
)查找容器还跟Context实例有关系吗? 使用Service或Application就找不到容器,换成Activity就能找到,这是为什么?
肯定有关系啦,别忘了Dialog在show
方法里是通过WindowManager来添加View的,而这个WindowManager对象就是从Context的getSystemService(WINDOW_SERVICE)
方法获得的。重点来了:因为Activity重写了Context的getSystemService
方法,在获取的WINDOW_SERVICE的时候返回了Activity主Window的WindowManager对象。当然了,这个主Window的WindowManager对象也没有什么特别之处,只是它里面的mParentWindow
指向的是主Window(其他非Activity的Context的WindowManager.mParentWindow
默认都是null)。WindowManagerGlobal在addView
的时候,如果检查到mParentWindow
不为null的话,就会对窗口属性(即上一个回答中说到的mWindowAttributes
)的token
进行赋值,它的逻辑是这样的:如果窗口类型为SUB_WINDOW(即子窗口),就会把
mParentWindow
对应的ViewRootImpl的mWindow
赋值给token
(上一个回答也有相关介绍);窗口类型为SYSTEM_WINDOW(系统级别的窗口,比如ANR Dialog),则不会对
token
进行赋值。因为普通应用的Window等级比系统Window低,所谓小庙容不下大佛;窗口类型为APPLICATION_WINDOW(Activity主Window和普通的Dialog就是这个类型),会把
mParentWindow
的mAppToken
(也就是所属Activity的mToken
)赋值给token
;根据上面这个规则,可以联想到会有两种情况导致窗口属性的
token
为null(token
为null就肯定找不到容器啦),一种是创建Dialog时传了非Activity的Context,另一种是Dialog的Window.type
指定为SYSTEM_WINDOW。为什么非要一个Token?
这是因为在WMS那边需要根据这个Token来确定Window的位置(不是说坐标),如果没有Token的话,就不知道这个窗口应该放到哪个容器上了。那为什么把Window的
其实一样没有找到,只是在获得SYSTEM_ALERT_WINDOW权限之后,会即时创建一个WindowToken而已(ActivityRecord也是继承自WindowToken),然后会把这个新创建的WindowToken附加到特定的容器上。来看图:常规的Dialog显示,是这样的。最底的那个绿色的WindowState,就是Dialog的窗口。type
指定为SYSTEM_WINDOW类型就能找到容器了呢?把Dialog的Window.
右边最底的那个WindowState就是SYSTEM_WINDOW类型的Dialog窗口,在层级关系上,跟隔壁的ActivityRecord是相等的。type
指定为SYSTEM_WINDOW之后,是这样的:Dialog窗口所在容器,就是刚刚说到的那个即时创建的WindowToken。
其实其他系统级别的窗口也是放置在这个WindowToken的父级容器DisplayArea.Tokens里面的,就像这样:噢对了,来了解一下WMS这边的各个容器的关系吧(深色箭头是extends的意思):
现在来回答第一问:为什么使用非Activity来创建并弹出Dialog,有时会发生BadTokenException?
主要是因为非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里面(这下知道放在哪里了)。总结:
Show一个普通的Dialog需要的并不是Activity本身,而是一个容器的token,我们平时会传Activity,只不过是Activity刚好对应WMS那边的一个WindowState的容器而已。赞,开始思考下一问~
你好 我这边有几个问题交流下 1. 回答中“因为Activity重写了Context的getSystemService方法 。。。” 不管是activity还是dialog在创建完PhoneWin ...查看更多
你好 我这边有几个问题交流下 1. 回答中“因为Activity重写了Context的getSystemService方法 。。。” 不管是activity还是dialog在创建完PhoneWindow后都会进行设置WindowManager,而WindowManager都会新建,而且他的mParentWindow其实就是其创建的PhoneWindow本身,(所以我感觉这里用不用activity的context感觉并不是特别重要) 2."窗口类型为APPLICATION_WINDOW(Activity主Window和普通的Dialog就是这个类型),会把mParentWindow的mAppToken(也就是所属Activity的mToken)赋值给token" 对于dialog创建的PhoneWindow,其内部的mAppToken是null(具体代码可看 dialog创建过程中 w.setWindowManager(mWindowManager, null, null);) 所以现在我没法理解为什么dialog在没有token的情况下能创建窗口呢,是在哪里重新赋过值吗?
1. Dialog.show方法调用时,使用的不是Dialog内创建的PhoneWindow的WindowManager对象,而是其构造函数的Context所对应的WindowManger,它是通过g ...查看更多
1. Dialog.show方法调用时,使用的不是Dialog内创建的PhoneWindow的WindowManager对象,而是其构造函数的Context所对应的WindowManger,它是通过getSystemService方法来获取的,所以跟重不重写getSystemService方法有关。 2. 在WindowManagerGlobal的addView方法内,会根据参数parentWindow是否为空来判断是否调用Window的adjustLayoutParamsForSubWindow方法,lp.token就是在这方法里赋值的。
才发现Dialog.show方法使用的不是Dialog内创建的PhoneWindow的WindowManager对象。。。 这样就理清了,哈哈 多谢了
想问一下,回答里面的图片是用什么软件画的?
我用ps画的,很灵活,但也很麻烦,很慢
洋神我去百度投靠你吧
鸿洋大神好像在字节
别吹比了,给我发简历再说
洋神我现在在京东,跳槽百度是不是不太好
看到回家的诱惑已经把context、token、windowToken的关系说的很全面了,我在这里做一些补充,既appWindowToken是怎么创建和获取的,为什么在Application中展示dialog会报错:
首先,写一个demo:
运行后,一定会报如下的错误:
这个错误是怎么来的呢,所谓的
token null is not valid
中的token又是什么呢?本篇我们来通过源码来分析一下。1. 错误追踪
首先跟着源码的步伐追踪一下为什么会报这个错误,从Dialog#show开始
这里调用的mWindowManager的addView方法,其形参mDecor是我们解析到的Dialog的DecorView, l 是WindowManager.LayoutParams 属性; mWindowManager的具体是现实WindowManagerImpl。
WindowManagerImpl调用WindowManagerGlobal#addView继续执行:
这里创建了viewRootImpl并执行了ViewRootImpl#setView方法:
在这个方法中,我们找到了抛出异常的地方,当res的值为WindowManagerGlobal.ADD_BAD_APP_TOKEN或则WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN时,会抛出文章开头处的错误。
这个res是IWindowSession#addToDisplay
方法的返回值。IWindowSession是一个Binder接口,负责ViewRootImpl和WindowManagerService的通信,在ViewRootImpl对象创建时获取。在这里IWindowSession#addToDisplay
最终会调用WindowManagerService#addWindow方法:通过这个方法,可以确定方法返回WindowManagerGlobal.ADD_BAD_APP_TOKEN或则WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN的条件,即Window的类型为子窗口时,parentWindow为空会返回WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN,当Window类型为APPLICATION_WINDOW时,当对应的WindowToken为空就会返回WindowManagerGlobal.ADD_BAD_APP_TOKEN
2. WindowToken是什么
在第一章节的最后一部分,我们引入了一个新概念:WindowToken,它和这个异常的判断息息相关,那么它是在哪里创建的呢?作用又是什么呢?
看下它的获取方式:
先看下AppWindowToken的继承体系:
回顾一下Activity的启动流程,在Activity的启动过程中,会调用ActivityStack#startActivityLocked方法:
在这个方法中会调用ActivityRecord#createWindowContainer方法,在这个方法里会创建AppWindowContainer的实例,进而创建AppWindowContainerController实例,在AppWindowContainerController中会创建AppWindowToken的实例,在创建实例时会调用AppWindowToken的构造方法。从上图AppWindowToken的继承关系图我们可以知道,AppWindowToken的父类是WindowToken,其构造方法如下:
看下onDisplayChanged方法:
DisplayContent#reParentWindowToken会将该WindowToken加入到一个叫做mTokenMap的Map中,其定义为
HashMap<IBinder, WindowToken> mTokenMap
. 而作为key的IBinder对象,就是我们在Activity启动流程中创建的AppToken对象。也就是说,每个AppToken会对应一个AppWindowToken,简单的示例图如下:3. 问题产生的根本原因
要想探寻这个错误产生的根本原因,我们只需要寻确认在异常产生时,Window的Type和appToken的值是什么即可。
Window的Type是比较好确认的,在创建Dialog的Window时,便生成了其对应的
WindowManager.LayoutParams
, 在其构造方法中已确定了Dialog对应Window的type:而appToken的情况则稍显复杂,我们首先要确定这个appToken是从哪里获取的。
先看一下Dialog的构造方法:
这个WindowManger是我们在show方法中用于执行addView的WindowManger。
那么接下来,我们就要确定在Application中启动Dialog和在Activity中启动Dialog这两种方式WindowManger的处理有什么不同就可以了。
首先看Activity的情况:
还是Dialog的构造方法,这个Context的实际类型是Activity的对象,而Activity重写了getSystemService方法:
因此,在Dialog的构造方法中,我们获取到的实际是Activity中定义的WindowMnager,我们看下这个WindowMnager的赋值,这部分逻辑在Activity#attach方法中:
这个WindowMnager是其对应Window的WindowManager, 而其对应的Window的WindowManager创建时,传入了一个parentWindow:
在Dialog的show流程中,因为所使用的WindowManager是含有parentWindow的,因此,在WindowMnagerGlobal#addView流程中,会触发以下逻辑:
而因为wparam的type是TYPE_APPLICATION, 因此Window#adjustLayoutParamsForSubWindow会调用如下逻辑:
这个mAppToken就是Activity中的token了。
至于Activity中的token是什么,感兴趣的朋友可以看一下我之前写过的文章
AMS源码分析(一)Activity生命周期管理分析完了Activity的情况,我们继续看一下Dialog在Application中展示的情况:
还是Dialog的构造方法:
Application并没有对Contex#getSystemService做重载,因此这里获取到的是context的WindowManager, 而Context的WindowMnager是不包含parentWindow的,因此在最终使用token获取AppWindowToken时会获取到空值,再结合Dialog的Window的Type, 即TYPE_APPLICATION,那么就会触发以下判断:
最终抛出异常。
我看你有些图裂了
我晚点帮忙处理下图片~~ 估计答主不太好操作。
@Deprecated public static final int TYPE_TOAST = FIRST_SYSTEM_WINDOW+5; 为什么系统弹 ...查看更多
@Deprecated public static final int TYPE_TOAST = FIRST_SYSTEM_WINDOW+5; 为什么系统弹出 Toast 不需要 android.permission.SYSTEM_ALERT_WINDOW 权限呢 ?
@鸿洋,裂图还没修复好
系统级别的Dialog不需要非 Activiy 的 context
常规的Dialog必须传入Activiy 的 context简单明了 清楚
不一定,先占坑