我们来详细分析 Android 中 Activity 窗口显示过程的 “阶段二:添加 Window,预测量 Window 尺寸”,这个阶段是窗口从应用端走向屏幕显示的关键一步,涉及到系统级的窗口管理和尺寸计算。
一、阶段二的整体定位:承上启下的关键环节
在阶段一(DecorView 初始化)之后,窗口已经有了 UI 骨架(DecorView),但还需要告诉系统 “我要显示了”。阶段二主要做两件事:
-
告诉窗口管理器(WMS)我要显示:通过跨进程通信,在系统层面注册这个窗口,创建对应的管理对象。
-
提前算好窗口的 “摆放位置和大小” :考虑屏幕边缘、刘海、系统栏等限制,算出窗口能占多大地方,为后续的布局和绘制做准备。
这两件事分别对应 ViewRootImpl.setView 方法中的 “关注点 1” 和 “关注点 2”,我们分开来看。
二、关注点 1:添加窗口 —— 让系统知道 “我存在”
1. 从应用端发起请求(客户端逻辑)
当 Activity 调用 WindowManager.addView(最终走到 ViewRootImpl.setView)时,会通过一个名为mWindowSession的 Binder 代理(跨进程通信工具),向系统服务端(WMS)发送 “添加窗口” 的请求:
java
res = mWindowSession.addToDisplayAsUser(..., // 远程调用WMS
这里的核心是告诉 WMS:“我是某个应用的窗口,参数都在这里,帮我注册一下!”
2. 服务端(WMS)的处理:创建窗口管理者
WMS 收到请求后,会做两件关键的事:
-
创建 WindowState 对象:这是系统内部管理窗口的核心对象,记录了窗口的所有信息(位置、大小、类型、权限等),就像给窗口发了一张 “身份证”。
-
挂载到窗口容器树:将 WindowState 添加到 WMS 的全局窗口列表(mWindowMap)和对应的显示容器(DisplayContent)中,这样系统后续就能管理、调度这个窗口了。
举个例子:比如你打开一个 App,WMS 就像一个 “窗口管家”,每个 Activity 窗口来注册时,管家会创建一个档案(WindowState),记录它的所有信息,并把档案放到对应的抽屉(显示容器)里,方便后续管理。
3. 权限与合法性检查
在创建 WindowState 之前,WMS 会做一系列检查:
- 权限检查:比如普通应用不能随便创建系统级窗口(需要系统权限)。
- 重复检查:同一个窗口不能重复注册,避免混乱。
- 显示状态检查:比如窗口对应的 Display(屏幕)是否存在,避免挂载到不存在的屏幕上。
三、关注点 2:预计算窗口尺寸 —— 算出 “窗口能占多大地方”
1. 为什么需要预计算?
想象你要在客厅放一个沙发,需要先知道客厅的大小,还要避开茶几、电视等障碍物。窗口预计算就是干这件事:在真正绘制窗口前,先算出它在屏幕上的 “可用区域”,考虑各种限制因素(系统栏、刘海、多窗口模式等),得到一个基准尺寸,方便后续的布局和绘制。
2. 计算过程:考虑各种 “屏幕障碍物”
预计算由 WindowLayout.computeFrames 方法完成,核心逻辑是根据窗口参数(LayoutParams)和屏幕环境,算出窗口的位置和大小,分三步:
(1)确定屏幕的 “干净区域”:排除系统元素的影响
- Insets(内边距) :比如屏幕底部的导航栏、顶部的状态栏,会占用一部分区域,窗口不能覆盖这些地方。计算时会从屏幕总尺寸中减去这些 Insets 的大小。
- 刘海屏处理:如果屏幕有刘海(前置摄像头区域),窗口默认不会显示在刘海区域(除非特殊配置),计算时会避开这块 “凸起”。
(2)处理窗口模式和布局参数
- 全屏 vs 非全屏:如果窗口是全屏(FLAG_LAYOUT_IN_SCREEN),则尽量占满整个干净区域;如果是浮动窗口或子窗口,则根据父窗口的大小调整。
- 多窗口模式:当多个应用并排显示时(比如分屏),窗口大小会被限制在父窗口(分屏区域)内,不能超出。
- 重力布局(Gravity) :比如窗口设置 “居中”,计算时会根据重力参数调整位置,确保最终显示在正确的位置。
(3)输出结果:ClientWindowFrames
计算结果保存在 ClientWindowFrames 中,包含三个关键区域:
- frame:窗口最终的位置和大小(比如左上角坐标、宽高)。
- displayFrame:屏幕中窗口可以显示的最大区域(排除 Insets、刘海等)。
- parentFrame:如果窗口有父容器(比如子窗口),这是父容器的可用区域,用于计算子窗口的布局。
3. 一个具体例子:普通 Activity 的全屏显示
假设手机屏幕是 1080x2340,底部导航栏高 100,顶部状态栏高 50,没有刘海:
- 原始屏幕尺寸:1080x2340
- 减去 Insets 后:displayFrame 变成 1080x (2340-100-50)=1080x2190
- 如果 Activity 是全屏模式:frame 会尽量填满 displayFrame(宽 1080,高 2190),位置从 (0,0) 开始。
- 如果 Activity 是居中显示的小窗口:frame 会根据 Gravity 参数(比如居中)计算位置,比如 (540 - 宽度 / 2, 1095 - 高度 / 2)。
四、阶段二的核心价值:为后续流程铺路
阶段二完成后,系统已经知道 “有一个窗口要显示”,并且知道 “它应该放在哪里、多大”。接下来的阶段三(预测量 View 树、创建 Surface)和阶段四(测量、布局、绘制),都会基于阶段二的结果进行:
- 预测量 View 树:需要知道窗口的基准尺寸,才能计算 DecorView 内部各个控件的位置和大小。
- 创建 SurfaceControl:后续绘制需要的画布(Surface)的大小,就是阶段二算出的 frame 尺寸。
- 通知 WMS 最终显示:阶段五的 “显示窗口” 操作,依赖 WMS 中已经注册的 WindowState 和正确的尺寸信息。
五、为什么预测量用 “UNSPECIFIED_LENGTH”?
文档中提到预测量时传入的宽高是 UNSPECIFIED_LENGTH(不限制尺寸),这是因为:
- 默认以屏幕为基准:大多数情况下,Activity 窗口是全屏或接近全屏的,直接用屏幕可用区域作为基准最合理。
- 后续可调整:如果应用通过 LayoutParams 设置了固定宽高(比如 MATCH_PARENT、具体数值),后续流程会根据预计算的基准尺寸进一步调整,确保兼容性。
总结:阶段二做了什么?
-
注册窗口:告诉系统 “我要显示”,创建 WindowState 并加入 WMS 的管理列表。
-
算位置大小:考虑屏幕障碍(Insets、刘海、多窗口),算出窗口的可用区域,为后续布局和绘制提供基准。
这一步就像盖房子前的 “土地审批” 和 “地基测量”:先获得官方许可(注册到 WMS),再测量地基大小(算出窗口尺寸),接下来就能放心地盖房子(绘制 UI)了。理解阶段二,就能明白 Android 如何在复杂的屏幕环境中,高效地管理每个窗口的显示位置和大小。