整体流程一览
ViewRootImpl.performTraversals()
│
├─── performMeasure() → View.measure() → onMeasure()
│
├─── performLayout() → View.layout() → onLayout()
│
└─── performDraw() → View.draw() → onDraw()
一、Measure(测量)
核心机制
父 View 把测量约束封装进 MeasureSpec(一个 32 位 int,高 2 位是模式,低 30 位是尺寸)传给子 View,子 View 在 onMeasure() 中根据约束计算自己的期望尺寸,最后调用 setMeasuredDimension() 提交结果。
三种模式:
| 模式 | 含义 | 典型来源 |
|---|---|---|
EXACTLY | 精确值,例如 100dp 或 match_parent | 父有确定尺寸 |
AT_MOST | 不超过某个上限,例如 wrap_content | 父允许子自由伸缩 |
UNSPECIFIED | 没有限制,ScrollView 内的子 View | 父不关心大小 |
wrap_content 失效
这是自定义 View 最高频的 bug。如果自定义 View 继承自 View(而非 TextView 等),不重写 onMeasure,它默认调用父类实现,而父类对 AT_MOST 和 EXACTLY 的处理是一样的——直接使用父传来的 size,效果等同于 match_parent。
正确做法:必须 拦截 AT_MOST,自己计算内容尺寸:
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val measuredWidth = when (widthMode) {
MeasureSpec.EXACTLY -> widthSize // 精确值,直接用
MeasureSpec.AT_MOST -> minOf(contentWidth, widthSize) // 不超过父允许的上限
else -> contentWidth // UNSPECIFIED,完全自由
}
// height 同理...
setMeasuredDimension(measuredWidth, measuredHeight)
}
二、Layout(布局)
核心机制
Measure 阶段只是得到「期望尺寸」,Layout 阶段才是真正确定坐标。父 View 在 onLayout() 中负责给每个子 View 调用 child.layout(left, top, right, bottom),将子 View 的四条边定死。
对于普通 View(叶节点),onLayout() 是空实现;对于 ViewGroup,必须重写它来完成子 View 的摆放逻辑。
ViewGroup 时,onLayout() 的核心职责是根据父容器大小、padding、子 View 的 margin 和测量结果,计算每个子 View 的 left/top/right/bottom,然后调用 child.layout() 完成摆放。常见逻辑包括顺序排布、换行排布、居中或右对齐排布。一般会结合 getMeasuredWidth()、getMeasuredHeight()、MarginLayoutParams 来计算。
错误用 LayoutParams 做动画
// ❌ 错误:每次动画帧都修改 LayoutParams
val lp = view.layoutParams as MarginLayoutParams
lp.leftMargin = currentX
view.layoutParams = lp // 这会触发 requestLayout()!
正确写法:使用属性动画的 translationX/Y,它底层走的是 RenderThread 的 DisplayList 变换,完全绕开 Measure 和 Layout:
// ✅ 正确:translationX 不触发 requestLayout,也不触发 invalidate 引发的重绘
ObjectAnimator.ofFloat(view, "translationX", 0f, 200f).apply {
duration = 300
start()
}
// 或者更简洁:
view.animate().translationX(200f).setDuration(300).start()
结论:如果只是在视觉上移动 View 而不影响其他子 View 的布局,永远优先用 translationX/Y,性能差异可以达到数量级。
三、Draw(绘制)——把像素画到屏幕上
核心机制
View.draw() 按固定顺序执行:
1. drawBackground() // 绘制背景
2. onDraw() // 绘制自身内容(重点重写这里)
3. dispatchDraw() // 绘制子 View(ViewGroup 负责)
4. onDrawForeground() // 绘制前景、滚动条等
核心陷阱:禁止在 onDraw 中创建对象
这是性能优化里最经典的一条铁律,也是面试时最能体现经验深度的点。
onDraw() 是一个高频方法。在硬件加速开启时,每一帧(60fps = 每 16ms)都可能调用它。如果你在里面 new 对象:
// ❌ 灾难性写法:每帧都创建新对象
override fun onDraw(canvas: Canvas) {
val paint = Paint() // 每帧 new 一个 Paint
paint.color = Color.RED
val rect = RectF(0f, 0f, width.toFloat(), height.toFloat()) // 每帧 new 一个 RectF
canvas.drawRoundRect(rect, 20f, 20f, paint)
}
后果:
- 每秒创建和丢弃 60 个对象
- 堆内存快速堆积 → GC 频繁触发
- GC 时主线程暂停(Stop-The-World)→ 掉帧(Jank),用户感知到卡顿
正确做法:把所有在 onDraw 中用到的对象提升为成员变量,在构造方法或 init 块中初始化:
class MyView(context: Context) : View(context) {
// ✅ 在类初始化时创建,整个 View 生命周期复用
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.RED
style = Paint.Style.FILL
}
private val rect = RectF()
override fun onDraw(canvas: Canvas) {
// 复用成员变量,不产生任何垃圾对象
rect.set(0f, 0f, width.toFloat(), height.toFloat())
canvas.drawRoundRect(rect, 20f, 20f, paint)
}
}
invalidate() 的精确控制
invalidate() 默认让整个 View 区域重绘。如果 View 只有局部内容变化(比如一个进度条只更新右侧),可以传入 Rect 参数,让系统只重绘脏区:
// 只重绘右半部分,减少不必要的绘制工作
invalidate(width / 2, 0, width, height)
clipRect 减少过度绘制
在 onDraw 开始时用 canvas.clipRect() 把绘制范围限定在可见区域,可以有效减少 Overdraw(过度绘制)。GPU 调试工具(开发者选项 → 显示过度绘制区域)显示深红色时,这是首选优化手段。
override fun onDraw(canvas: Canvas) {
canvas.save()
canvas.clipRect(visibleRect) // 只绘制可见区域
// ... 复杂绘制逻辑 ...
canvas.restore()
}
四、硬件加速与 DisplayList
现代 Android(API 14+)默认开启硬件加速。理解它能解释很多性能现象:
- View 的绘制结果被记录为 DisplayList(GPU 指令集),而不是直接光栅化。
- 当
invalidate()触发时,只有被标脏的 View 重新录制 DisplayList,其他 View 的 DisplayList 被 RenderThread 直接复用。 translationX/Y、alpha、scaleX/Y等属性在硬件加速下只需更新 GPU 变换矩阵,不需要重新录制 DisplayList,这就是属性动画比重绘快的根本原因。
五、一张图串联全流程
VSYNC 信号
│
▼
ViewRootImpl.performTraversals()
│
├─[1] performMeasure()
│ └─ 自顶向下:父传 MeasureSpec → 子算出 measuredWidth/Height
│ ⚠️ 自定义View 必须处理 AT_MOST(wrap_content)
│
├─[2] performLayout()
│ └─ 自顶向下:父调用 child.layout() 确定四边坐标
│ ⚠️ 位置动画优先用 translationX/Y,避免触发 requestLayout
│
└─[3] performDraw()
└─ 自顶向下:View 录制 DrawOp 到 DisplayList
⚠️ onDraw 中禁止 new 对象,防止 Memory Churn → GC → Jank
六、事件传递机制
Touch 事件的传递是另一条独立于绘制流程的「树上游走」逻辑,但同样遵循自顶向下分发、自底向上消费的原则。
核心机制:三个关键方法
每个 View / ViewGroup 都有这三个方法,职责各不同:
| 方法 | 存在于 | 职责 |
|---|---|---|
dispatchTouchEvent(ev) | View & ViewGroup | 事件的总调度入口,决定事件往哪走 |
onInterceptTouchEvent(ev) | 仅 ViewGroup | 决定是否拦截,返回 true 则自己处理,子 View 收不到 |
onTouchEvent(ev) | View & ViewGroup | 真正消费事件的地方,返回 true 表示已消费 |
伪代码描述整个流程:
// ViewGroup.dispatchTouchEvent 的简化逻辑
fun dispatchTouchEvent(ev: MotionEvent): Boolean {
var handled = false
if (!onInterceptTouchEvent(ev)) {
// 没有拦截,交给子 View
handled = child.dispatchTouchEvent(ev)
}
if (!handled) {
// 子 View 没消费,或者拦截了,自己的 onTouchEvent 兜底
handled = onTouchEvent(ev)
}
return handled
}
ACTION_DOWN 是整个序列的开关
一次完整的触摸序列(DOWN → MOVE → UP)是否能被某个 View 接收,完全由 ACTION_DOWN 决定。
- 如果某个 View 在
onTouchEvent中对ACTION_DOWN返回了false(不消费),系统就会认为它对这次触摸不感兴趣,后续的 MOVE、UP 事件都不会再分发给它。 - 系统会把这次触摸序列的「归属」记录在
ViewGroup的mFirstTouchTarget字段里。DOWN 没被消费,mFirstTouchTarget为 null,后续事件直接走 ViewGroup 自己的onTouchEvent。
// 常见错误:以为每个 ACTION 都会重新走一遍分发流程
override fun onTouchEvent(event: MotionEvent): Boolean {
return when (event.action) {
MotionEvent.ACTION_DOWN -> false // ❌ 拒绝 DOWN,后续 MOVE/UP 全部丢失
MotionEvent.ACTION_MOVE -> doSomething()
else -> super.onTouchEvent(event)
}
}
拦截后的"补刀":ACTION_CANCEL
当父 ViewGroup 在 MOVE 阶段决定拦截(onInterceptTouchEvent 从 false 变为 true),子 View 会收到一个 ACTION_CANCEL 事件,告知它"这次序列我不再给你了"。
实践中必须处理 ACTION_CANCEL,否则子 View 的状态可能卡在 pressed 或选中状态:
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> startTracking()
MotionEvent.ACTION_UP -> commitAction()
MotionEvent.ACTION_CANCEL -> resetState() // ✅ 必须处理,和 UP 一样做清理
}
return true
}
实践陷阱:滑动冲突(最高频的实际问题)
场景:ScrollView 嵌套 RecyclerView(或反过来),两者都想消费垂直方向的滑动。
内部拦截法(推荐,由子 View 主动控制):
子 View(内层)通过 requestDisallowInterceptTouchEvent(true) 告诉父 View"别来拦截我":
// 内层 RecyclerView 的处理逻辑
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
// 按下时立刻请求父 View 不要拦截
parent.requestDisallowInterceptTouchEvent(true)
}
MotionEvent.ACTION_MOVE -> {
if (canScrollVertically(-1) || canScrollVertically(1)) {
// 自己还能滚,继续阻止父 View 拦截
parent.requestDisallowInterceptTouchEvent(true)
} else {
// 滚到边界了,放开拦截权,让父 View 接管
parent.requestDisallowInterceptTouchEvent(false)
}
}
}
return super.onTouchEvent(event)
}
外部拦截法(由父 View 控制):
重写父 ViewGroup 的 onInterceptTouchEvent,根据滑动方向动态决定是否拦截:
// 外层 ScrollView 的处理逻辑
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
val x = ev.x; val y = ev.y
return when (ev.action) {
MotionEvent.ACTION_DOWN -> {
lastX = x; lastY = y
false // DOWN 永远不拦截,否则子 View 连 DOWN 都收不到
}
MotionEvent.ACTION_MOVE -> {
val dx = abs(x - lastX)
val dy = abs(y - lastY)
dy > dx // 垂直滑动才拦截,水平滑动交给子 View
}
else -> false
}
}
事件传递流程总图
Activity.dispatchTouchEvent()
│
▼
PhoneWindow → DecorView.dispatchTouchEvent()
│
▼
ViewGroup.dispatchTouchEvent()
│
├─ onInterceptTouchEvent() 返回 true?
│ ├─ 是 → 自己的 onTouchEvent() ← 此后子 View 收到 ACTION_CANCEL
│ └─ 否 → child.dispatchTouchEvent()
│ │
│ └─ 子 View.onTouchEvent()
│ ├─ 返回 true → 事件消费完毕
│ └─ 返回 false → 冒泡回父 View.onTouchEvent()
│
└─ 所有子 View 都未消费 → 父 ViewGroup.onTouchEvent()
关键结论
- DOWN 决定归属:哪个 View 消费了 DOWN,后续整列事件就归它,不会再走分发树。
- 父 View 默认不拦截:
onInterceptTouchEvent默认返回false,需要主动重写才能拦截。 requestDisallowInterceptTouchEvent是解决滑动冲突的利器,但注意 DOWN 事件会重置这个标志位(父 View 会在每次 DOWN 时调用resetTouchState()),所以必须在每次 DOWN 时重新申请。
总结
Measure:“我会在自定义 View 里重写
onMeasure处理AT_MOST,同时避免在滚动回调或动画中修改LayoutParams,因为requestLayout()会重新触发整棵树的测量。”Layout:“位移动画会用
translationX/Y,它底层走 RenderThread 的 GPU 矩阵变换,完全绕开 Measure 和 Layout,性能远好于修改LayoutParams。”Draw:"
onDraw是高频调用方法,我坚持不在里面new任何对象,所有Paint、Path、RectF都在构造方法里初始化并复用,避免内存抖动触发 GC 造成 Jank。"