Android的Window详解

6,002 阅读35分钟

前言

本篇文章是《Android开发艺术探索》第8章的理解和实践,该篇文章涉及的源码知识点较多,来逐一分解。

为什么会突然想了解一下Window呢,原因有如下2个:

  • 一个是在Android View工作原理的探索时有提及到ViewRootImpl这个类,当时说的是该类负责View的测量、布局和绘制,那这个ViewRootImpl的来由是什么,为什么是它来负责绘制;
  • 一个是我们经常使用Dialog、PopupWindow等弹窗,同时设置其Window属性,但是这些有什么区别,各种Window的属性又是啥。

而这俩个原因,从某种程度上说就说明了为什么Android需要设计出这个Window。这里我们先说一些概念和设计原则,再来一一通过实践和源码来理解。

参考的文档有:

mp.weixin.qq.com/s/1j1lsciZF…

《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_APPLICATIONTYPE_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_DIALOGFIRST_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_BEHINDWindow 后面的内容都变暗
FLAG_BLUR_BEHINDAPI 已经过时,Window 后面的内容都变模糊
FLAG_NOT_FOCUSABLEWindow 不能获得输入焦点,即不接受任何按键或按钮事件,例如该 Window 上 有 EditView,点击 EditView 是 不会弹出软键盘的 Window 范围外的事件依旧为原窗口处理;例如点击该窗口外的view,依然会有响应。另外只要设置了此Flag,都将会启用FLAG_NOT_TOUCH_MODAL
FLAG_NOT_TOUCH_MODAL设置了该 Flag,将 Window 之外的按键事件发送给后面的 Window 处理, 而自己只会处理 Window 区域内的触摸事件,Window 之外的 view 也是可以响应 touch 事件。
FLAG_NOT_TOUCHABLEWindow 将不会接受任何 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_ADJUSTwindow 会调整大小以适应软键盘窗口
SOFT_INPUT_ADJUST_UNSPECIFIED没有指定状态,系统会选择一个合适的状态或依赖于主题的设置
SOFT_INPUT_ADJUST_RESIZE1. 当软键盘弹出时,窗口会调整大小,例如点击一个EditView,整个layout都将平移可见且处于软件盘的上方2. 同样的该模式不能与SOFT_INPUT_ADJUST_PAN结合使用3. 如果窗口的布局参数标志包含FLAG_FULLSCREEN,则将忽略这个值,窗口不会调整大小,但会保持全屏
SOFT_INPUT_ADJUST_PAN1. 当软键盘弹出时,窗口不需要调整大小, 要确保输入焦点是可见的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类:

androidxref.com/9.0.0_r3/xr…

@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的实现单例。

通过上面分析,可以画出下面架构图:

image.png

其中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. 分析1处,调用View的dispatchDetectedFromWindow方法,在该方法内会调用onDeteachedFromWindow方法,该方法我们在做自定义View时,可以在该方法内做一些资源回收的工作,比如终止动画、停止线程等。

  2. 分析2处,垃圾回收相关工作,比如清楚数据和消息,移除回调等。

  3. 分析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来完成的。这里我们就有几个疑问:

  1. 这个Activity的WindowManager实例是什么时候创建的,我们前面分析知道其实最后都会调用WindowManagerGlobal类的APP单例来真正去实现,那WindowManager的实现类WindowManagerImpl有什么用呢?为什么不直接使用WindowManagerGlobal类。

  2. 了解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个主要步骤:

  1. 如果没有DecorView,就创建它。

DecorView直接翻译叫做装饰View,它其实是一个FrameLayout,DecorView是Activity中的顶级View,一般来说它包含内部标题栏和内容栏,而且这个会随着主题变化而改变。

而其中的内容栏是一定要存在的,它的固定Id就是content,DecorView的创建过程由installDecor方法来完成,而创建完的DecorView还是一个空白的FrameLayout;然后再通过generateLayout方法来加载布局文件到DecorView中,具体的布局文件和系统版本和主题有关,一般是一个LinearLayout,并且内容ViewGroup的id固定为content,就是mContentParent。

  1. 将View添加到DecorView的mContentParent中。

在初始化完DecorView后,直接解析出需要设置的布局,把它添加到mContentParent中。而这里由于mContentParent的id是content,也就侧面说明了为什么在Activity中设置布局的方法名是setContentView而不是setView。

  1. 回调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个问题经常被一些人误解:

  1. 有些资料认为PhoneWindow就是Window,是View的容器,负责管理容器内的View,WindowManagerImpl可以让里面添加View;但是这个说法无法解释一个问题,由于每个Window都对应一个ViewRootImpl,为什么在addView的方法中会创建一个ViewRootImpl,又在ViewRootImpl中和WMS通信创建Window呢?这明显说不通。

  2. 还有些资料认为PhoneWindwo就是Window,在addView方法中添加的不是View而是Window;但是这个说法同样无法解释为什么在addView方法中创建Window的过程却没有创建PhoneWindow对象。

所以还是那句话,Window是一个抽象的概念,它代表一个View树,而PhoneWindow则表示是一个Window的工具类,来辅助我们创建Window。

从前面Activity的Window创建过程以及我们平时熟悉的事件分发机制,可以轻松画出下面的图:

image.png

但是该图有个致命问题,就是很容易让人误解这个PhoneWindow,还是那句话,Window是一个抽象概念,是不真实存在的,而PhoneWindow则可以看出WindowUtils类,所以下图更合适:

image.png

这里的PhoneWindow确实存在,但是它不是一个真的Window,所以用绿色虚线表示,而Window则是通过PhoneWindow添加的,它其实是一个抽象概念,我们也是无法看见的,所以也用虚线表示。

那这里的PhoneWindow的意义是什么呢?可以从下面3个方面看出其设计意图:

  1. 提供DecorView模板。

我们在Activity中通过setContentView设置我们所想展示的UI界面布局,而该方法在前面分析流程中说了,它会利用PhoneWindow来创建DecorView,而DecorView的创建又和运用的主题等有关,所以这里通过给出一个DecorView的UI模板来简化这部分工作。

  1. 抽离Activity中关于Window的逻辑。

Activity的职责非常多,如果所有事情都它自己来做就非常臃肿,所以关于Window相关的事情就交给了PhonewWindow来处理。实际上,Activity调用的是WindowManagerImpl,但是PhoneWindow和WindowManagerImpl俩者是成对存在,他们共同处理Windown事务,所以这里写成交给PhoneWindow处理没有问题。

当Activity需要添加View到屏幕上时,直接通过setContentView,而该方法又调用PhoneWindow的setContentView方法,来实现把布局设置到屏幕上,至于具体如何完成,Activity不必管。

  1. 限制组件添加Window的权限。PhoneWindow内部有一个token属性,用于验证一个PhoneWindow是否允许添加Window。在Activity创建PhoneWindow的时候,会把从AMS传过来的token赋值给它,从而它也有了添加token的权限。

好了,到这里我们对常见的类都有了明确的认识:PhoneWindow其实是一个Window帮助类,它和WindowManagerImpl成对出现,而最后都会通过WindowManagerGlobal的全局单例来真正实现。

Dialog的Window创建过程

Dialog的Window的创建过程和Actvitiy类似,可以分为如下几个步骤:

  1. 创建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,同样设置一些回调。

  1. 初始化DecorView,并将Dialog的视图添加到DecorView中。

这个步骤也和Activity类似:

public void setContentView(@NonNull View view) {
    mWindow.setContentView(view);
}
  1. 将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中的代码,代码较多,我们来简单说一下流程:

  1. 调用NMS方法携带3个参数,分别是当前包名、tn远程回调和Toast时长。NMS先把Toast请求封装为ToastRecord对象,添加到mToastQueue队列中。

  2. mToastQueue是一个50容量的ArrayList,这样做是为了防止DOS,假如通过某个方法大量的连续弹出Toast,这将导致其他应用没机会弹出Toast。

  3. 正常情况下,是达不到存储上限的,当ToastRecord被添加到mToastQueue中,NMS会通过showNextToastLocked方法来显示当前的Toast。

  4. Toast的显示是由ToastRecord的callback来完成的,而这个callback实际上就是传递进来的tn,所以最终调用TN中的方法会是在Binder线程池中。

  5. 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()
    }
}

运行如下:

image.png

居然可以正常显示出了Toast了,所以这里我们可以知道了Toast不能显示在子线程和UI绘制在子线程其实并不是一个错误,Toast是可以在子线程中弹出的。

而后面的代码也不必多说了,和Activity、Dialog一样,在show方法中会去调用WindowManager的addView方法,最终显示出UI。

关于其他的比如PopupWindow的Window显示流程,这里就不细说了。

总结

不知不觉这篇文章已经一万多字了,能看到这里的读者大致也可以对Window有了个比较健全的理解,最后还是按照文章的目录给做个总结。

  1. 概念部分
  • Window和Window类是俩个东西,Window是抽象概念、不真实存在的,类比于学生和班集体。
  • Window是View的载体,View是Window的表现形式,可以把一个View树看成一个Window。
  • Android设计出Window是为了方便显示不同View的层级以及事件分发等控制方式。
  1. Window的属性
  • type属性,即Z-Order属性,值越大,显示层级越高,离用户越近,分为应用级Window、子Window和系统Window。
  • flag,设置各种Window的事件处理和分发控制。
  • 软键盘设置,设置软键盘弹出策略,不仅可以用代码设置,还可以在manifest中设置。
  • 其他一些属性,设置显示位置等。
  1. Window内部机制
  • WindowManager是访问Window的入口,通过3个方法来完成操作Window。
  • WindowManagerGlobal是APP单例类,是实现WindowManager的类,是一个单例类。
  • addView时,在WindowManagerGlobal中会创建ViewRootImpl类,这时就是一个View树对应一个ViewRootImpl实例。
  • 在ViewRootImpl中会和WMS进行IPC通信,真正Window的创建是通过WMS来创建。
  • ViewRootImpl是View树和Window的桥梁,一边和WMS通信,一边可以绘制View。
  1. 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。