Android View三剑客原理大白话解析
用装修房子类比理解:
测量(Measure)→ 量房间尺寸
布局(Layout)→ 摆家具位置
绘制(Draw)→ 刷墙贴装饰
一、测量原理(卷尺测量阶段)
1. 父View的约束条件
// 测量规格 = 模式 + 尺寸
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(
parentWidth,
MeasureSpec.EXACTLY // 模式:精确值/最大值/未限制
);
三种模式解释:
EXACTLY:爹说了算(如match_parent或具体数值)AT_MOST:最多这么大(如wrap_content)UNSPECIFIED:随便你(ScrollView等可滚动布局)
2. 自定义View测量要点
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// 步骤1:计算自己想要的尺寸
val desiredWidth = calculateWidth()
val desiredHeight = calculateHeight()
// 步骤2:遵守父View的限制
val finalWidth = resolveSize(desiredWidth, widthMeasureSpec)
val finalHeight = resolveSize(desiredHeight, heightMeasureSpec)
// 步骤3:保存测量结果
setMeasuredDimension(finalWidth, finalHeight)
}
避坑指南:
- 忘记调用
setMeasuredDimension会抛异常 wrap_content不处理会填满父容器
二、布局原理(家具摆放阶段)
1. 坐标确定流程
父View调用子View.layout(l, t, r, b)
↓
子View的mLeft/mTop/mRight/mBottom被赋值
↓
触发onLayout()(如果是ViewGroup需遍历布局子View)
2. 常用布局示例
LinearLayout垂直排列源码逻辑:
void layoutVertical(int left, int top, int right, int bottom) {
int childTop = mPaddingTop;
for (View child : getChildren()) {
int childHeight = child.getMeasuredHeight();
// 设置子View位置
child.layout(left, childTop,
right, childTop + childHeight);
childTop += childHeight + dividerHeight;
}
}
三、绘制原理(装修粉刷阶段)
1. 绘制顺序层级图
1. 绘制背景 → drawBackground(canvas)
2. 绘制自己 → onDraw(canvas)
3. 绘制子View → dispatchDraw(canvas)
4. 绘制装饰(滚动条等) → onDrawForeground(canvas)
2. 优化绘制性能技巧
// 正确做法:避免在onDraw中创建对象
val paint = Paint().apply {
color = Color.RED
isAntiAlias = true
}
override fun onDraw(canvas: Canvas) {
// ✅ 复用预定义对象
canvas.drawCircle(x, y, radius, paint)
// ❌ 禁止在绘制时new对象!
// val tempPaint = Paint()
}
四、全流程协作机制
1. 从根View开始的遍历
ViewRootImpl.performTraversals()
↓ 触发
measure() → onMeasure()
↓
layout() → onLayout()
↓
draw() → onDraw()
2. 触发更新的两种方式
| 方法 | 作用范围 | 性能消耗 |
|---|---|---|
invalidate() | 只重绘当前区域 | 较低 |
requestLayout() | 重新测量+布局+绘制 | 较高 |
五、高频面试题破解
Q1:为什么自定义View wrap_content失效?
原因分析:
// 错误实现:直接使用MeasureSpec的尺寸
setMeasuredDimension(
MeasureSpec.getSize(widthMeasureSpec),
MeasureSpec.getSize(heightMeasureSpec)
)
正确方案:
// 当模式是AT_MOST时使用计算值
val width = when(MeasureSpec.getMode(widthMeasureSpec)) {
MeasureSpec.AT_MOST -> min(desiredWidth, MeasureSpec.getSize(...))
else -> MeasureSpec.getSize(...)
}
Q2:View的绘制流程会多次执行吗?
绘制触发条件:
- 新View添加到视图树
- View调用
invalidate() - 动画执行期间
- 屏幕区域失效(如被遮挡后重新显示)
六、性能优化黄金法则
- 减少层级嵌套 → 用ConstraintLayout替代多层LinearLayout
- 避免过度绘制 → 开启开发者选项中的"显示过度绘制区域"
- 善用include/merge → 复用布局文件
- ViewStub延迟加载 → 耗时布局按需加载
View三流程终极口诀:
测量就像量尺寸,父给限制子遵守
布局定位摆位置,上下左右算清楚
绘制如同刷油漆,先底后面再装饰
requestLayout全量走,invalidate只重绘
层级优化是王道,性能体验两手抓!