前言
本篇文章是《Android开发艺术探索》第8章的理解和实践,该篇文章涉及的源码知识点较多,来逐一分解。
为什么会突然想了解一下Window呢,原因有如下2个:
- 一个是在Android View工作原理的探索时有提及到ViewRootImpl这个类,当时说的是该类负责View的测量、布局和绘制,那这个ViewRootImpl的来由是什么,为什么是它来负责绘制;
- 一个是我们经常使用Dialog、PopupWindow等弹窗,同时设置其Window属性,但是这些有什么区别,各种Window的属性又是啥。
而这俩个原因,从某种程度上说就说明了为什么Android需要设计出这个Window。这里我们先说一些概念和设计原则,再来一一通过实践和源码来理解。
参考的文档有:
《Android开发艺术探索》
文章较长,本人能力有限,有问题欢迎评论指教。
正文
其实关于Window的知识点比较多,这里我们先说概念,先有一个总体的认识,这样比较方便理解。
一些概念
首先,Window表示是一个窗口的概念,在大屏的电脑屏幕上我们比较好理解,我打开一个应用,就是一个窗口,在Android中是一个应用定义为一个窗口还是一个Dialog就定义为一个窗口呢,真实情况如何。
Window和View的关系
这和我们普通的认知是不一样的,在Android中Window是一个抽象的概念,Android所有的视图都是通过Window来呈现,不论是Activity、Dialog还是Toast,视图实际都可以看成是附加在window上,即Window是View的载体。
这里如何理解呢 我们看到的EditText、ImageView等都是View,而一个View树就可以看成是一个Window,我们只能看到View,无法看到Window,Window本身并不存在;这个就类比班集体这个概念,学生就是一个个View,而一个班的学生就是班集体,而班集体是一个抽象的概念,本身并不存在。
View树
这里说道View树是啥呢,结合前面所说的Android任何视图都是通过Window来呈现这个说法,在activity中,最底层的布局就是一个View树,它没有父布局了;而对于一个自定义布局的Dialog来说,Dialog的顶层布局就不属于activity的View树,这是2个View树,所以是2个Window。
设计初衷
那为什么Android要把一个View树看成是一个Window呢 这就好比班集体概念是一样的,比如要做活动了,我可以直接以班级为单位来下达命令,组织活动,十分方便。
而设计Window的第一个重要作用就是view的显示层级,比如我在Activity上弹出一个Dialog,又弹出一个Toast,那么该如何保证Dialog显示是在Activity上的,而Toast又是在Dialog上的;这时我刷新Activity的UI,Dialog的UI是否需要刷新,而把这些View树给分开,使用Window管理,就可以方便实现不同View树的分层级显示;
另一个重要作用是方便点击事件的分发,还是前面的例子,这时给屏幕一个点击事件,这时是Dialog响应点击事件还是Activity响应点击事件,这个也可以由Window来实现。
总的来说,设计出Window就是为了解耦,虽然显示还是View来显示,我们把View树给看成一个集体,这样在处理显示和事件传递就非常方便了。
Window和WindowManager
在Android源码中,Window是一个抽象类,而它唯一的实现类是PhoneWindow类,注意这里的用词,我用的是Window类来指明代码中的抽象类,而平时说的Window则是一个窗口的概念。
在Android中,我们创建一个Window是一个非常简单的事情,只需要通过WindowManager即可完成,WindowManager是访问Window的入口,而真正实现Window的类是WindowManagerService,和WMS的通信是一个IPC通信。
还是记住前面的概念,Window是View的载体,View是Window的表现形式。
比如下面代码:
btn = Button(this)
btn?.text = "button"
layoutParams = WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT, 0, 0, PixelFormat.TRANSPARENT
)
layoutParams?.gravity = Gravity.START or Gravity.TOP
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
layoutParams?.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
} else {
layoutParams?.type = WindowManager.LayoutParams.TYPE_PHONE
}
layoutParams?.x = 0
layoutParams?.y = 0
layoutParams?.type = WindowManager.LayoutParams.TYPE_APPLICATION
windowManager.addView(btn, layoutParams)
就可以在页面上显示出一个Window,而该Window的表现形式就是一个按钮,可以发现我们是通过WindowManager类的addView方法来实现的,除此之外还有其他几个API接口:
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
public void removeView(View view);
通过这3个方法就可以操作Window了。关于这里添加的逻辑,以及显示的逻辑,我们等会再说,我们先看看这个非常关键的window都有哪些属性。
Window的属性
为什么要有window,就是为了更好的管理View,那如何管理View呢,这里就是通过设置Window的属性。这里一共有4类属性,在日常开发中肯定都直接或者间接使用过。
type属性
type表示该Window是什么类型,这里一共有3种Window类型,分别是应用程序Window,子程序Window和系统Window;而类型的区分就决定了Window的显示层序,假如一个Window是系统Window,它一定会显示在其他应用程序Window上面。
而控制显示层级顺序有个属性是Z-Order,即Z轴的值,这里的type就是Z-Order的值,值越大,显示的越上面,即type大的Window可以盖住type小的Window。
所以3种Window对应的Z-Order值是不一样的,下面是每种Window的type范围以及常见的Z-Order值。
应用程序Window
应用程序Window的type值范围是[1-99],什么是应用程序Window,比如Activity所展示的页面,在WindowManager#LayoutParams中定义了如下应用程序的type值:
// 应用程序 Window 的开始值\
public static final int FIRST_APPLICATION_WINDOW = 1;
// 应用程序 Window 的基础值\
public static final int TYPE_BASE_APPLICATION = 1;\
// 普通的应用程序\
public static final int TYPE_APPLICATION = 2;\
// 特殊的应用程序窗口,当程序可以显示 Window 之前使用这个 Window 来显示一些东西\
public static final int TYPE_APPLICATION_STARTING = 3;\
// TYPE_APPLICATION 的变体,在应用程序显示之前,WindowManager 会等待这个 Window 绘制完毕\
public static final int TYPE_DRAWN_APPLICATION = 4;\
// 应用程序 Window 的结束值\
public static final int LAST_APPLICATION_WINDOW = 99;
类型 | 备注 |
---|---|
FIRST_APPLICATION_WINDOW | 应用程序Window的开始值 |
TYPE_BASE_APPLICATION | 应用程序Window的基础值 |
TYPE_APPLICATION | 普通应用程序 |
TYPE_APPLICATION_STARTING | 特殊的应用程序窗口,用来显示Window在应用程序开始的时候,通常被系统调用直到应用程序自己显示出窗口 |
TYPE_DRAWN_APPLICATION | TYPE_APPLICATION_STARTING变种,会在应用程序显示前等待这个Window绘制完成 |
LAST_APPLICATION_WINDOW | 应用程序Window的结束值 |
子Window(Sub Window)
表示子Window,它的范围是[1000,1999],这些Window会按照Z-Order顺序依附于父Window上,而且他们的坐标是相当于父Window的,例如PopupWindow和一些Dialog,对应的type值如下:
/**
* 子Window的开始值,该Window的token必须设置在他们依附的父Window
*/
public static final int FIRST_SUB_WINDOW = 1000;
/**
* 应用程序Window上面的面板
*/
public static final int TYPE_APPLICATION_PANEL = FIRST_SUB_WINDOW;
/**
* 用于显示多媒体(比如视频)的Window,这些Windows会显示在他们依附的Window后面
*/
public static final int TYPE_APPLICATION_MEDIA = FIRST_SUB_WINDOW + 1;
/**
* 应用程序Window上面的子面板
*/
public static final int TYPE_APPLICATION_SUB_PANEL = FIRST_SUB_WINDOW + 2;
/**
* 当前Window的布局和顶级Window布局相同时,不能作为子代的容器
*/
public static final int TYPE_APPLICATION_ATTACHED_DIALOG = FIRST_SUB_WINDOW + 3;
/**
* 用于在媒体Window上显示覆盖物
* @hide
*/
@UnsupportedAppUsage
public static final int TYPE_APPLICATION_MEDIA_OVERLAY = FIRST_SUB_WINDOW + 4;
/**
* 依附在应用Window上和它的子面板Window上的子面板
* @hide
*/
public static final int TYPE_APPLICATION_ABOVE_SUB_PANEL = FIRST_SUB_WINDOW + 5;
/**
* 子Window的结束值
*/
public static final int LAST_SUB_WINDOW = 1999;
类型 | 备注 |
---|---|
FIRST_SUB_WINDOW | 子Window的开始值 |
TYPE_APPLICATION_PANEL | 应用程序Window上面的面板 |
TYPE_APPLICATION_MEDIA | 用于显示多媒体(比如视频)的Window,这些Windows会显示在他们依附的Window后面 |
TYPE_APPLICATION_SUB_PANEL | 应用程序Window上面的子面板 |
TYPE_APPLICATION_ATTACHED_DIALOG | 当前Window的布局和顶级Window布局相同时,不能作为子代的容器 |
TYPE_APPLICATION_MEDIA_OVERLAY | 用于在媒体Window上显示覆盖物 |
TYPE_APPLICATION_ABOVE_SUB_PANEL | 依附在应用Window上和它的子面板Window上的子面板 |
LAST_SUB_WINDOW | 子Window的结束值 |
系统Window(System Window)
系统Window的范围是[2000,2999],常见的系统的Window有Toast、输入法窗口、系统音量条窗口、系统错误窗口等,对应type的值如下:
// 系统Window类型的开始值\
public static final int FIRST_SYSTEM_WINDOW = 2000;\
\
// 系统状态栏,只能有一个状态栏,它被放置在屏幕的顶部,所有其他窗口都向下移动\
public static final int TYPE_STATUS_BAR = FIRST_SYSTEM_WINDOW;\
\
// 系统搜索窗口,只能有一个搜索栏,它被放置在屏幕的顶部\
public static final int TYPE_SEARCH_BAR = FIRST_SYSTEM_WINDOW+1;\
\
@Deprecated\
// API 已经过时,用 TYPE_APPLICATION_OVERLAY 代替\
public static final int TYPE_PHONE = FIRST_SYSTEM_WINDOW+2;\
\
@Deprecated\
// API 已经过时,用 TYPE_APPLICATION_OVERLAY 代替\
public static final int TYPE_SYSTEM_ALERT = FIRST_SYSTEM_WINDOW+3;\
\
// 已经从系统中被移除,可以使用 TYPE_KEYGUARD_DIALOG 代替\
public static final int TYPE_KEYGUARD = FIRST_SYSTEM_WINDOW+4;\
\
@Deprecated\
// API 已经过时,用 TYPE_APPLICATION_OVERLAY 代替\
public static final int TYPE_TOAST = FIRST_SYSTEM_WINDOW+5;\
\
@Deprecated\
// API 已经过时,用 TYPE_APPLICATION_OVERLAY 代替\
public static final int TYPE_SYSTEM_OVERLAY = FIRST_SYSTEM_WINDOW+6;\
\
@Deprecated\
// API 已经过时,用 TYPE_APPLICATION_OVERLAY 代替\
public static final int TYPE_PRIORITY_PHONE = FIRST_SYSTEM_WINDOW+7;\
\
// 系统对话框窗口\
public static final int TYPE_SYSTEM_DIALOG = FIRST_SYSTEM_WINDOW+8;\
\
// 锁屏时显示的对话框\
public static final int TYPE_KEYGUARD_DIALOG = FIRST_SYSTEM_WINDOW+9;\
\
@Deprecated\
// API 已经过时,用 TYPE_APPLICATION_OVERLAY 代替\
public static final int TYPE_SYSTEM_ERROR = FIRST_SYSTEM_WINDOW+10;\
\
// 输入法窗口,位于普通 UI 之上,应用程序可重新布局以免被此窗口覆盖\
public static final int TYPE_INPUT_METHOD = FIRST_SYSTEM_WINDOW+11;\
\
// 输入法对话框,显示于当前输入法窗口之上\
public static final int TYPE_INPUT_METHOD_DIALOG= FIRST_SYSTEM_WINDOW+12;\
\
// 墙纸\
public static final int TYPE_WALLPAPER = FIRST_SYSTEM_WINDOW+13;\
\
// 状态栏的滑动面板\
public static final int TYPE_STATUS_BAR_PANEL = FIRST_SYSTEM_WINDOW+14;\
\
// 应用程序叠加窗口显示在所有窗口之上\
public static final int TYPE_APPLICATION_OVERLAY = FIRST_SYSTEM_WINDOW + 38;\
\
// 系统Window类型的结束值\
public static final int LAST_SYSTEM_WINDOW = 2999;
系统Window的值比较多,但是很多都过时了。
其中要注意使用系统的Window需要申请权限,即Manifest.permission.SYSTEM_ALERT_WINDOW权限。
Flag属性
使用type设置完Window的显示层级就完成了设计Window的第一个初衷了,那如何设置Window的其他显示项和配置项呢,在源码中为我们提供了许多Flag值,通过设置不同的Flag值可以得到对应的效果。
同样这些Flag的值也是定义在WindowManager#LayoutParams类中,如下:
// 当 Window 可见时允许锁屏\
public static final int FLAG_ALLOW_LOCK_WHILE_SCREEN_ON = 0x00000001;\
\
// Window 后面的内容都变暗\
public static final int FLAG_DIM_BEHIND = 0x00000002;\
\
@Deprecated\
// API 已经过时,Window 后面的内容都变模糊\
public static final int FLAG_BLUR_BEHIND = 0x00000004;\
\
// Window 不能获得输入焦点,即不接受任何按键或按钮事件,例如该 Window 上 有 EditView,点击 EditView 是 不会弹出软键盘的\
// Window 范围外的事件依旧为原窗口处理;例如点击该窗口外的view,依然会有响应。另外只要设置了此Flag,都将会启用FLAG_NOT_TOUCH_MODAL\
public static final int FLAG_NOT_FOCUSABLE = 0x00000008;\
\
// 设置了该 Flag,将 Window 之外的按键事件发送给后面的 Window 处理, 而自己只会处理 Window 区域内的触摸事件\
// Window 之外的 view 也是可以响应 touch 事件。\
public static final int FLAG_NOT_TOUCH_MODAL = 0x00000020;\
\
// 设置了该Flag,表示该 Window 将不会接受任何 touch 事件,例如点击该 Window 不会有响应,只会传给下面有聚焦的窗口。\
public static final int FLAG_NOT_TOUCHABLE = 0x00000010;\
\
// 只要 Window 可见时屏幕就会一直亮着\
public static final int FLAG_KEEP_SCREEN_ON = 0x00000080;\
\
// 允许 Window 占满整个屏幕\
public static final int FLAG_LAYOUT_IN_SCREEN = 0x00000100;\
\
// 允许 Window 超过屏幕之外\
public static final int FLAG_LAYOUT_NO_LIMITS = 0x00000200;\
\
// 全屏显示,隐藏所有的 Window 装饰,比如在游戏、播放器中的全屏显示\
public static final int FLAG_FULLSCREEN = 0x00000400;\
\
// 表示比FLAG_FULLSCREEN低一级,会显示状态栏\
public static final int FLAG_FORCE_NOT_FULLSCREEN = 0x00000800;\
\
// 当用户的脸贴近屏幕时(比如打电话),不会去响应此事件\
public static final int FLAG_IGNORE_CHEEK_PRESSES = 0x00008000;\
\
// 则当按键动作发生在 Window 之外时,将接收到一个MotionEvent.ACTION_OUTSIDE事件。\
public static final int FLAG_WATCH_OUTSIDE_TOUCH = 0x00040000;\
\
@Deprecated\
// 窗口可以在锁屏的 Window 之上显示, 使用 Activity#setShowWhenLocked(boolean) 方法代替\
public static final int FLAG_SHOW_WHEN_LOCKED = 0x00080000;\
\
// 表示负责绘制系统栏背景。如果设置,系统栏将以透明背景绘制,\
// 此 Window 中的相应区域将填充 Window#getStatusBarColor()和 Window#getNavigationBarColor()中指定的颜色。\
public static final int FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS = 0x80000000;\
\
// 表示要求系统壁纸显示在该 Window 后面,Window 表面必须是半透明的,才能真正看到它背后的壁纸\
public static final int FLAG_SHOW_WALLPAPER = 0x00100000;
Flag | 备注 |
---|---|
FLAG_ALLOW_LOCK_WHILE_SCREEN_ON | 当 Window 可见时允许锁屏 |
FLAG_DIM_BEHIND | Window 后面的内容都变暗 |
FLAG_BLUR_BEHIND | API 已经过时,Window 后面的内容都变模糊 |
FLAG_NOT_FOCUSABLE | Window 不能获得输入焦点,即不接受任何按键或按钮事件,例如该 Window 上 有 EditView,点击 EditView 是 不会弹出软键盘的 Window 范围外的事件依旧为原窗口处理;例如点击该窗口外的view,依然会有响应。另外只要设置了此Flag,都将会启用FLAG_NOT_TOUCH_MODAL |
FLAG_NOT_TOUCH_MODAL | 设置了该 Flag,将 Window 之外的按键事件发送给后面的 Window 处理, 而自己只会处理 Window 区域内的触摸事件,Window 之外的 view 也是可以响应 touch 事件。 |
FLAG_NOT_TOUCHABLE | Window 将不会接受任何 touch 事件,例如点击该 Window 不会有响应,只会传给下面有聚焦的窗口。 |
FLAG_KEEP_SCREEN_ON | 只要 Window 可见时屏幕就会一直亮着 |
FLAG_LAYOUT_IN_SCREEN | 允许 Window 占满整个屏幕 |
FLAG_LAYOUT_NO_LIMITS | 允许 Window 超过屏幕之外 |
FLAG_FULLSCREEN | 全屏显示,隐藏所有的 Window 装饰,比如在游戏、播放器中的全屏显示 |
FLAG_FORCE_NOT_FULLSCREEN | 表示比FLAG_FULLSCREEN低一级,会显示状态栏 |
FLAG_IGNORE_CHEEK_PRESSES | 当用户的脸贴近屏幕时(比如打电话),不会去响应此事件 |
FLAG_WATCH_OUTSIDE_TOUCH | 则当按键动作发生在 Window 之外时,将接收到一个MotionEvent.ACTION_OUTSIDE事件 |
FLAG_SHOW_WHEN_LOCKED | 窗口可以在锁屏的 Window 之上显示, 使用 Activity#setShowWhenLocked(boolean) 方法代替 |
FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS | 表示负责绘制系统栏背景。如果设置,系统栏将以透明背景绘制,此 Window 中的相应区域将填充 Window#getStatusBarColor()和 Window#getNavigationBarColor()中指定的颜色。 |
FLAG_SHOW_WALLPAPER | 表示要求系统壁纸显示在该 Window 后面,Window 表面必须是半透明的,才能真正看到它背后的壁纸 |
这些Flag其中很多我们在平时开发中,比如设置Dialog等都使用过,现在我们真正知道了这些Flag的作用,可以让我们更好地去管理View。
SoftInputMode(软键盘)
表示Window软键盘输入区域的显示模式,比如我们在微信聊天时,我们希望点击输入框软键盘弹起来的时候,能把输入框也顶上去,这样就可以看见自己输入的内容了。
在之前设置软键盘的时候有时在代码中设置,有时在XML中设置,一直没有注意到底设置的是谁的属性,现在终于知道了,设置的是Window的属性。
同样在WindowManager#LayoutParams中有对软键盘的所有设置:
// 没有指定状态,系统会选择一个合适的状态或者依赖于主题的配置
public static final int SOFT_INPUT_STATE_UNCHANGED = 1;
// 当用户进入该窗口时,隐藏软键盘
public static final int SOFT_INPUT_STATE_HIDDEN = 2;
// 当窗口获取焦点时,隐藏软键盘
public static final int SOFT_INPUT_STATE_ALWAYS_HIDDEN = 3;
// 当用户进入窗口时,显示软键盘
public static final int SOFT_INPUT_STATE_VISIBLE = 4;
// 当窗口获取焦点时,显示软键盘
public static final int SOFT_INPUT_STATE_ALWAYS_VISIBLE = 5;
// window会调整大小以适应软键盘窗口
public static final int SOFT_INPUT_MASK_ADJUST = 0xf0;
// 没有指定状态,系统会选择一个合适的状态或依赖于主题的设置
public static final int SOFT_INPUT_ADJUST_UNSPECIFIED = 0x00;
// 当软键盘弹出时,窗口会调整大小,例如点击一个EditView,整个layout都将平移可见且处于软件盘的上方
// 同样的该模式不能与SOFT_INPUT_ADJUST_PAN结合使用;
// 如果窗口的布局参数标志包含FLAG_FULLSCREEN,则将忽略这个值,窗口不会调整大小,但会保持全屏。
public static final int SOFT_INPUT_ADJUST_RESIZE = 0x10;
// 当软键盘弹出时,窗口不需要调整大小, 要确保输入焦点是可见的,
// 例如有两个EditView的输入框,一个为Ev1,一个为Ev2,当你点击Ev1想要输入数据时,当前的Ev1的输入框会移到软键盘上方
// 该模式不能与SOFT_INPUT_ADJUST_RESIZE结合使用
public static final int SOFT_INPUT_ADJUST_PAN = 0x20;
// 将不会调整大小,直接覆盖在window上
public static final int SOFT_INPUT_ADJUST_NOTHING = 0x30;
软键盘配置 | 备注 |
---|---|
SOFT_INPUT_STATE_UNCHANGED | 不会改变软键盘的状态 |
SOFT_INPUT_STATE_VISIBLE | 当用户进入窗口时,显示软键盘 |
SOFT_INPUT_STATE_HIDDEN | 当用户进入该窗口时,隐藏软键盘 |
SOFT_INPUT_STATE_ALWAYS_HIDDEN | 当窗口获取焦点时,隐藏软键盘 |
SOFT_INPUT_STATE_ALWAYS_VISIBLE | 当窗口获取焦点时,显示软键盘 |
SOFT_INPUT_MASK_ADJUST | window 会调整大小以适应软键盘窗口 |
SOFT_INPUT_ADJUST_UNSPECIFIED | 没有指定状态,系统会选择一个合适的状态或依赖于主题的设置 |
SOFT_INPUT_ADJUST_RESIZE | 1. 当软键盘弹出时,窗口会调整大小,例如点击一个EditView,整个layout都将平移可见且处于软件盘的上方2. 同样的该模式不能与SOFT_INPUT_ADJUST_PAN结合使用3. 如果窗口的布局参数标志包含FLAG_FULLSCREEN,则将忽略这个值,窗口不会调整大小,但会保持全屏 |
SOFT_INPUT_ADJUST_PAN | 1. 当软键盘弹出时,窗口不需要调整大小, 要确保输入焦点是可见的2. 例如有两个EditView的输入框,一个为Ev1,一个为Ev2,当你点击Ev1想要输入数据时,当前的Ev1的输入框会移到软键盘上方3. 该模式不能与SOFT_INPUT_ADJUST_RESIZE结合使用 |
SOFT_INPUT_ADJUST_NOTHING | 将不会调整大小,直接覆盖在window上 |
我们还可以在AndroidManifest文件中设置软键盘的弹出规则:
<activity android:windowSoftInputMode="adjustNothing" />
这样我们就知道软键盘的弹出规则其实是按照Window来划分的,我们在代码中就可以灵活设置。
其他属性
除了上面的一些重要的属性外,还有几个比较常用的属性:
- x与y属性:指定Window左上角的位置。
- alpha:Window的透明度。
- gravity:Window在屏幕中的位置,使用的是Gravity类的常量。
- format:Window的像素格式,值定义在PixelFormat中。
好,到这里我们就说完了Window的属性。结合最开始说的Window就是View的载体,View是Window的表现形式来看,我们通过设置Window的各种属性,就可以在面对处理Activity、Dialog等各种不同View的表现形式了,对Android View的层级有了更清晰的认识。
原理探究
其实到这里,我们就对Window的作用以及使用都有了一个比较概况的了解,但是还有一些问题我们需要理解,这对后面了解Android系统很重要。
目前大概有如下问题需要我们再进一步探究源码:
- 第一个就是文章最开始说的ViewRootImpl类,既然Window是一个View树的抽象概念,那ViewRootImpl作为需要绘制View的类,它和Window是什么关系?
- 第二个问题就是在Android中有个唯一实现Window接口的PhoneWindow类,该类是具体的Window吗?它有什么作用?
- 既然Activity、Dialog和Toast都是通过Window来显示和管理View,他们有什么区别?为什么Dialog必须在Activity中弹出?
- 关于Token到底是什么?为什么有时弹出Dialog的Context不对会报出BadToken的异常?
上述问题我相信很多开发者都会有这个疑问,但是一时半会又说不清楚,这些知识比较复杂,需要深入源码来探究。
Window的内部机制
在前面说了Window是一个抽象概念,而添加一个Window是通过WindowManager来完成,所以想捋清楚Window和ViewRootImpl是什么关系,我们就需要来看一下Window的内部机制,这里从Window的添加、删除和更新说起。
Window的添加过程
Window的添加过程需要通过WindowManager的addView来实现,我们在Activity中使用下面代码:
windowManager.addView(btn, layoutParams)
这里可以直接获取Activity的WindowManager,然后调用addView方法,点击方法进去:
public interface ViewManager
{
public void addView(View view, ViewGroup.LayoutParams params);
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
public void removeView(View view);
}
这里我们可以发现WindowManager是继承这个接口的,而WindowManager也是一个接口:
@SystemService(Context.WINDOW_SERVICE)
public interface WindowManager extends ViewManager
那这个WindowManager的实现类是啥呢 通过代码中的强转我们可以知道,其实现类是WindowManagerImpl类,该类是通过getSystemService方法获得,我们可以去源码中找到WindowManagerImpl类:
@Override
91 public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
92 applyDefaultToken(params);
93 mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
94 }
这里可以发现是调用mGlobal去处理,这个mGlobal是WindowManagerGlobal实例,而且它还是一个APP全局单例,这里的工作模式就是典型的桥接模式,把所有的操作都委托给WindowManagerGlobal来实现;
所以WindowManagerGlobal是WindowManager的真正逻辑实现,我们看一下该类中的实现:
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow, int userId) {
//判断参数的合法性
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
if (display == null) {
throw new IllegalArgumentException("display must not be null");
}
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
}
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
//如果是子Window,需要对参数做额外调整
if (parentWindow != null) {
parentWindow.adjustLayoutParamsForSubWindow(wparams);
} else {
final Context context = view.getContext();
if (context != null
&& (context.getApplicationInfo().flags
& ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) {
wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
}
}
//ViewRootImpl实例
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
// 省略
//创建ViewRootImpl实例,并且设置参数
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
//分别记录View树 ViewRootImpl和Window参数,见分析1
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
try {
//最后通过ViewRootImpl来添加Window,见分析2
root.setView(view, wparams, panelParentView, userId);
} catch (RuntimeException e) {
...
}
}
}
上面方法较长,但是逻辑还是比较简单,由于WindowManagerGlobal是单例,它是真正WindowManager的逻辑实现类,所以需要把要处理的Window等都记录起来,这里也就是分析1处所说的,这里在类中直接定义3个集和来保存:
private final ArrayList<View> mViews = new ArrayList<View>();
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
private final ArrayList<WindowManager.LayoutParams> mParams =
new ArrayList<WindowManager.LayoutParams>();
分析2,在这里使用创建的ViewRootImpl实例,调用setView方法来添加Window,我们看一下该方法:
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
int userId) {
synchronized (this) {
...
//分析1,在被添加到WindowManager之前调用一次
requestLayout();
...
//通过WindowSession来完成IPC调用,完成创建Window
try {
mOrigWindowType = mWindowAttributes.type;
mAttachInfo.mRecomputeGlobalAttributes = true;
collectViewAttributes();
adjustLayoutParamsForCompatibility(mWindowAttributes);
controlInsetsForCompatibility(mWindowAttributes);
res = mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), userId,
mInsetsController.getRequestedVisibilities(), inputChannel, mTempInsets,
mTempControls);
if (mTranslator != null) {
mTranslator.translateInsetsStateInScreenToAppWindow(mTempInsets);
mTranslator.translateSourceControlsInScreenToAppWindow(mTempControls);
}
} catch (RemoteException e) {
mAdded = false;
mView = null;
mAttachInfo.mRootView = null;
mFallbackEventHandler.setView(null);
unscheduleTraversals();
setAccessibilityFocus(null, null);
throw new RuntimeException("Adding window failed", e);
} finally {
if (restore) {
attrs.restore();
}
}
...
}
}
上面代码是ViewRootImpl的setView方法部分逻辑,它主要干俩件事,第一件事就是更新界面,在注释分析1的地方,通过调用requestLayout来完成异步刷新请求,方法实现如下:
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
其中scheduleTraversal方法就是View绘制的入口;
接着会通过mWindowSession的addToDisplay方法来完成Window的添加过程,那这个mWindowSession是什么类的实例呢
通过查看源码可知,mWindowSession是一个IWindowSession对象,而IWindowSession是一个IBinder接口,所以mWindowSession只是一个Binder对象,而实现类在WindowManagerService中,这里通过mWindowSession完成了IPC通信。
然后真正添加Window的逻辑就交由WindowManagerService(简称WMS)了,由于WMS比较复杂,这里就不过多深入了。
到这里我们就解决了我们第一个疑惑了,也就是ViewRootImpl类的作用,该类是View和WindowManagerService的桥梁,在该类中对View进行了绘制,同时又通过IPC通信让WMS创建了Window。
其中几个涉及的类,来梳理一下:
-
ViewRootImpl,在调用addView时会创建实例,这也就说明一个View树对应一个ViewRootImpl,同时它是Window和View之间的桥梁,一边负责View的绘制,一边负责IPC通信到WMS创建Window。
-
IWindowSession实例,它是APP范围内单例,是一个Binder,负责和WMS通信。这里为什么一个一个应用就一个实例呢,这是因为WMS是系统服务,它要服务很多个APP,而一个APP又有多个Window,所以每个Window都要WMS来管理,则太多了,这样WMS只需要和APP的IWindowSession进行通信即可。
-
WindowManagerGlobal实例,前面我们调用WindowManager的addView方法时,会调用该类的单例,它可以看成是WindowManager的实现单例。
通过上面分析,可以画出下面架构图:
其中WMS可以先不分析,通过Window的添加过程分析,我们就可以大概捋清楚各种类的关系。
Window的删除过程
Window的删除过程和添加过程,都是先通过WindowManagerImpl后,然后调用WindowManagerGlobal单例的removeView方法实现,代码如下:
public void removeView(View view, boolean immediate) {
//判断参数合法性
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
synchronized (mLock) {
//遍历找出需要删除的View
int index = findViewLocked(view, true);
View curView = mRoots.get(index).getView();
//真正的删除
removeViewLocked(index, immediate);
if (curView == view) {
return;
}
throw new IllegalStateException("Calling with view " + view
+ " but the ViewAncestor is attached to " + curView);
}
}
这里逻辑非常简单,在前面添加Window里说过,在WindowManagerGlobal类中会有几个数组分别来保存View、ViewRootImpl等,所以这里非常容易就可以找到需要删除的View树,然后调用removeViewLocked方法:
private void removeViewLocked(int index, boolean immediate) {
ViewRootImpl root = mRoots.get(index);
View view = root.getView();
if (root != null) {
root.getImeFocusController().onWindowDismissed();
}
boolean deferred = root.die(immediate);
if (view != null) {
view.assignParent(null);
if (deferred) {
mDyingViews.add(view);
}
}
}
在removeViewLocked方法内我们可以看出是通过ViewRootImpl来完成删除操作的,这里先调用ViewRootImpl的die方法,然后把View加入mDyingViews数组中,该数组表示待删除的View列表,我们看一下这个die方法:
boolean die(boolean immediate) {
if (immediate && !mIsInTraversal) {
doDie();
return false;
}
if (!mIsDrawing) {
destroyHardwareRenderer();
} else {
Log.e(mTag, "Attempting to destroy the window while drawing!\n" +
" window=" + this + ", title=" + mWindowAttributes.getTitle());
}
mHandler.sendEmptyMessage(MSG_DIE);
return true;
}
在这里区分了是否立即删除,如果是理解删除则调用doDie方法,如果不是的话会使用Handler发送一个消息,我们直接看一下doDie方法:
void doDie() {
checkThread();
if (LOCAL_LOGV) Log.v(mTag, "DIE in " + this + " of " + mSurface);
synchronized (this) {
if (mRemoved) {
return;
}
mRemoved = true;
if (mAdded) {
//
dispatchDetachedFromWindow();
}
...
}
//刷新数据
WindowManagerGlobal.getInstance().doRemoveView(this);
}
在该方法内,真正删除View的逻辑在dispatchDetachedFormWindow方法中,而且调用WindowManagerGlobal的doRemoveView方法来刷新保存的列表,我们来看一下disptchDetachedFromWindow:
void dispatchDetachedFromWindow() {
mInsetsController.onWindowFocusLost();
mFirstInputStage.onDetachedFromWindow();
if (mView != null && mView.mAttachInfo != null) {
mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(false);
//分析1
mView.dispatchDetachedFromWindow();
}
//分析2
mAccessibilityInteractionConnectionManager.ensureNoConnection();
removeSendWindowContentChangedCallback();
destroyHardwareRenderer();
setAccessibilityFocus(null, null);
mInsetsController.cancelExistingAnimations();
mView.assignParent(null);
mView = null;
mAttachInfo.mRootView = null;
destroySurface();
if (mInputQueueCallback != null && mInputQueue != null) {
mInputQueueCallback.onInputQueueDestroyed(mInputQueue);
mInputQueue.dispose();
mInputQueueCallback = null;
mInputQueue = null;
}
try {
//分析3
mWindowSession.remove(mWindow);
} catch (RemoteException e) {
}
if (mInputEventReceiver != null) {
mInputEventReceiver.dispose();
mInputEventReceiver = null;
}
unregisterListeners();
unscheduleTraversals();
}
上面方法逻辑也非常简单,主要就做了3件事:
-
分析1处,调用View的dispatchDetectedFromWindow方法,在该方法内会调用onDeteachedFromWindow方法,该方法我们在做自定义View时,可以在该方法内做一些资源回收的工作,比如终止动画、停止线程等。
-
分析2处,垃圾回收相关工作,比如清楚数据和消息,移除回调等。
-
分析3处,通过Session的remove方法来删除Window,这里也是一个IPC过程,真正删除的地方还是WMS。
通过删除Window的过程,我们更加可以确定了前面说的几个类的关系。
Window的更新过程
看完了Window的添加和删除过程,再看Window的更新过程就比较容易了,我们还是看WindowManagerGlobal中的updateViewLayout方法:
public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
//参数校验
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
}
//对view设置LayoutParams
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
view.setLayoutParams(wparams);
//更新
synchronized (mLock) {
int index = findViewLocked(view, true);
ViewRootImpl root = mRoots.get(index);
mParams.remove(index);
mParams.add(index, wparams);
root.setLayoutParams(wparams, false);
}
}
上面代码逻辑非常简单,先是更新view的布局信息,然后更新mParams列表,最后调用ViewRootImpl的setLayoutParams方法,在该方法中会对View进行重新布局,包括测量、布局和绘制,然后通过WindowSession调用IPC通信给WMS来更新Window。
Window的创建过程
通过上面我们手动分析Window的添加、删除和更新过程原理,我们知道View是Window的表现形式,而Window是View的载体,因此任何有视图的地方都有Window,比如Activity、Dialog、Toast等视图。
在前面说Window内部机制的时候,我们添加一个Window是如下方法:
windowManager.addView(btn, layoutParams)
这里我们直接调用Activity的WindowManager来完成的。这里我们就有几个疑问:
-
这个Activity的WindowManager实例是什么时候创建的,我们前面分析知道其实最后都会调用WindowManagerGlobal类的APP单例来真正去实现,那WindowManager的实现类WindowManagerImpl有什么用呢?为什么不直接使用WindowManagerGlobal类。
-
了解Activity的开发者知道,有一个Window的实现类PhoneWindow,包含一个DecorView,然后里面才是我们平时设置的布局,那这一层关系是如何建立的呢?
上面2个问题,我相信很多开发者都想知道,所以这里我们就来分析分析Android内置几种视图的Window的创建过程,这非常有利于理解Android系统。
Activity的Window创建过程
要分析Activity的Window创建过程,必须要了解Activity的启动流程,这里就先不过多说启动流程相关的内容,在Activity启动的过程中,最终会由ActivityThread中的performLaunchActivity方法来完成真个启动过程。
在这个方法中会先通过类加载器创建Activity的实例对象,然后调用其attach方法为其关联运行过程中所依赖的上下文环境变量:
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
...
Activity activity = null;
try {
java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
...
} catch (Exception e) {
...
}
try {
...
Window window = null;
...
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window, r.configCallback,
r.assistToken, r.shareableActivityToken);
...
return activity;
}
在Activity中的attach方法中,系统会创建Activity所属的Window对象,并且为其设置回调接口:
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(mWindowControllerCallback);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
这里可以发现创建了PhoneWindow实例,而且设置了回调,同时Activity自己就实现了接口,所以当Window接收到外界的状态改变时,就会回调Activity的方法,常这个Callback接口中有一些我们比较熟悉的方法:
public interface Callback {
public boolean dispatchKeyEvent(KeyEvent event);
boolean onMenuOpened(int featureId, @NonNull Menu menu);
boolean onMenuItemSelected(int featureId, @NonNull MenuItem item);
public void onWindowFocusChanged(boolean hasFocus);
public void onAttachedToWindow();
public void onDetachedFromWindow();
...
}
这几个方法可以说是我们非常熟悉的了,现在知道它是在这个时候通过Window给我们设置的回调。
然后就是我们经常听说的PhoneWindow了,在这里我们会创建出该实例,这里有个点要特别注意,那就是这个PhoneWindow。
PhoneWindow是实现Window接口,同时也是Android唯一一个实现Window接口的类,那它的实例就是确切的Window实例吗 注意,这里不是的。这里的PhoneWindow类并不是我们前面所说的抽象Window,它只是可以看成协助Activity的Window帮助类。
回到主线,那我们Activity的View树是如何和Window建立关联呢,在setContentView方法中实现的,如下:
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
该方法我们再也熟悉不过了,而这里的getWindow返回的“Window”就是前面的PhoneWindwo类实例,我们看一下其实现方法:
public void setContentView(int layoutResID) {
//如果没有DecorView,就创建它
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
//将Activity布局添加到mContentParent中
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
//进行回调到Activity
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
上面代码大致有如下3个主要步骤:
- 如果没有DecorView,就创建它。
DecorView直接翻译叫做装饰View,它其实是一个FrameLayout,DecorView是Activity中的顶级View,一般来说它包含内部标题栏和内容栏,而且这个会随着主题变化而改变。
而其中的内容栏是一定要存在的,它的固定Id就是content,DecorView的创建过程由installDecor方法来完成,而创建完的DecorView还是一个空白的FrameLayout;然后再通过generateLayout方法来加载布局文件到DecorView中,具体的布局文件和系统版本和主题有关,一般是一个LinearLayout,并且内容ViewGroup的id固定为content,就是mContentParent。
- 将View添加到DecorView的mContentParent中。
在初始化完DecorView后,直接解析出需要设置的布局,把它添加到mContentParent中。而这里由于mContentParent的id是content,也就侧面说明了为什么在Activity中设置布局的方法名是setContentView而不是setView。
- 回调Activity的onContentChanged方法通知Activity视图已经发送改变。
这个就比较简单了,由于Activity实现了Window的Callback接口,这里表示Activity的布局已经被添加到DecorView的mContentParent中了,于是要通知Activity。这个默认是个空实现,我们在子Activity中处理这个回调。
经过上面3个步骤,我们的DecorView已经创建且初始化完毕,而且Activity的文件也成功添加到DecorView的mContentParent中,但是注意这个DecorView并没有被WindowManager添加到Window中。
这里需要正确理解Window概念,虽然前面创建了PhoneWindow,但是它却不是真正的Window,这时虽然DecorView被创建出来了,但是无法接收外界输入信息。
在ActivityThread的handleResumeActivity中,会先调用Activity的onResume方法,接着调用Activity的makeVisible方法:
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
这里又是熟悉的代码,也就是前面所说的Window添加过程,这里才真正把DecorView和Window绑定,且通过WMS显示出来,Activity才可以正常收到外界输入信息。
同时这里也说明了在Activity的生命周期中,为什么在onResume回调中才可以接受到手指的触摸事件。
PhoneWindow
看完Activity的Window的创建过程,我们要搞清楚一个东西,就是PhoneWindow,我们查看一下该类的定义:
/**
* Android-specific Window.
public class PhoneWindow extends Window
可以发现它虽然是继承至Window,但是它却不是Window的存在形式,因为Window是抽象、不存在的。
这里看起来有点费解,我们可以给出2个问题, 这2个问题经常被一些人误解:
-
有些资料认为PhoneWindow就是Window,是View的容器,负责管理容器内的View,WindowManagerImpl可以让里面添加View;但是这个说法无法解释一个问题,由于每个Window都对应一个ViewRootImpl,为什么在addView的方法中会创建一个ViewRootImpl,又在ViewRootImpl中和WMS通信创建Window呢?这明显说不通。
-
还有些资料认为PhoneWindwo就是Window,在addView方法中添加的不是View而是Window;但是这个说法同样无法解释为什么在addView方法中创建Window的过程却没有创建PhoneWindow对象。
所以还是那句话,Window是一个抽象的概念,它代表一个View树,而PhoneWindow则表示是一个Window的工具类,来辅助我们创建Window。
从前面Activity的Window创建过程以及我们平时熟悉的事件分发机制,可以轻松画出下面的图:
但是该图有个致命问题,就是很容易让人误解这个PhoneWindow,还是那句话,Window是一个抽象概念,是不真实存在的,而PhoneWindow则可以看出WindowUtils类,所以下图更合适:
这里的PhoneWindow确实存在,但是它不是一个真的Window,所以用绿色虚线表示,而Window则是通过PhoneWindow添加的,它其实是一个抽象概念,我们也是无法看见的,所以也用虚线表示。
那这里的PhoneWindow的意义是什么呢?可以从下面3个方面看出其设计意图:
- 提供DecorView模板。
我们在Activity中通过setContentView设置我们所想展示的UI界面布局,而该方法在前面分析流程中说了,它会利用PhoneWindow来创建DecorView,而DecorView的创建又和运用的主题等有关,所以这里通过给出一个DecorView的UI模板来简化这部分工作。
- 抽离Activity中关于Window的逻辑。
Activity的职责非常多,如果所有事情都它自己来做就非常臃肿,所以关于Window相关的事情就交给了PhonewWindow来处理。实际上,Activity调用的是WindowManagerImpl,但是PhoneWindow和WindowManagerImpl俩者是成对存在,他们共同处理Windown事务,所以这里写成交给PhoneWindow处理没有问题。
当Activity需要添加View到屏幕上时,直接通过setContentView,而该方法又调用PhoneWindow的setContentView方法,来实现把布局设置到屏幕上,至于具体如何完成,Activity不必管。
- 限制组件添加Window的权限。PhoneWindow内部有一个token属性,用于验证一个PhoneWindow是否允许添加Window。在Activity创建PhoneWindow的时候,会把从AMS传过来的token赋值给它,从而它也有了添加token的权限。
好了,到这里我们对常见的类都有了明确的认识:PhoneWindow其实是一个Window帮助类,它和WindowManagerImpl成对出现,而最后都会通过WindowManagerGlobal的全局单例来真正实现。
Dialog的Window创建过程
Dialog的Window的创建过程和Actvitiy类似,可以分为如下几个步骤:
- 创建Window
创建Window的地方就是直接在Dialog的构造函数中:
Dialog(@UiContext @NonNull Context context, @StyleRes int themeResId,
boolean createContextThemeWrapper) {
...
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
final Window w = new PhoneWindow(mContext);
mWindow = w;
w.setCallback(this);
w.setOnWindowDismissedCallback(this);
w.setOnWindowSwipeDismissedCallback(() -> {
if (mCancelable) {
cancel();
}
});
w.setWindowManager(mWindowManager, null, null);
w.setGravity(Gravity.CENTER);
mListenersHandler = new ListenersHandler(this);
}
可以发现这里和Activity一样创建的是PhoneWindow,同样设置一些回调。
- 初始化DecorView,并将Dialog的视图添加到DecorView中。
这个步骤也和Activity类似:
public void setContentView(@NonNull View view) {
mWindow.setContentView(view);
}
- 将DecorView添加到Window中并显示。
前面在Activity中,是在onResume回调中做的该逻辑,在Dialog中,是在show方法中完成的这个步骤:
public void show() {
...
mWindowManager.addView(mDecor, l);
mShowing = true;
..,
}
然后当Dialog被关闭时,在dismissDialog方法中通过removeViewImmediate方法来移除Window:
void dismissDialog() {
...
try {
mWindowManager.removeViewImmediate(mDecor);
} finally {
...
}
}
从这里发现Dialog的Window创建过程和Activity及其相似。
普通的Dialog有一个特殊之处,那就是必须使用Activity的Context,如果采用Application的Context,会报如下错误:
val dialog = Dialog(this.applicationContext)
val tv = TextView(this)
dialog.setContentView(tv)
dialog.show()
上述代码报错如下:
Process: com.zyh.window, PID: 18714
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.zyh.window/com.zyh.window.MainActivity}: android.view.WindowManager$BadTokenException: Unable to add window -- token null is not for an application
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2670)
这个错误很明确,是没有应用token导致的,而应用token一般只有Activity拥有,所以这里只需要使用Activity作为Context来显示对话框即可。
另外,系统Window是不需要token的,所以可设置Dialog的Window的type为系统Dialog,注意这里是需要申请权限的,在前面我们type属性中,我们已经说过了。
Toast的Window创建过程
Toast的Window创建过程比较复杂点,但是这里可以挖掘的地方有点多,其中就有一个非常著名的问题:子线程可以弹出Toast吗 看完Toast的Window创建过程,就明白了。
由于Toast具备定时取消这个功能,所以Toast内部有俩类IPC过程,第一类是Toast访问NotificationManagerService(简称NMS),第二类是NMS回调Toast的TN接口。Toast属于系统Window,它内部的视图由2种方式指定,一种是系统默认样式,一种是setView方法设置,但是都对应着内部mNextView成员。
Toast提供了show和cancel方法,分别用于显示和隐藏Toast,但是他们都是一个IPC过程:
public void show() {
...
//
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
final int displayId = mContext.getDisplayId();
try {
if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
if (mNextView != null) {
//调用NMS
service.enqueueToast(pkg, mToken, tn, mDuration, displayId);
} else {
...
}
} else {
...
}
} catch (RemoteException e) {
// Empty
}
}
public void cancel() {
if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)
&& mNextView == null) {
try {
//IPC通信
getService().cancelToast(mContext.getOpPackageName(), mToken);
} catch (RemoteException e) {
}
} else {
}
}
可以发现Toast的显示和隐藏都是需要通过NMS来实现,由于NMS运行在系统进程中,所以只能通过IPC来实现。
注意这里的TN类,它是一个Binder类,在Toast和NMS进行IPC过程中,当NMS处理Toast的显示或者隐藏请求时会回调TN中的方法,所以它比较关键,等会重点分析。
既然是先和NMS通信,我们简单说一下NMS中的代码,代码较多,我们来简单说一下流程:
-
调用NMS方法携带3个参数,分别是当前包名、tn远程回调和Toast时长。NMS先把Toast请求封装为ToastRecord对象,添加到mToastQueue队列中。
-
mToastQueue是一个50容量的ArrayList,这样做是为了防止DOS,假如通过某个方法大量的连续弹出Toast,这将导致其他应用没机会弹出Toast。
-
正常情况下,是达不到存储上限的,当ToastRecord被添加到mToastQueue中,NMS会通过showNextToastLocked方法来显示当前的Toast。
-
Toast的显示是由ToastRecord的callback来完成的,而这个callback实际上就是传递进来的tn,所以最终调用TN中的方法会是在Binder线程池中。
-
Toast显示后,NMS还会通过scheduleTimeoutLocked方法发送一个延迟消息,同样是经过callback回调,这个就是取消Toast的动作。
所以从上面分析来看,Toast和NMS进行了多次IPC通信,但是真正去显示Toast的还是得由Toast类来完成,也就是上面所说的TN类:
private static class TN extends ITransientNotification.Stub {
...
final Handler mHandler;
...
TN(Context context, String packageName, Binder token, List<Callback> callbacks,
@Nullable Looper looper) {
IAccessibilityManager accessibilityManager = IAccessibilityManager.Stub.asInterface(
ServiceManager.getService(Context.ACCESSIBILITY_SERVICE));
mPresenter = new ToastPresenter(context, accessibilityManager, getService(),
packageName);
mParams = mPresenter.getLayoutParams();
mPackageName = packageName;
mToken = token;
mCallbacks = callbacks;
mHandler = new Handler(looper, null) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case SHOW: {
IBinder token = (IBinder) msg.obj;
handleShow(token);
break;
}
case HIDE: {
handleHide();
// Don't do this in handleHide() because it is also invoked by
// handleShow()
mNextView = null;
break;
}
case CANCEL: {
handleHide();
// Don't do this in handleHide() because it is also invoked by
// handleShow()
mNextView = null;
try {
getService().cancelToast(mPackageName, mToken);
} catch (RemoteException e) {
}
break;
}
}
}
};
}
...
}
这里我们会发现这里使用了Handler,但是前面梳理NMS时会说TN的回调其实是在Binder线程中,即运行在子线程中,所以这里的mHandler对象就有意思了
当我们在主线程弹出Toast肯定没问题,那我们在子线程弹出Toast呢
private fun showToast(){
thread {
Toast.makeText(this, "哈哈哈", Toast.LENGTH_SHORT).show()
}
}
上述代码会报下面错误:
Process: com.zyh.window, PID: 13724
java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
at android.os.Handler.<init>(Handler.java:200)
at android.os.Handler.<init>(Handler.java:114)
at android.widget.Toast$TN$2.<init>(Toast.java:351)
at android.widget.Toast$TN.<init>(Toast.java:351)
at android.widget.Toast.<init>(Toast.java:103)
可以发现这里的错误并不是说View不能在子线程绘制,而是说没有Looper,所以我们给子线程也获取一下Looper:
private fun showToast(){
thread {
Looper.prepare()
Toast.makeText(this, "哈哈哈", Toast.LENGTH_SHORT).cancel()
Looper.loop()
}
}
运行如下:
居然可以正常显示出了Toast了,所以这里我们可以知道了Toast不能显示在子线程和UI绘制在子线程其实并不是一个错误,Toast是可以在子线程中弹出的。
而后面的代码也不必多说了,和Activity、Dialog一样,在show方法中会去调用WindowManager的addView方法,最终显示出UI。
关于其他的比如PopupWindow的Window显示流程,这里就不细说了。
总结
不知不觉这篇文章已经一万多字了,能看到这里的读者大致也可以对Window有了个比较健全的理解,最后还是按照文章的目录给做个总结。
- 概念部分
- Window和Window类是俩个东西,Window是抽象概念、不真实存在的,类比于学生和班集体。
- Window是View的载体,View是Window的表现形式,可以把一个View树看成一个Window。
- Android设计出Window是为了方便显示不同View的层级以及事件分发等控制方式。
- Window的属性
- type属性,即Z-Order属性,值越大,显示层级越高,离用户越近,分为应用级Window、子Window和系统Window。
- flag,设置各种Window的事件处理和分发控制。
- 软键盘设置,设置软键盘弹出策略,不仅可以用代码设置,还可以在manifest中设置。
- 其他一些属性,设置显示位置等。
- Window内部机制
- WindowManager是访问Window的入口,通过3个方法来完成操作Window。
- WindowManagerGlobal是APP单例类,是实现WindowManager的类,是一个单例类。
- addView时,在WindowManagerGlobal中会创建ViewRootImpl类,这时就是一个View树对应一个ViewRootImpl实例。
- 在ViewRootImpl中会和WMS进行IPC通信,真正Window的创建是通过WMS来创建。
- ViewRootImpl是View树和Window的桥梁,一边和WMS通信,一边可以绘制View。
- Window的创建过程
- 通过Activity的Window创建过程,可以知道PhoneWindow并不是真实的Window,它是一个Window帮助类。
- PhoneWindow和WindowManagerImpl是一一对应,真实逻辑是在WindowManagerGlobal中实现。
- 设计PhoneWindow主要是为了解耦,提供DecorView模板已经剥离出Window逻辑。
- DecorView添加到Window是在Activity的onResume方法中,即该生命周期后,才可以获取手指触摸事件。
- Dialog的Window创建和Activity类似,DecorView被WMS创建是在show方法中,同时普通Dialog只能使用Activity的Context。
- Toast的创建过程,需要Toast和NMS多次IPC交互,由于TN方法回调在Binder线程池中,所以子线程想弹出Toast,必须要先获取到Looper。