我们用一个 "盖房子" 的故事来讲透这个问题,全程结合 Android 源码的核心逻辑,保证大家能看懂。
故事背景:小区盖房记
把 Android 系统比作一个 "小区",每个 App 都是小区里的一栋 "楼":
- Window(窗口) :就是楼的 "地皮",规定了这栋楼能占多大地方(比如不能超出小区边界)。
- View(视图) :就是楼里的 "房间",我们看到的按钮、文字都是不同的房间。
- ViewRootImpl:相当于 "施工队长",负责按规矩把房间盖在合适的位置。
- WindowManager:相当于 "小区物业",负责审批地皮使用、监督施工。
- LayoutParams:相当于 "施工图纸",规定了房间的大小、位置等信息。
一、两种盖房方式:精装套餐 vs 自建房
小区里有两种盖房方式,对应我们的问题场景:
1. Activity 的 DecorView:开发商的 "精装套餐"
你买了一套开发商(系统)的精装房(Activity),不需要自己设计,开发商会给你一个默认的 "整体户型"(DecorView)。
流程细节:
-
你签购房合同(调用
setContentView)时,开发商会默默做两件事:- 给你配一个 "整体户型图"(DecorView),这个户型图的默认图纸(LayoutParams)是 "占满整个地皮"(
MATCH_PARENT)。 - 找物业(WindowManager)申请地皮(Window),然后派施工队长(ViewRootImpl)来施工。
- 给你配一个 "整体户型图"(DecorView),这个户型图的默认图纸(LayoutParams)是 "占满整个地皮"(
-
施工队长(ViewRootImpl)拿到图纸后,会先问物业:"这地皮实际能盖多大?"(计算窗口可用空间)。
物业会说:"地皮总面积 100 平,但小区规定要留 20 平做公共区域(状态栏、导航栏),实际能用 80 平。"(对应源码中ViewRootImpl计算mWidth、mHeight时减去 Insets 的逻辑)。 -
施工队长再看图纸:"户型要占满可用空间"(
LayoutParams.width = MATCH_PARENT),于是算出房间的 "尺寸要求"(MeasureSpec):"宽度必须是 80 平,不能多不能少"(EXACTLY + 80)。
(源码对应:ViewRootImpl.performTraversals()中通过getRootMeasureSpec(mWidth, lp.width)生成 MeasureSpec) -
最后按这个要求盖房(调用
DecorView.measure()),房间就正好占满 80 平。
2. WindowManager 直接加 MyView:自己盖 "自建房"
你不想买精装房,想在小区里自己盖个小房子(MyView),流程如下:
流程细节:
- 你直接找物业(WindowManager)申请地皮,同时提交自己画的图纸(
WindowManager.LayoutParams),比如图纸上写着:"宽度 30 平,高度随便(能放下就行)"(width=300dp,height=WRAP_CONTENT)。 - 物业审批通过后,同样派施工队长(ViewRootImpl)来施工。
- 施工队长还是先问物业:"地皮可用空间多少?" 得到同样的答案:"80 平"。
- 再看你的图纸:"宽度 30 平" → 尺寸要求是 "必须 30 平"(
EXACTLY + 30);"高度随便" → 尺寸要求是 "最多 80 平,能小就小"(AT_MOST + 80)。
(源码逻辑和 DecorView 一样,都是getRootMeasureSpec()方法,只是传入的lp.width、lp.height不同) - 最后按这个要求盖房(
MyView.measure()),宽度固定 30 平,高度根据内容自动调整(但不会超过 80 平)。
二、核心差异:图纸(LayoutParams)不同,结果可能不同
两种方式的施工流程完全一样(都是施工队长主导,先问地皮大小,再按图纸算尺寸要求),但因为 "图纸" 不同,可能导致最终房间大小不同:
- 精装房的图纸(DecorView 的 LayoutParams)是系统默认的 "占满可用空间",所以房间会铺满地皮剩下的空间。
- 自建房的图纸(MyView 的 LayoutParams)是你自己定的,可能是固定大小、自适应大小等,所以房间大小由你决定(但不能超过地皮限制)。
三、没有 "上级房间"(父容器),参数从哪来?
不管是精装房的整体户型(DecorView)还是自建房(MyView),它们都是 "最高级别的房间"(顶级 View),没有上级房间(父容器)。那施工队长怎么知道该盖多大?
答案是:两个关键信息源(源码层面的参数来源):
-
图纸(WindowManager.LayoutParams)
无论是系统给 DecorView 默认生成的,还是你给 MyView 手动设置的,这个图纸里写了:- 房间要多大(
width、height:是铺满、自适应还是固定值); - 房间的类型(
type:是普通房间还是特殊房间,比如悬浮窗)。
(源码中,ViewRootImpl会持有这个LayoutParams,在performTraversals()中作为计算依据)
- 房间要多大(
-
地皮实际可用空间(窗口尺寸 - 公共区域)
施工队长会从物业(WMS,WindowManagerService)拿到地皮的总大小,再减去小区规定的公共区域(状态栏、导航栏等,通过WindowInsets传递),得到实际能盖房的空间。
(源码中,ViewRootImpl通过mWidth、mHeight存储这个可用空间,这两个值在窗口创建或旋转时由 WMS 更新)
四、源码里的 "施工队长工作手册"
施工队长(ViewRootImpl)的核心工作就是在performTraversals()方法里计算尺寸要求,关键代码如下(简化后):
// ViewRootImpl.java
private void performTraversals() {
// 1. 从物业拿到地皮可用空间(mWidth、mHeight)
// (实际是通过WMS计算,减去Insets后的尺寸)
// 2. 拿到图纸(LayoutParams)
WindowManager.LayoutParams lp = mWindowAttributes;
// 3. 计算顶级View的尺寸要求(MeasureSpec)
int widthSpec = getRootMeasureSpec(mWidth, lp.width);
int heightSpec = getRootMeasureSpec(mHeight, lp.height);
// 4. 按要求盖房(触发measure)
mView.measure(widthSpec, heightSpec);
}
// 计算尺寸要求的核心方法
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// 图纸要求"铺满" → 必须等于地皮可用空间
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// 图纸要求"自适应" → 最大不能超过地皮可用空间
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// 图纸要求"固定大小" → 必须等于指定值
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
不管是 DecorView 还是 MyView,最终都会走到这段代码。区别只在于lp.width、lp.height的值不同(一个是系统默认,一个是开发者设置)。
总结:
顶级 View(DecorView 或 MyView)的测量,就像施工队长按 "图纸"(LayoutParams)和 "地皮可用空间"(窗口尺寸 - 公共区域)算出来的盖房要求,流程完全一样,只是图纸内容不同可能导致最终大小不同。没有父容器没关系,因为施工队长(ViewRootImpl)会直接从物业(系统)拿到所有必要的参数。