一、测量阶段(Measure)
1. MeasureSpec 机制详解
java
// MeasureSpec结构(32位int)
int spec = (mode << 30) | size;
-
三种模式:
模式 值 触发场景 特点 UNSPECIFIED
0 ScrollView/RecyclerView子View 父容器不限制子View尺寸 EXACTLY
1 match_parent/固定值(dp) 子View必须使用指定尺寸 AT_MOST
2 wrap_content 子View尺寸不超过指定值 -
生成规则 (核心方法
ViewGroup.getChildMeasureSpec()
):javapublic static int getChildMeasureSpec(int parentSpec, int padding, int childDimension) { int parentMode = MeasureSpec.getMode(parentSpec); int parentSize = MeasureSpec.getSize(parentSpec); int size = Math.max(0, parentSize - padding); int resultSize = 0; int resultMode = 0; switch (parentMode) { case MeasureSpec.EXACTLY: // 父容器有确定尺寸 if (childDimension >= 0) { // 子View固定尺寸 resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { resultSize = size; // 子View填满父容器 resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.WRAP_CONTENT) { resultSize = size; // 子View尺寸不超过父容器 resultMode = MeasureSpec.AT_MOST; } break; case MeasureSpec.AT_MOST: // 父容器尺寸不确定 if (childDimension >= 0) { resultSize = childDimension; // 子View固定尺寸优先 resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { resultSize = size; // 子View尺寸不超过父容器 resultMode = MeasureSpec.AT_MOST; } else if (childDimension == LayoutParams.WRAP_CONTENT) { resultSize = size; // 同上 resultMode = MeasureSpec.AT_MOST; } break; case MeasureSpec.UNSPECIFIED: // 父容器无限制 if (childDimension >= 0) { resultSize = childDimension; // 子View固定尺寸 resultMode = MeasureSpec.EXACTLY; } else { resultSize = 0; // 子View可任意尺寸 resultMode = MeasureSpec.UNSPECIFIED; } break; } return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }
2. 测量流程源码级分析

关键步骤:
-
ViewRootImpl 触发
performTraversals()
-
DecorView 开始测量:
-
调用
measure()
→onMeasure()
-
遍历子View(ViewGroup)并调用其
measure()
-
-
ViewGroup 测量逻辑:
-
通过
measureChildWithMargins()
为子View生成MeasureSpec -
调用子View的
measure()
方法
-
-
子View 响应测量:
-
在
onMeasure()
中计算自身尺寸 -
必须调用
setMeasuredDimension()
保存结果
-
3. 高频问题:为何 onMeasure() 被多次调用?
调用场景 | 触发原因 | 调用次数 | 源码定位 |
---|---|---|---|
常规Activity | Surface创建流程 | 2次 | ViewRootImpl#measureHierarchy() → performMeasure() relayoutWindow()后再次performMeasure() |
Dialog主题 | 宽度自适应尝试 | 最多6次 | measureHierarchy()中三次测量尝试: 1. 预设宽度(baseSize) 2. 折中宽度((baseSize+desiredWidth)/2) 3. 全屏宽度 |
权重布局 | LinearLayout权重计算 | +1次 | LinearLayout#measureVertical()中二次测量带权重子View |
滑动容器 | RecyclerView预加载 | 动态增加 | RecyclerView#onMeasure()中预测量屏幕外item |
常见问题:
Q1:描述MeasureSpec的作用和组成?
A:
MeasureSpec是32位int值,包含:
-
模式(高2位):
-
UNSPECIFIED
:父容器不限制子View尺寸(如ScrollView子View) -
EXACTLY
:子View必须使用精确尺寸(match_parent/固定值) -
AT_MOST
:子View尺寸不超过设定值(wrap_content)
-
-
尺寸(低30位):父容器提供的可用空间
-
核心作用:父容器通过MeasureSpec向子View传递布局约束条件
Q2:自定义View时onMeasure()要注意什么?
A: 必须处理三点:
-
UNSPECIFIED模式:
javaprotected void onMeasure(int widthSpec, int heightSpec) { int width = resolveSize(minWidth, widthSpec); // 处理无限制情况 int height = resolveSize(minHeight, heightSpec); setMeasuredDimension(width, height); }
-
wrap_content支持:
javaif (widthMode == MeasureSpec.AT_MOST) { width = Math.min(desiredWidth, widthSize); // 限制在AT_MOST范围内 }
-
调用setMeasuredDimension():
源码中使用的是**setMeasuredDimension(getDefaultSizexxx),**而对于getDefaultSize,对于AT_MOST以及EXACTLY的情况,返回大小事一样的,如果自定义View,需要重写onMeasure方法,对Wrap_content情况进行处理
Q3:ScrollView子View的测量为何特殊?
A: 源码强制修改高度模式为UNSPECIFIED:
java
// frameworks/base/core/java/android/widget/ScrollView.java
protected void measureChild(View child, int parentWidthSpec, int parentHeightSpec) {
int childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
child.measure(childWidthMeasureSpec, childHeightSpec);
}
结果 :无论子View设置wrap_content
或match_parent
,实际高度均为内容高度(可超过屏幕),通过滚动查看完整内容。
Q4:ViewGroup如何测量子View?
A: 关键三步:
-
计算可用空间:
javaint availableWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
-
生成子View MeasureSpec:
javaint childWidthSpec = getChildMeasureSpec(parentWidthSpec, paddingLeft + paddingRight, lp.width);
-
触发子View测量:
javachild.measure(childWidthSpec, childHeightSpec); // 测量后通过child.getMeasuredWidth()获取结果
Q5:解释ViewRootImpl的测量触发链
A: 核心链路:

-
performTraversals() 是三大流程的总入口
-
measureHierarchy() 处理Dialog等自适应布局的多次测量
-
relayoutWindow() 在测量后申请Surface,触发二次测量
Q6:根视图MeasureSpec如何得到
A:
观察performTraversals()方法可以发现如下代码:
java
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
可以看到,这里调用了getRootMeasureSpec()方法去获取widthMeasureSpec和heightMeasureSpec的值,注意方法中传入的参数,其中lp.width和lp.height在创建ViewGroup实例的时候就被赋值了,它们都等于MATCH_PARENT。然后看下getRootMeasureSpec()方法中的代码,如下所示:
java
private 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;
}
可以看到,这里使用了MeasureSpec.makeMeasureSpec()方法来组装一个MeasureSpec,当rootDimension参数等于MATCH_PARENT的时候,MeasureSpec的specMode就等于EXACTLY,当rootDimension等于WRAP_CONTENT的时候,MeasureSpec的specMode就等于AT_MOST。并且MATCH_PARENT和WRAP_CONTENT时的specSize都是等于windowSize的,也就意味着根视图总是会充满全屏的。
二、布局阶段(Layout)
核心流程

关键知识点
-
LayoutParams的核心作用
-
存储View的布局参数(宽/高、margin等)
-
自定义ViewGroup需重写
generateLayoutParams()
支持margin
-
-
ViewGroup布局流程
javaprotected void onLayout(boolean changed, int l, int t, int r, int b) { for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); // 1. 计算子View位置(根据业务逻辑) int left = calculateChildLeft(); int top = calculateChildTop(); // 2. 调用子View.layout child.layout(left, top, left + width, top + height); } }
-
位置计算要点
-
基于
getMeasuredWidth/Height
(测量阶段结果) -
需处理
padding
(父容器)和margin
(子View)
-
三、绘制阶段(Draw)
核心流程

关键知识点
-
绘制顺序(软件绘制)
javapublic void draw(Canvas canvas) { drawBackground(canvas); // 1. 绘制背景 onDraw(canvas); // 2. 绘制自身内容 dispatchDraw(canvas); // 3. 绘制子View(ViewGroup实现) onDrawForeground(canvas); // 4. 绘制前景/滚动条 }
-
绘制阶段:一是绘制流程执行的顺序,测量和布局阶段都是先执行子 View 再执行 ViewGroup 自身,而绘制是先执行 ViewGroup 绘制流程,再执行子 View 的绘制流程.View 的绘制流程和 ViewGroup 的绘制流程几乎一模一样,唯一的区别是 View 中的 dispatchDraw() 是空实现,因为它没有子视图.
-
硬件加速本质
-
DisplayListCanvas:在UI线程记录绘制指令
-
RenderThread:在渲染线程执行GPU绘图
-
优势:避免重复录制指令(如View未失效时复用DisplayList)
-
-
优化点
-
避免在
onDraw
中创建对象(引发GC) -
使用
canvas.clipRect()
减少过度绘制
-
总结
Q:简述自定义View的三大流程及核心方法?
A:
-
Measure(测量)
-
目标:确定View的宽高
-
关键方法:
onMeasure(int widthMeasureSpec, int heightMeasureSpec)
-
核心机制:父容器通过
MeasureSpec
传递约束条件,子View调用setMeasuredDimension()
保存结果
-
-
Layout(布局)
-
目标:确定View的位置(四个顶点坐标)
-
关键方法:
onLayout(boolean changed, int l, int t, int r, int b)
-
核心机制:父容器遍历子View并调用其
layout()
,子View通过setFrame()
保存坐标
-
-
Draw(绘制)
-
目标:将View绘制到屏幕
-
关键方法:
onDraw(Canvas canvas)
-
执行顺序:背景 → 自身内容 → 子View → 前景
-
硬件加速:通过
DisplayListCanvas
录制指令,由RenderThread异步执行
-