Android Window机制解析

前言

 其实提起Window,作为Android开发都能提上几嘴,比如Window是界面的载体,我们的Activity最终就是要通过WindowManagerService把界面加载到Window展示出来;Window有一个唯一的实现类是PhoneWindow等等等等。但话说回来,如果问自己真的理解Window的整个机制吗,那我肯定支支吾吾,不敢作声。

 也源于上一篇View自定义的文章,既然子view都是由父view来布局的,那么最顶层的view呢?当我们打开APP,Activity的画面是怎么展现到我们面前,这中间的过程经历了什么,又是怎么调度的呢?神秘的DecorView和ViewRootImpl在其中又扮演者什么角色呢?

 那么就带着这些问题和好奇,我们来好好梳理一下Window的整个机制。

注:以下源码基于Android SDK API 31

相关概念

 Window的知识点比较多,先说一下相关的概念,方便后面理解。

 首先Window表示的是窗口的概念,但这跟PC端有点差别,比如PC端说的多窗口其实就是指打开多个应用(我是这么理解,不知道对不对)。而Android的窗口并不单单是指代应用层面,比如你打开一个APP,当前确实就是一个Window,但如果在当前页面有个Dialog,那其实这个Dialog它自己也是个Window,只不过是依赖在当前这个界面的Window上。

Window和View

 在Android中Window是一个抽象的概念,Android所有的视图都是通过Window来呈现,不论是Activity、Dialog还是Toast,对应的视图其实都是附加在Window上的,也就是说Window是View的载体。

 为什么说Window是一个抽象的概念呢,比如一个Activity的View树(整体布局),展现出来我们就可以说是一个Window。但是我们只能看到View(体现出来),却无法看到实际的Window(展现形式),因为它本身并不存在。有个比喻很恰当,就是类比班集体的话,学生就是一个个View,一整个班的学生就是班集体,但班集体是抽象出来的概念,我们看不见摸不着。当然了,还是那句话,概念是帮助我们理解事物的,我们不必去纠结概念本身,能够(通过源码)理解就行。

View树

 View树其实就是一系列View(ViewGroup)的组合。在Activity中,对应的整体布局就是一个View树,它有自己最顶层的View(ViewGroup),也就最底层(或者说嵌套最内层)的View(ViewGroup);而对于一个自定义布局的Dialog来说,它也拥有自己独立的整体布局,它并不属于Activity的View树,所以这就是两个不同的View树,对应两个不同的Window。

设计初衷

 为什么Android要把一个View树看成是一个Window呢?其实这就是好比班集体的概念一样,每当组织活动的时候,把通知下发给各个班集体就行,然后每个班集体再自己内部消化,这样管理起来就比较容易。

 那既然不同的Window管理着自己对应的View树,那不同的Window同时出现怎么办?因为手机的屏幕就那么大,总不能再去分屏操作吧(当然这也行),那么就引出了Window的显示层级(或者说View的显示层级)。比如我们在Activity上
弹出Dialog,然后再弹出一个Toast,那么该如何保证Dialog是显示在Activity之上,而Toast又是显示在Dialog之上的呢?此时如果我们刷新Activity的UI,那Dialog和Toast的UI也需要刷新吗?而当把这些View树分开,通过各自的Window来管理之后,事情就变得简单了,可以比较轻松的去实现不同View树的分层级显示了。

 而这样做的带来的另一个好处就是便于点击事件的分发,还是上面的例子,这时我们给屏幕一个点击事件,那么是该由Activity来处理还是Dialog来处理呢?这也由Window来统一分发和实现就行。

 总体来说,Window的出现就是为了解耦,虽然实际还是由View来显示,但我们通过把View树当成一个集体,让视图的显示和事件传递都变得非常方便。

Window和WindowManager

 通过源码我们可以发现,Window实际上是一个抽象类,而它的一个唯一实现类就是PhoneWindow。

Window.java
/**
 * 顶级窗口外观和行为策略的抽象基类。此类的实例应用作添加到窗口管理器的顶级视图。它提供标准的 UI 策略,例如背景、标 
 * 题区域、默认键处理等。
 * 此抽象类的唯一现有实现是 android.view.PhoneWindow,当你需要 Window 时,应实例化该实现。
 */
public abstract class Window {
     ...
}

PhoneWindow.java
public class PhoneWindow extends Window implements MenuBuilder.Callback {
    private final static String TAG = "PhoneWindow";
    ...
}

 创建Window的话很简单,只需要WindowManager即可完成。

val textView = TextView(this).apply {
      text = "通过WindowManager添加一个View(Window)"
}

val layoutParams = WindowManager.LayoutParams(
      WindowManager.LayoutParams.WRAP_CONTENT,
      WindowManager.LayoutParams.WRAP_CONTENT,
      0,
      0,
      PixelFormat.TRANSPARENT
)
layoutParams.gravity = Gravity.CENTER
layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION
val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
windowManager.addView(textView, layoutParams)

 上面的代码逻辑就可以在页面上创建出一个Window,而该Window的表现形式(我们直观看到的)就是一个TextView,Window本身我们是看不见摸不着的。可以看到,我们是通过WindowManager类的addView方法来实现的,看下WindowManager:

public interface WindowManager extends ViewManager {
           ....
}

  WindowManager其实是一个接口,继承自ViewManager:

/**
 * 用于向 Activity 添加和删除子视图的接口。若要获取此类的实例,请调用 Context.getSystemService()。
 */
public interface ViewManager {
    public void addView(View view, ViewGroup.LayoutParams params);
    public void updateViewLayout(View view, ViewGroup.LayoutParams params);
    public void removeView(View view);
}

  ViewManager也是一个接口,很明显我们刚刚使用的addView方法,就是这里继承的。ViewManager接口有三个方法,其实可以看出就是用来规范视图相关操作的。注意看注释,用于向 Activity 添加和删除子视图的接口。若要获取此类的实例,请调用 Context.getSystemService()。我们再回头看下刚刚在界面上创建TextView时的操作,我们的WindowManager实例是哪里来的,是不是就是通过Context的getSystemService()方法拿到的。这里先不展开,大概清楚就行,后面再具体说。

  所以我们知道,通过WindowManager的这三个方法,就可以来操作Window了。关于这里具体的添加逻辑和显示逻辑,我们等下说,先来看看Window的属性有哪些。

type属性

  type表示的是Window的类型,一共有三种:应用程序Window子程序Window系统Window。而类型就决定了Window的显示层序,假如一个Window的类型是系统Window,那么它一定会显示在其他应用程序类型的Window上面。

  显示层级是由Z-Order属性来控制的,即Z轴的值(类似3D坐标轴),而type实际上就是给Z-Order提供值的。type值越大,对应的Window就显示在越上面(层级越高),换句话说,就是type值大的Window可以把type值小的盖住。我们来看下不同类型的Window都有哪个type值。

应用程序Window(Application Window)

  应用程序Window的type值范围是[1-99],那什么是应用程序Window呢?比如我们最熟悉的Activity(所展示的页面)就是,这也很好理解,因为Activity就是应用最基础的展现形式,那当然属于应用程序Window了。系统封装了一系列的常量,在WindowManager#LayoutParams里:

//表示普通应用程序窗口的窗口类型的开始。
public static final int FIRST_APPLICATION_WINDOW = 1;

//窗口类型:作为整个应用程序的“基本”窗口的应用程序窗口;所有其他应用程序窗口将出现在其顶部。在多用户系统中,仅在拥有
//用户的窗口上显示。
public static final int TYPE_BASE_APPLICATION   = 1;

//窗口类型:普通应用程序窗口。token 必须是Activity的token,用于标识窗口所属的用户。在多用户系统中,仅在拥有用户的窗口
//上显示。
public static final int TYPE_APPLICATION        = 2;

//窗口类型:应用程序启动时显示的特殊应用程序窗口,不供应用程序本身使用。
public static final int TYPE_APPLICATION_STARTING = 3;

//窗口类型:TYPE_APPLICATION的变体,可确保窗口管理器在显示应用之前等待绘制此窗口。
public static final int TYPE_DRAWN_APPLICATION = 4;

//应用程序窗口类型的结束值
public static final int LAST_APPLICATION_WINDOW = 99;
子Window(Sub Window)

  子Window的type值范围是[1000,1999],可以看到值就是在应用程序Window之上,这些子Window会按照Z-Order顺序依附于父Window上,而且他们的坐标是相当于父Window的,所以这也就是为什么叫子Window的原因。比如Dialog和PopupWindow,都属于子Window,对应的type值如下:

WindowManager#LayoutParams

//子窗口类型的开始,这些窗口的token必须设置为它们所附加到的窗口。这些类型的窗口按Z-order保留在其附加窗口旁边,并且它们的
//坐标空间相对于其附加窗口。
public static final int FIRST_SUB_WINDOW = 1000;

//窗口类型:应用程序窗口顶部的面板,这些窗口显示在其附加窗口的顶部。
public static final int TYPE_APPLICATION_PANEL = FIRST_SUB_WINDOW;

//窗口类型:用于显示媒体(如视频)的窗口,这些窗口显示在其附加窗口的后面。
public static final int TYPE_APPLICATION_MEDIA = FIRST_SUB_WINDOW + 1;

//窗口类型:应用程序窗口顶部的子面板。这些窗口显示在其附加窗口和任何TYPE_APPLICATION_PANEL面板的顶部
public static final int TYPE_APPLICATION_SUB_PANEL = FIRST_SUB_WINDOW + 2;

//窗口类型:与TYPE_APPLICATION_PANEL类似,但窗口的布局是作为顶级窗口的布局发生的,而不是作为其容器的子窗口。
public static final int TYPE_APPLICATION_ATTACHED_DIALOG = FIRST_SUB_WINDOW + 3;

//窗口类型:用于在媒体窗口顶部显示叠加层的窗口。这些窗口显示在TYPE_APPLICATION_MEDIA和应用程序窗口之间。它们应该是半透
//明的,才有用。这是一个丑陋的大黑客,所以废弃掉了。
@UnsupportedAppUsage
public static final int TYPE_APPLICATION_MEDIA_OVERLAY  = FIRST_SUB_WINDOW + 4;

//窗口类型:应用程序窗口顶部的上方子面板及其子面板窗口。这些窗口显示在其附加窗口和任何TYPE_APPLICATION_SUB_PANEL面板
//的顶部。
public static final int TYPE_APPLICATION_ABOVE_SUB_PANEL = FIRST_SUB_WINDOW + 5;

//子窗口类型的结束值。
public static final int LAST_SUB_WINDOW = 1999;
系统Window(System Window)

  系统Window的范围是[2000,2999],常见的系统的Window有Toast、输入法窗口、系统音量条窗口、系统错误窗口等,对应type的值如下:

WindowManager#LayoutParams

//特定于系统的窗口类型的初始值, 这些通常不是由应用程序创建的。
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;

//窗口类型:电话。这些是非应用程序窗口,提供用户与电话的交互(特别是来电)。这些窗口通常位于所有应用程序的上方,但位于
//状态栏的后面。已弃用,请改用TYPE_APPLICATION_OVERLAY。
@Deprecated
public static final int TYPE_PHONE              = FIRST_SYSTEM_WINDOW+2;

//窗口类型:系统窗口,如低电量警报。这些窗口始终位于应用程序窗口的顶部。已弃用,请改用TYPE_APPLICATION_OVERLAY。
@Deprecated
public static final int TYPE_SYSTEM_ALERT       = FIRST_SYSTEM_WINDOW+3;

//窗口类型:键盘锁窗口
public static final int TYPE_KEYGUARD           = FIRST_SYSTEM_WINDOW+4;

//中间省略...

//系统窗口类型的结束值。
public static final int LAST_SYSTEM_WINDOW      = 2999;

  有点多,就不全贴出来了,而且有很多都被废弃了,大家可以自己去看。这里要注意的是使用系统的Window需要申请权限,即Manifest.permission.SYSTEM_ALERT_WINDOW权限。

flag属性

  通过type属性让我们能够控制Window的显示层级,那如何设置Window的其他显示项和配置项呢,源码中为我们提供了许多Flag值,通过设置不同的Flag值可以得到对应的效果。同样是定义在WindowManager#LayoutParams类中:

//窗口标志:只要此窗口对用户可见,就允许锁屏界面在屏幕打开时激活。这可以单独使用,也可以与
//FLAG_KEEP_SCREEN_ON和/或FLAG_SHOW_WHEN_LOCKED结合使用
public static final int FLAG_ALLOW_LOCK_WHILE_SCREEN_ON     = 0x00000001;

//窗口标志:此窗口后面的所有内容都将变暗。使用 dimAmount 控制 dim 的量。
public static final int FLAG_DIM_BEHIND        = 0x00000002;

//窗口标志:为此窗口启用模糊。
public static final int FLAG_BLUR_BEHIND        = 0x00000004;

//窗口标志:此窗口永远不会获得键输入焦点,因此用户无法向其发送键或其他按钮事件。相反,这些将转到它后面的任何
//可聚焦窗口,并会直接启用FLAG_NOT_TOUCH_MODAL。
public static final int FLAG_NOT_FOCUSABLE      = 0x00000008;

//窗口标志:此窗口永远无法接收触摸事件。此标志的目的是将触摸留给此窗口下方的某个窗口(按Z-order)处理。
public static final int FLAG_NOT_TOUCHABLE      = 0x00000010;

//窗口标志:即使此窗口是可聚焦的(未设置其 {@link FLAG_NOT_FOCUSABLE}),也允许将窗口外部的任何指针事件发送到
//其后面的窗口。否则,它将消耗所有指针事件本身,无论它们是否在窗口内。
public static final int FLAG_NOT_TOUCH_MODAL    = 0x00000020;

@Deprecated
public static final int FLAG_TOUCHABLE_WHEN_WAKING = 0x00000040;

//窗口标志:只要此窗口对用户可见,就要保持设备的屏幕打开并保持明亮。
public static final int FLAG_KEEP_SCREEN_ON     = 0x00000080;

//附加窗口的窗口标志:将窗口放置在整个屏幕中,忽略来自父窗口的任何约束。
public static final int FLAG_LAYOUT_IN_SCREEN   = 0x00000100;

//窗口标志:允许窗口延伸到屏幕之外。
public static final int FLAG_LAYOUT_NO_LIMITS   = 0x00000200;

//窗口标志:显示此窗口时隐藏所有屏幕装饰(如状态栏)。这允许窗口自行使用整个显示空间 -- 当设置了此标志的应用窗口位于顶层时,
//状态栏将被隐藏。但已经被废弃了,
//请改用 {@link WindowInsetsController#hide(int)} 和 {@link Type#statusBars()}。
@Deprecated
public static final int FLAG_FULLSCREEN      = 0x00000400;

//窗口标志:覆盖 {@link FLAG_FULLSCREEN} 并强制显示屏幕装饰(例如状态栏)
//此值“意外”成为 API,不应由第三方应用程序使用。所以也被废弃了。
@Deprecated
public static final int FLAG_FORCE_NOT_FULLSCREEN   = 0x00000800;

//窗口标志:将此窗口合成到屏幕时打开抖动。
@Deprecated
public static final int FLAG_DITHER             = 0x00001000;

//窗口标志:将窗口的内容视为安全内容,防止其出现在屏幕截图中或在非安全显示器上查看。
public static final int FLAG_SECURE             = 0x00002000;

//窗口标志:一种特殊模式,其中布局参数用于在将曲面合成到屏幕时执行曲面缩放。
public static final int FLAG_SCALED             = 0x00004000;

//窗口标志:用于当用户将屏幕靠在脸上时经常使用的窗口,它将积极过滤事件流,以防止在这种情况下出现特定窗口可能不希望的意外
//按下,当检测到此类事件流时,应用程序将收到一个 CANCEL motion 事件来指示这一点,以便应用程序可以通过不对事件执行任何操作
//来相应地处理此问题,直到直到手指松开。
// 其实就是当用户的脸贴近屏幕时(比如打电话),不会去响应此事件
public static final int FLAG_IGNORE_CHEEK_PRESSES    = 0x00008000;

@Deprecated
public static final int FLAG_LAYOUT_INSET_DECOR = 0x00010000;

//窗口标志:设置后,反转窗口的输入法可聚焦性。设置此标志的效果取决于是否设置了 {@link FLAG_NOT_FOCUSABLE}:
//如果未设置 {@link FLAG_NOT_FOCUSABLE},即当窗口可聚焦时,则设置此标志可防止此窗口成为输入法的目标。因此,
//它将无法与输入法交互,并且将分层到输入法之上(除非其上方有另一个输入法目标)。
//如果设置了 {@link FLAG_NOT_FOCUSABLE}  ,则设置此标志会请求将窗口作为输入法目标,即使窗口不可聚焦也是如此。
//因此,它将在输入法下方分层。注意:无论此标志如何,设置 {@link FLAG_NOT_FOCUSABLE} 的 Windows 都无法与输入法交互。
public static final int FLAG_ALT_FOCUSABLE_IM = 0x00020000;

//窗口标志:如果设置了 {@link FLAG_NOT_TOUCH_MODAL},则可以将此标志设置为接收一个特殊的 MotionEvent,其中包含发生在
//窗口外的触摸的操作 {@link MotionEvent#ACTION_OUTSIDE MotionEvent.ACTION_OUTSIDE}。请注意,您不会收到完整的下移手势,
//只会收到第一个下移的位置作为ACTION_OUTSIDE。
public static final int FLAG_WATCH_OUTSIDE_TOUCH = 0x00040000;

//窗口标志:要求在该窗口后面显示系统墙纸。窗口表面必须是半透明的,才能真正看到它后面的墙纸;此标志仅确保如果此窗口实际上
//具有半透明区域,墙纸表面将在那里。
public static final int FLAG_SHOW_WALLPAPER = 0x00100000;

//窗口标志:设置后,窗口将接受超出其边界的触摸事件,以发送到也支持拆分触摸的其他窗口。如果未设置此标志,则向下的第一个指针
//将确定所有后续触摸所到的窗口,直到所有指针都向上。设置此标志后,每个向下的指针(不一定是第一个指针)将确定该指针的所有
//后续触摸将转到的窗口,直到该指针上升,从而使具有多个指针的触摸能够跨多个窗口拆分。
public static final int FLAG_SPLIT_TOUCH = 0x00800000;

//指示是否应对此窗口进行硬件加速。请求硬件加速并不能保证它会发生。
//此标志只能通过编程方式进行控制,以启用硬件加速。要以编程方式为给定窗口启用硬件加速,请执行以下操作:
//Window w = activity.getWindow(); // in Activity's onCreate() for instance
//w.setFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, //WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
//请务必记住,在设置活动或对话框的内容视图(set content view)之前,必须设置此标志。
//也就是说,当我们要启用硬件加速时,需要在代码层面动态设置,且要在视图加载之前(Activity的话就是要在setContentView()方法之前)
public static final int FLAG_HARDWARE_ACCELERATED = 0x01000000;

//处于本地焦点模式的窗口的标志。
public static final int FLAG_LOCAL_FOCUS_MODE = 0x10000000;

//指示此窗口负责绘制系统栏的背景的标志。如果设置,则使用透明背景绘制系统栏,并且此窗口中的相应区域将填充
// {@link Window#getStatusBarColor()} 和 {@link Window#getNavigationBarColor()} 中指定的颜色。
public static final int FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS = 0x80000000;

  flag属性也很多...其中有些废弃的我就省略了。之所以完整的过一遍是因为我觉得,对flag属性熟悉一遍还是挺有必要的,因为在日常开发里还是比较容易碰上的,比如使用Dialog时,会经常用到这些属性。当我们真正知道这些Flag的作用之后,可以让我们更好地去管理View。

SoftInputMode(软键盘)

 表示Window软键盘输入区域的显示模式,我们前面说过,键盘是系统Window,它的层级比较高,也就是说它会盖住下面层级的Window。比如在微信聊天界面,当我们点击输入框弹出键盘的时候,如果不做处理,那么就会把下面层级的Window盖住,这当然不是我们希望的。

 所以我们对软键盘的一些属性设置,其实设置的就是软键盘所对应Window的属性。同样,系统封装了一系列常量值在WindowManager#LayoutParams里:

//{@link softInputMode} 的可见性状态:未指定任何状态。当窗口获得焦点时,系统可能会显示或隐藏软件键盘以获得更好的用户体验。
public static final int SOFT_INPUT_STATE_UNSPECIFIED = 0;

//{@link softInputMode} 的可见性状态:请不要更改软输入区域的状态。
public static final int SOFT_INPUT_STATE_UNCHANGED = 1;

//{@link softInputMode} 的可见性状态:请在正常情况下隐藏任何软输入区域(当用户向前导航到您的窗口时)。
public static final int SOFT_INPUT_STATE_HIDDEN = 2;

//{@link softInputMode} 的可见性状态:当此窗口获得焦点时,请始终隐藏任何软输入区域。
public static final int SOFT_INPUT_STATE_ALWAYS_HIDDEN = 3;

//{@link softInputMode} 的可见性状态:请在正常情况下显示软输入区域(当用户向前导航到您的窗口时)。
public static final int SOFT_INPUT_STATE_VISIBLE = 4;

//{@link softInputMode} 的可见性状态:当此窗口接收到输入焦点时,请始终使软输入区域可见。
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;

//...

 具体大家可以参照源码和注释去看,我们还可以在AndroidManifest(activity标签)文件中设置软键盘的弹出规则

<activity
        android:name=".MainActivity"
        android:windowSoftInputMode="adjustNothing"
        android:exported="true" />

 也就是说我们可以给不同的Activity(所在的Window)来配置不同的软键盘弹出属性。这里埋个坑,那每开一个Activity都会对应加载出一个Window吗?

其他属性
  • x和y:Window的左上角坐标,相对于屏幕的坐标轴系。
  • alpha:Window的透明度。
  • gravity: Window在屏幕中的位置,使用的是Gravity类的常量。
  • format: Window的像素格式,值定义在PixelFormat中。

 到这里我们基本也就把Window的属性都过完了,Window作为View的载体,而View是Window的具体表现形式,这就使得我们通过设置Window的各种属性,可以更方便的处理Activity、Dialog等各种不同View的表现形式,这也让我们对Android View的层级有了更清晰的认识。

原理探究

 虽然到这里我们对Window有个大概的了解,但其实应该疑惑更多,比如:

  1. Window是View的载体,而ViewRootImpl作为View的绘制类,那它和Window是什么关系?
  2. Activity、Dialog和Toast都是通过Window来显示和管理View,他们有什么区别?为什么Dialog必须在Activity中弹出?
  3. MainActivity(首个Activity)是怎么样展示出来的,这中间经历了哪些过程?

 那么,就让我们来通过源码一探究竟。

Window的内部机制

 我们前面说了,Window是一个抽象的概念,添加Window是通过WindowManager来完成的,所以我们就通过WindowManager的使用方法作为切入点,来探究一下Window的内部机制。(PS:我记得郭神说过,探究源码最好的切入点就是从用法入手)

Window的添加过程

 例子前面说过了,就不再赘述了。

val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
windowManager.addView(textView, layoutParams)

 我们知道,WindowManager是一个接口,那么addView()的具体逻辑是谁来实现的呢?我们看下getSystemService()方法:

Activity.java

@Override
    public Object getSystemService(@ServiceName @NonNull String name) {
        if (getBaseContext() == null) {
            throw new IllegalStateException(
                    "System services not available to Activities before onCreate()");
        }

        if (WINDOW_SERVICE.equals(name)) {
            return mWindowManager;
        } else if (SEARCH_SERVICE.equals(name)) {
            ensureSearchManager();
            return mSearchManager;
        }
        return super.getSystemService(name);
    }

 也就是,我们获取到的WindowManager实例就是mWindowManager,看下它是怎么初始化的:


mWindowManager = mWindow.getWindowManager();

  在Activity的attach()方法中,mWindowManager被初始化,我们再跟进去看看:

Window.java

public WindowManager getWindowManager() {
        return mWindowManager;
}

 同样再看下mWindowManager是怎么被初始化的:

Window.java

public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
            boolean hardwareAccelerated) {
        //省略...
        if (wm == null) {
            wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
        }
        mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}

 到这里其实我们也就知道了,其实WindowManager的真正实现类是WindowManagerImpl类.

public class WindowManagerImpl implements WindowManager {
}

 以上逻辑的具体调用时机和流程我们先不管,因为我们暂时想知道的只是WindowManager的真正实现类。所以这时候真正的切入点就出现了,我们看下WindowManagerImpl的addView方法:

@Override
public void addView(View arg0, android.view.ViewGroup.LayoutParams arg1) {
        // pass
}

 大失所望,它是空的呜呜T_T...其实我们通过源码网站WindowManagerImpl.java就可以看到它的实现逻辑:

@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}

也就是说,最终addView是交给mGlobal来做的,而mGlobal是WindowManagerGlobal的一个全局单例:

WindowManagerGlobal.java

@UnsupportedAppUsage
    public static WindowManagerGlobal getInstance() {
        synchronized (WindowManagerGlobal.class) {
            if (sDefaultWindowManager == null) {
                sDefaultWindowManager = new WindowManagerGlobal();
            }
            return sDefaultWindowManager;
        }
    }

 这里的工作模式就是典型的桥接模式,把所有的操作都委托给WindowManagerGlobal来实现,所以WindowManagerGlobal才是WindowManager的真正逻辑实现类,我们看一下addView的实现:

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;
        //如果parentWindow不为空,就说明当前Window是子Window,需要对参数做额外调整
        if (parentWindow != null) {
            parentWindow.adjustLayoutParamsForSubWindow(wparams);
        } else {
            // If there's no parent, then hardware acceleration for this view is
            // set from the application's hardware acceleration setting.
            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参数
            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);

            // 最后执行此操作,因为它会触发消息以开始执行操作
            try {
                //最后通过ViewRootImpl来添加Window
                root.setView(view, wparams, panelParentView, userId);
            } catch (RuntimeException e) {
                //省略...
            }
        }
    }

 代码比较长,删减了一些不相关的。这里提一句,看源码的时候千万不要想去把所有东西都看懂,否则就会陷进去,到最后都不知道自己一开始是干嘛来了,我们只要抓住主线任务就行了。这段代码的主逻辑还是很清晰的,创建出ViewRootImpl实例,然后将当前要处理的窗口视图所对应的信息(View、ViewRootImpl、WindowManager.LayoutParams),通过三个ArrayList保存起来,最后调用
ViewRootImpl的setView()方法,我们来看下它的执行逻辑:

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
            int userId) {
        synchronized (this) {
            if (mView == null) {
                mView = view;

                ...

                //关键点1
                //在将第一个布局添加到WindowManager之前执行一次,以确保我们在从系统接收任何其他事件之前进行重新布局
                requestLayout();

                 //关键点2
                //通过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.getRequestedVisibility(), 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();
                    }
                }

                ...

        }
    }

 这个方法非常长,但我们抓出主线逻辑的话其实主要就是干俩件事,第一件就是调用requestLayout()方法来更新界面。注意这里的Layout和我们自定义View中重写的onLayout()以及layout()不是同一个语义,这里的requestLayout()指的是整个布局流程。我们来看下它的实现:

@Override
public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
}

 熟悉吧,scheduleTraversals,顾名思义,就是准备开始遍历,遍历谁呢,当然就是View树了。相信就算你不清楚它的具体逻辑,你也听过UI的绘制就是从scheduleTraversals开始的。没错,scheduleTraversals()方法就是View绘制的入口,这里就不展开了,简单说下。scheduleTraversals()方法内部会调用到performTraversals()方法,而performTraversals()方法最终会调用performMeasure()方法,来对自身的View树开始执行绘制三部曲。

 我们再回头看,ViewRootImpl的setView()方法做的第二件事就是通过mWindowSession的addToDisplayAsUser()方法来完成Window的添加过程,那这个mWindowSession又是什么呢?

ViewRootImpl.java

@UnsupportedAppUsage
final IWindowSession mWindowSession;

public ViewRootImpl(Context context, Display display) {
        this(context, display, WindowManagerGlobal.getWindowSession(), false);
}

public ViewRootImpl(@UiContext Context context, Display display, IWindowSession session) {
        this(context, display, session, false);
    }

 mWindowSession是一个IWindowSession对象,通过WindowManagerGlobal的getWindowSession()获取。

@UnsupportedAppUsage
public static IWindowSession getWindowSession() {
        synchronized (WindowManagerGlobal.class) {
            if (sWindowSession == null) {
                try {
                    // Emulate the legacy behavior.  The global instance of InputMethodManager
                    // was instantiated here.
                    InputMethodManager.ensureDefaultInstanceForDefaultDisplayIfNecessary();
                    IWindowManager windowManager = getWindowManagerService();
                    sWindowSession = windowManager.openSession(
                            new IWindowSessionCallback.Stub() {
                                @Override
                                public void onAnimatorScaleChanged(float scale) {
                                    ValueAnimator.setDurationScale(scale);
                                }
                            });
                } catch (RemoteException e) {
                    throw e.rethrowFromSystemServer();
                }
            }
            return sWindowSession;
        }
    }

 而IWindowSession是一个IBinder接口(这点我们从命名上就看得出来),所以mWindowSession只是一个Binder对象,而实现类在WindowManagerService中,这里通过mWindowSession完成了IPC通信。

 然后真正添加Window的逻辑就交由WindowManagerService(简称WMS)了,由于WMS比较复杂,这里就不过多深入了。也就是说,系统服务帮我们创建了Window,这也符合系统的统一管控,但这个时候我们并没有发现有对应的Window实例出现,先带着这个问题。

 到这里,我们也就清楚了ViewRootImpl类的具体作用了。我们从WindowManager的addView方法入手,再到找出WindowManager的实现类WindowManagerImpl,再到WindowManagerImpl把操作委托给WindowManagerGlobal类,我们发现WindowManagerGlobal类才是WindowManager真正的逻辑实现类。然后再到WindowManagerGlobal内创建出ViewRootImpl的实例,最后通过ViewRootImpl的setView方法来开始布局逻辑(requestLayout)以及IPC完成Window的添加(创建)。

 所以ViewRootImpl类扮演的角色就是View和WindowManagerService的桥梁,一边执行View的绘制逻辑,一边又通过IPC通信让WMS来创建Window。

 梳理下这几个涉及的类:

  • ViewRootImpl:每次我们在入口处调用addView()方法,到最后都会创建一个ViewRootImpl实例出来,所以这也就说明了,一个View树会对应着一个ViewRootImpl实例。同时它是Window和View之间的桥梁,一边负责View的绘制,一边负责IPC通信到WMS创建Window。

  • IWindowSession:全局(APP范围内)单例,是一个Binder,负责和WMS通信。那它为什么要设计成单例模式呢?这是因为WMS是系统服务,那么我们手机内的所有APP都要和它交互,而一个APP又(可能)有多个Window,所以每个Window都要WMS来管理的话,那就太繁琐了,也会浪费资源。而当每个应用内只有一个单例时,WMS只需要和该IWindowSession实例进行通信即可,由它来代表整个APP和WMS通信。

  • WindowManagerGlobal:WindowManager的真正逻辑实现类,从名字也可以看出来,它也是单例模式。当我们调用WindowManager的addView方法时,最终就是调用它的单例来处理。

 所以它们的关系如下:


Window架构图(图转).jpg
Window的删除过程

 我们再来看下Window的删除过程,和添加操作一样,最终会由WindowManagerGlobal类来处理,具体的话是removeView()方法,我们看下逻辑:

@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
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);
            //通过index来拿到对应的ViewRootImpl实例,然后拿到对应View
            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);
        }
}

private int findViewLocked(View view, boolean required) {
        final int index = mViews.indexOf(view);
        if (required && index < 0) {
            throw new IllegalArgumentException("View=" + view + " not attached to window manager");
        }
        return index;
}

 逻辑很简单,找到对用的View,然后执行删除操作。在前面添加Window里说过,WindowManagerGlobal类中会有几个数组分别来保存当前视图的View、ViewRootImpl等信息,所以我们这时候就能很容易找到需要删除的View(或者说View树)。但你会发现,虽然我们有一个保存View的数组mViews,但我们只是找到对用的下标,而真正View的获取是通过ViewRootImpl的getView()方法来得到的。其实我们前面也说了,ViewRootImpl就是负责View的绘制的,每个ViewRootImpl实例都有自己的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);
            }
        }
}

 可以看到最终还是通过ViewRootImpl来完成删除操作的,先拿到对应的View,然后调用ViewRootImpl的die()方法来处理,最后把View加入mDyingViews数组中,该数组表示待删除的View列表,我们看一下这个die方法:

/**
     * @param immediate True, do now if not in traversal. False, put on queue and do later.
     * @return True, request has been queued. False, request has been completed.
     */
    boolean die(boolean immediate) {
        // Make sure we do execute immediately if we are in the middle of a traversal or the damage
        // done by dispatchDetachedFromWindow will cause havoc on return.
        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;
}

 当我们设置立即删除或者没有在进行View相关绘图操作时,就直接调用doDie()方法;否则就延后处理,通过Handler把消息发出去。看下doDie()方法:

ViewRootImpl.java

void doDie() {
        checkThread();
        if (LOCAL_LOGV) Log.v(mTag, "DIE in " + this + " of " + mSurface);
        synchronized (this) {
            if (mRemoved) {
                return;
            }
            mRemoved = true;
            if (mAdded) {
                //View的删除逻辑
                dispatchDetachedFromWindow();
            }

           ...

            mAdded = false;
        }
        //移除前面说的保存View视图对应信息的数组里当前ViewRootImpl实例所对应的对象
        WindowManagerGlobal.getInstance().doRemoveView(this);
}

 删减了下代码,真正删除View的逻辑在dispatchDetachedFormWindow方法中,然后再调用WindowManagerGlobal的doRemoveView方法来刷新保存的列表。我们先看下dispatchDetachedFormWindow方法:

ViewRootImpl.java

void dispatchDetachedFromWindow() {
        ...
        if (mView != null && mView.mAttachInfo != null) {
            ...
            //标记点1
            mView.dispatchDetachedFromWindow();
        }
 
        //标记点2
        mAccessibilityInteractionConnectionManager.ensureNoConnection();
        mAccessibilityManager.removeAccessibilityStateChangeListener(
                mAccessibilityInteractionConnectionManager);
        mAccessibilityManager.removeHighTextContrastStateChangeListener(
                mHighContrastTextManager);
        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) {
        }
        ...
        //移除同步屏障信号,不再去刷新
        unscheduleTraversals();
}

 主要做了三件事:

标记点1:调用了View的dispatchDetectedFromWindow方法,在该方法内会调用onDeteachedFromWindow方法,该方法我们在做自定义View时,可以在该方法内做一些资源回收的工作,比如终止动画、停止线程等。以及其他一些Detach的监听回调。

标记点2:垃圾回收相关工作,比如清楚数据和消息,移除回调等。

标记点3:通过IWindowSession的remove方法来删除Window,这里也是一个IPC过程,真正删除的地方还是WMS。

 通过删除Window的过程,让我们再次确定了几个类的关系及它们所扮演的角色。还有个Window的更新过程我们就不看了,也比较简单,大家可以自行去看。

 到这里我们就把Window的几个方法都过了一遍,也理清了整个流程,但是这都是我们主动的逻辑调用,也就是说对应的Window都是我们在应用跑起来,Activity加载完再去操作的。那么你肯定会好奇:

  1. Activity是怎样加载的,它的加载流程跟我们上面分析的一样吗?那它的WindowManager实例是什么时候创建的?

  2. 我们前面通过分析知道,其实最后都会调用WindowManagerGlobal类的APP单例来真正去实现对应的逻辑,那WindowManager的实现类WindowManagerImpl有什么用呢,我们为什么不直接使用WindowManagerGlobal类?

  3. 我们都知道,Window有一个唯一的实现类PhoneWindow,它又是在这当中充当了什么角色?

 那就让我们来探究一下Android内置几种视图的Window的创建过程,这非常有利于我们理解Android系统。

Activity的Window创建过程

 我们知道,当我们点击手机的APP图标之后,那么我们的APP就开始进入启动流程了。虚拟机创建,通过一系列系统调度(这里就不展开启动流程相关内容了),我们会进到主线程(也就是UI线程)的入口ActivityThread类的main方法里来,做一些初始化工作之后,再由系统调用performLaunchActivity方法来完成整个Activity启动过程。看下performLaunchActivity方法:

/**
 * Activity启动的核心实现
*/
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        ...

        //activity
        Activity activity = null;
        try {
            java.lang.ClassLoader cl = appContext.getClassLoader();
            //创建activity实例
            activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
            ...
        } catch (Exception e) {
            ...
        }

        try {
            ...

            if (activity != null) {
                ...

                Window window = null;
                ...

                //关键点1
                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);

                ... 

                if (r.isPersistable()) {
                    mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
                } else {
                    //关键点2,回调Activity的onCreate()方法
                    mInstrumentation.callActivityOnCreate(activity, r.state);
                }

                ...

            }
            ...

        } catch (SuperNotCalledException e) {
            throw e;

        } catch (Exception e) {
            ...
        }

        return activity;
}

 先通过类加载器创建Activity的实例对象(这边有个工具类Instrumentation,是专门用来辅助执行Activity相关操作的),然后调用Activity的attach方法为其关联运行过程中所依赖的上下文环境变量,最后再调用Instrumentation类的callActivityOnCreate()方法,来回调Activity的onCreate()方法,相信到onCreate()方法,大家也就熟悉了。我们先看下attach()方法做了什么:

@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
final void attach(...) { //省略参数
        ...

        //创建PhoneWindow实例
        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        mWindow.setWindowControllerCallback(mWindowControllerCallback);
        mWindow.setCallback(this);
        mWindow.setOnWindowDismissedCallback(this);
        mWindow.getLayoutInflater().setPrivateFactory(this);

        ...
        
        //保存当前线程,checkThread的时候会用到
        mUiThread = Thread.currentThread();
        mMainThread = aThread;
        ...

        mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
        if (mParent != null) {
            mWindow.setContainer(mParent.getWindow());
        }
        mWindowManager = mWindow.getWindowManager();

        ...
}

 在这边创建了PhoneWindow实例,而且设置了一些相关回调,同时Activity自己就实现了接口,所以当Window接收到外界的状态改变时,就会回调到Activity里来。我们主要来看下Callback接口,它是定义在Window类里的:

public interface Callback {

        public boolean dispatchKeyEvent(KeyEvent event);

        public boolean dispatchTouchEvent(MotionEvent event);

        boolean onMenuOpened(int featureId, @NonNull Menu menu);

        boolean onMenuItemSelected(int featureId, @NonNull MenuItem item);

        public void onContentChanged();

        public void onWindowFocusChanged(boolean hasFocus);

        public void onAttachedToWindow();

        public void onDetachedFromWindow();

        ...
}

 可以看到,有些我们很熟悉的Activity回调方法,就是在这个时候通过Window设置的监听回调。

 那到这里,Window(PhoneWindow)也有了,那怎么跟Activity的View联系呢?回头看performLaunchActivity()方法,当我们执行完attach()方法之后,就会去执行mInstrumentation的callActivityOnCreate()方法(关键点2),我们看下callActivityOnCreate做了什么:

Instrumentation.java
/**
 * 执行对活动的 Activity.onCreate 方法的调用。
*/
public void callActivityOnCreate(Activity activity, Bundle icicle) {
        prePerformCreate(activity);
        activity.performCreate(icicle);
        postPerformCreate(activity);
}

 我们再跟到Activity的performCreate()方法里:

final void performCreate(Bundle icicle) {
        performCreate(icicle, null);
}

@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
final void performCreate(Bundle icicle, PersistableBundle persistentState) {
        ...
      
        if (persistentState != null) {
            onCreate(icicle, persistentState);
        } else {
            //Activity生命周期第一个回调
            onCreate(icicle);
        }
        ...

        //把事件分发给自己的Fragment,回调Fragment的onActivityCreated()方法
        mFragments.dispatchActivityCreated();

        ...
}

 熟悉吧,onCreate方法,也就是这个时候,Activity开始了生命周期回调,然后我们在onCreate里面加载布局,具体来看下它的逻辑:

/**
 * 在Activity开始时调用。这是大多数初始化应该进行的地方:调用 {@link setContentView(int)} 来扩充 Activity 的界面,使用 {@link 
 * findViewById} 以编程方式与界面中的小组件进行交互。
*/
protected void onCreate(@Nullable Bundle savedInstanceState) {
        ...
       
        //同样,把事件分发给自己的Fragment
        mFragments.dispatchCreate();
        //获取到所有对当前Activity生命周期监听的监听器,把事件分发出去,
        //这也就是为什么ViewModel等组件能有生命周期感知能力的原因
        dispatchActivityCreated(savedInstanceState);
       
        ...
}

 可以看到,逻辑很简单,主要就是事件的分发。那么也就意味着,还有对应的布局逻辑就要在我们重写的时候做,注意看注释,也就是说我们重写时,要去调用setContentView()方法来加载Activity对应的布局。更熟悉了吧,这就是我们从第一次写出第一个界面时,就跟我们见面的方法,来看下在Activity类里的原始实现:

/**
 * 从布局资源设置Activity内容。资源将被扩充,将所有顶级视图添加到Activity中。
*/
public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
}

 先是调用了getWindow()方法来获取Window实例,这里返回的是mWindow,而mWindow就是上面我们提到的在Activity的attach()方法里实例化出来的,也就是PhoneWindow实例,我们跟进去看下:

PhoneWindow.java

@Override
public void setContentView(int layoutResID) {
        if (mContentParent == null) {
            //关键点1
            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 {
            //关键点2
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        ...
        //这个Callback就是上面提到的Window类的Callback,这里回调onContentChanged方法,
        //把内容加载完毕的事件通知出去,回调到Activity
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        ...
}

 其实主逻辑就是当mContentParent为空的时候,调用installDecor()方法(顾名思义,就是来创建DecorView的),然后再通过mLayoutInflater的inflate()方法把我们对应的布局资源加载到mContentParent容器里。很显然,这个mContentParent是一个ViewGroup:

//这是放置窗口(window)内容的视图,它要么是 mDecor 本身,要么是内容所在的 mDecor 的子项。
ViewGroup mContentParent;

 我们先分析关键点1,也就是installDecor()方法:

PhoneWindow.java

private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
           //创建DecorView实例
            mDecor = generateDecor(-1);
            ...
        } else {
            //将DecorView与当前的PhoneWindow绑定
            mDecor.setWindow(this);
        }
        if (mContentParent == null) {
            //获取到content的装载容器
            mContentParent = generateLayout(mDecor);

            ...

            final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(
                    R.id.decor_content_parent);

            if (decorContentParent != null) {
                mDecorContentParent = decorContentParent;
                mDecorContentParent.setWindowCallback(getCallback());
                if (mDecorContentParent.getTitle() == null) {
                    mDecorContentParent.setWindowTitle(mTitle);
                }

                ...

            } else {
                //标题相关内容就是在这里设置的
                mTitleView = findViewById(R.id.title);
                if (mTitleView != null) {
                    if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) {
                        final View titleContainer = findViewById(R.id.title_container);
                        if (titleContainer != null) {
                            titleContainer.setVisibility(View.GONE);
                        } else {
                            mTitleView.setVisibility(View.GONE);
                        }
                        mContentParent.setForeground(null);
                    } else {
                        mTitleView.setText(mTitle);
                    }
                }
            }

            ...
        }
}

 主要逻辑就是创建了DecorView的实例,然后通过generateLayout()方法生成装载容器。我们再进到generateLayout()方法(本来想一笔带过,但发现这个方法挺重要,经过它流程更清晰):

PhoneWindow.java

protected ViewGroup generateLayout(DecorView decor) {
        ...

        //设置系统UI的可见性,状态栏和导航栏的可见性就是在这里设置的
        final int sysUiVis = decor.getSystemUiVisibility();
        final int statusLightFlag = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
        final int statusFlag = a.getBoolean(R.styleable.Window_windowLightStatusBar, false)
                ? statusLightFlag : 0;
        final int navLightFlag = View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
        final int navFlag = a.getBoolean(R.styleable.Window_windowLightNavigationBar, false)
                ? navLightFlag : 0;
        decor.setSystemUiVisibility(
                (sysUiVis & ~(statusLightFlag | navLightFlag)) | (statusFlag | navFlag));

        // Inflate the window decor.
        //注意看这里注释,要来填充DecorView了

        //下面这么一长串的代码就是根据不同的窗口特性来选择DecorView对应的布局
        int layoutResource;
        int features = getLocalFeatures();
        // System.out.println("Features: 0x" + Integer.toHexString(features));
        if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
            if (mIsFloating) {
                TypedValue res = new TypedValue();
                getContext().getTheme().resolveAttribute(
                        R.attr.dialogTitleIconsDecorLayout, res, true);
                layoutResource = res.resourceId;
            } else {
                layoutResource = R.layout.screen_title_icons;
            }
            // XXX Remove this once action bar supports these features.
            removeFeature(FEATURE_ACTION_BAR);
            // System.out.println("Title Icons!");
        } else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
                && (features & (1 << FEATURE_ACTION_BAR)) == 0) {
            // Special case for a window with only a progress bar (and title).
            // XXX Need to have a no-title version of embedded windows.
            layoutResource = R.layout.screen_progress;
            // System.out.println("Progress!");
        } else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
            // Special case for a window with a custom title.
            // If the window is floating, we need a dialog layout
            if (mIsFloating) {
                TypedValue res = new TypedValue();
                getContext().getTheme().resolveAttribute(
                        R.attr.dialogCustomTitleDecorLayout, res, true);
                layoutResource = res.resourceId;
            } else {
                layoutResource = R.layout.screen_custom_title;
            }
            // XXX Remove this once action bar supports these features.
            removeFeature(FEATURE_ACTION_BAR);
        } else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
            // If no other features and not embedded, only need a title.
            // If the window is floating, we need a dialog layout
            if (mIsFloating) {
                TypedValue res = new TypedValue();
                getContext().getTheme().resolveAttribute(
                        R.attr.dialogTitleDecorLayout, res, true);
                layoutResource = res.resourceId;
            } else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {
                layoutResource = a.getResourceId(
                        R.styleable.Window_windowActionBarFullscreenDecorLayout,
                        R.layout.screen_action_bar);
            } else {
                layoutResource = R.layout.screen_title;
            }
            // System.out.println("Title!");
        } else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
            layoutResource = R.layout.screen_simple_overlay_action_mode;
        } else {
            //默认情况下没有别的装饰就会使用screen_simple布局
            layoutResource = R.layout.screen_simple;
        }

        mDecor.startChanging();

        //关键点3
        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

        //关键点4
        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        
        ...

        mDecor.finishChanging();
        return contentParent;
}

 前面的一大串代码就是根据Window的本地特性来选择DecorView的布局文件,默认情况最终会使用screen_simple布局文件:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
    <FrameLayout
         android:id="@android:id/content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:foregroundInsidePadding="false"
         android:foregroundGravity="fill_horizontal|top"
         android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

 可以看到,它是一个LinearLayout布局,包含一个actionbar和一个id为content的FrameLayout。眼熟吧,最终Activity的布局其实就是方法到这个FrameLayout里面。然后再调用DecorView的onResourcesLoaded()方法把这个布局文件加载到DecorView中,我们跟进来看下:

DecorView.java

void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
        ...

        mDecorCaptionView = createDecorCaptionView(inflater);
        //加载出screen_simple布局文件
        final View root = inflater.inflate(layoutResource, null);
        if (mDecorCaptionView != null) {
            ...
        } else {
            // 默认情况会走这边的逻辑,把加载出来的root视图添加到DecorView中
            addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        }
        mContentRoot = (ViewGroup) root;
        ...
}

 DecorView自身其实是继承自FrameLayout,这样一来它就会包含一个LinearLayout,而这个LinearLayout内部有含有标题栏(actionbar)和一个id为content的FrameLayout容器。

 再看下关键点4,获取到id为ID_ANDROID_CONTENT的容器实例,而这个ID_ANDROID_CONTENT指向的就是上面说的id为content的FrameLayout容器:

//XML 布局文件中的主布局应具有的 ID。
ublic static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;

 最后generateLayout()方法会把这个id为content的容器返回,赋值给PhoneWindow的成员变量mContentParent。这也说明了其实generateLayout方法不仅获取返回装载容器,而且它还做了DecorView的填充逻辑。

 那我们再回头看下关键点2,也就是执行完installDecor()方法之后,会调用LayoutInflater的inflate()方法:

LayoutInflater.java

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
        ...
        //通过反射来获取View
        View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
        if (view != null) {
            return view;
        }
        XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
}

 tryInflatePrecompiled()方法是通过反射来获取到预编译的布局类,然后拿到布局的顶层View,添加到mContentParent(也就是上文说的装载容器)中。注意区分这里LayoutInflater类的作用,它只是负责填充(膨胀 inflate)视图,跟View的具体绘制没关系。所以这里是先通过反射来获取View,如果成功就直接返回,如果不成功再通过XmlResourceParser来解析,就不再展开了。也就是说,对于我们布局文件的加载(把布局文件填充到PhoneWindow的装在容器mContentParent中),其实是在这里完成的。另一方面也表明了,对于Activity布局文件的装载,是PhoneWindow在管控。

 到这里你会发现,我们的DecorView已经创建且初始化完毕,而且Activity的文件也成功添加到mContentParent(DecorView的内容装载容器)中,但是并没有看到像前面说的通过WindowManager来添加Window啊?别急,我们接着往下看。

 当Activity的生命周期再往下走时,就会经由ActivityThread的handleResumeActivity()方法来最终进入到onResume的生命周期回调,我们看下handleResumeActivity()方法做了什么:

ActivityThread.java

@Override
public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,
            boolean isForward, String reason) {

        ...

        // The window is now visible if it has been added, we are not
        // simply finishing, and we are not starting another activity.
        if (!r.activity.mFinished && willBeVisible && r.activity.mDecor != null && !r.hideForNow) {
            ...

            r.activity.mVisibleFromServer = true;
            mNumVisibleActivities++;
            //开始对客户端可见
            if (r.activity.mVisibleFromClient) {
                r.activity.makeVisible();
            }
        }

        ...

        //开始执行空闲事务,一些与启动无关的逻辑可以放到这边来做,优化启动速度
        Looper.myQueue().addIdleHandler(new Idler());
}

 此时,Activity开始对用户可见,会调用Activity的makeVisible()方法:

Activity.java

void makeVisible() {
        if (!mWindowAdded) {
            ViewManager wm = getWindowManager();
            wm.addView(mDecor, getWindow().getAttributes());
            mWindowAdded = true;
        }
        mDecor.setVisibility(View.VISIBLE);
}

 怎么样,回到熟悉的感觉了吧!这就是我们一开始说的通过WindowManager来addView的方式,只不过这里加的是Activity的顶层View:DecorView。至此,DecorView真正地通过WMS加到Window里来,再经过一系列的绘制过程(scheduleTraversals),整个View树被绘制到屏幕,状态转为可见,Activity才可以正常收到外界输入信息。同时这里也说明了在Activity的生命周期中,为什么在onResume回调中才可以接受到手指的触摸事件。

 我们再来简单总结一下上面的几个步骤:

  1. 创建DecorView
     DecorView翻译过来就是装饰视图,它其实是一个FrameLayout,是Activity中的顶级View。一般来说它会有一个子view:LinearLayout,包含内部标题栏和内容栏,会随着主题变化而改变。但其中的内容栏是一定存在的,它的固定id是content,是Activity布局最终加载的容器。DecorView的创建过程由installDecor方法来完成,而创建完的DecorView还是一个空白的FrameLayout,然后再通过generateLayout方法(内部调用DecorView的onResourcesLoaded方法)来加载出自己的布局文件。

  2. 将布局文件加载到内容装载容器
     紧接着再通过LayoutInflater的inflate()方法把Activity的布局文件填充加载到DecorView的内容装载容器里,也就是mContentParent中。而这里由于mContentParent的id是content,也就侧面说明了为什么在Activity中设置布局的方法名是setContentView而不是setView。其实从这里也可以看出,对于布局文件的加载(以及标题的设置),都是由PhoneWindow类来管控的,只不过具体视图是通过DecorView来展现。

  3. 回调Activity的onContentChanged方法
     由于Activity实现了Window的Callback接口,这里Activity的布局已经被添加到DecorView的mContentParent中了,于是最后回调Activity的onContentChanged方法,将事件传递出去,我们可以在我们的Activity中处理这个回调。

  4. ActivityThread的handleResumeActivity()方法得到执行
     Activity的生命周期往下走,通过执行handleResumeActivity()方法,最终DecorView通过WindowManager添加到Window中来,而接下去的逻辑就是我们一开始分析的addView逻辑了。至此,Activity的Window创建过程也就完成了。

总结

 到这里,我们也就把Activity的Window创建流程梳理完了,我们简单再回顾下。首先是Activity之所以能展现在我们面前(屏幕上),是因为通过WMS系统服务来替我们创建了Window,但是其实这个Window我们是看不见摸不着的(PhoneWindow其实也只是管理类,更多的作用是为DecorView提供UI模板),它只是一个抽象的概念(但我们可以通过Activity的getWindow方法获取),而具体的表现形式则是DecorView,它是实实在在的,我们能在屏幕上看得见的,最终把Activity的布局加载展示出来。

 可真是一场酣畅淋漓的战斗呀!说实话,阅读源码的过程真的是又枯燥又繁琐,又让人头大,前前后后花了好多天才理完,头都要爆炸了。但是当我们最后把整个逻辑都理清楚,拨开乌云见月明的时候,就会让人异常兴奋,很有成就感,这就是代码的魅力吧!

参考:https://juejin.cn/post/7119004719892135966#heading-16

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 207,113评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,644评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,340评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,449评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,445评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,166评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,442评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,105评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,601评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,066评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,161评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,792评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,351评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,352评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,584评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,618评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,916评论 2 344

推荐阅读更多精彩内容