Android 老司机被打脸!Dialog 对应的 Context 必须是 Activity 吗?
后来发现我的回答确实错了,于是通过每日一问分享给大家。
于是有了本文,我负责被打脸,小缘负责解答,我反正不会被打脸第二次了,希望大家也能更清晰的认识这一块。
问题
在我们的印象里,如果构造一个 Dialog 传入一个非 Activiy 的 context,则可能会出现 bad token exception。
今天我们就来彻底搞清楚这一块,问题来了:
1、为什么传入一个非 Activity 的 context 会出现错误?
2、传入的 context 一定要是 Activity 吗?
解答
1.先来看第二问:创建 Dialog 对象依赖的 Context 必须是 Activity 吗?
相信大家曾经都有遇到过需要在 Application 或者 Service 里弹出 Dialog 的情景,就算平时做的正式项目没有这种需求,那也应该在刚开始学习 Android 或者写 Demo 的时候试过。
所以对于这个问题,回答肯定是:不是的。
在创建 Dialog 对象时,context 参数传 Activity 和传 Service 或 Application 之类的非 Activity 的 Context 对象有什么区别呢?
有经验的同学会说,想要通过非 Activity 对象创建并正常显示 Dialog,首先必须拥有 SYSTEM_ALERT_WINDOW 权限,还有,在调用 Dialog.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?。
为什么会这样?
常规的 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 的 type 指定为 SYSTEM_WINDOW 类型就能找到容器了呢?
其实一样没有找到,只是在获得 SYSTEM_ALERT_WINDOW 权限之后,会即时创建一个 WindowToken 而已(ActivityRecord 也是继承自 WindowToken),然后会把这个新创建的 WindowToken 附加到特定的容器上。
来看图:
常规的 Dialog 显示,是这样的。
最底的那个绿色的 WindowState,就是 Dialog 的窗口。
把 Dialog 的 Window.type 指定为 SYSTEM_WINDOW 之后,是这样的:
右边最底的那个 WindowState 就是 SYSTEM_WINDOW 类型的 Dialog 窗口,在层级关系上,跟隔壁的 ActivityRecord 是相等的。
Dialog 窗口所在容器,就是刚刚说到的那个即时创建的 WindowToken。
评论