整体流程一览

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精确值,例如 100dpmatch_parent父有确定尺寸
AT_MOST不超过某个上限,例如 wrap_content父允许子自由伸缩
UNSPECIFIED没有限制,ScrollView 内的子 View父不关心大小

wrap_content 失效

这是自定义 View 最高频的 bug。如果自定义 View 继承自 View(而非 TextView 等),不重写 onMeasure,它默认调用父类实现,而父类对 AT_MOSTEXACTLY 的处理是一样的——直接使用父传来的 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,它底层走的是 RenderThreadDisplayList 变换,完全绕开 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/YalphascaleX/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 事件都不会再分发给它
  • 系统会把这次触摸序列的「归属」记录在 ViewGroupmFirstTouchTarget 字段里。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()

关键结论

  1. DOWN 决定归属:哪个 View 消费了 DOWN,后续整列事件就归它,不会再走分发树。
  2. 父 View 默认不拦截onInterceptTouchEvent 默认返回 false,需要主动重写才能拦截。
  3. requestDisallowInterceptTouchEvent 是解决滑动冲突的利器,但注意 DOWN 事件会重置这个标志位(父 View 会在每次 DOWN 时调用 resetTouchState()),所以必须在每次 DOWN 时重新申请。

总结

  1. Measure:“我会在自定义 View 里重写 onMeasure 处理 AT_MOST,同时避免在滚动回调或动画中修改 LayoutParams,因为 requestLayout() 会重新触发整棵树的测量。”

  2. Layout:“位移动画会用 translationX/Y,它底层走 RenderThread 的 GPU 矩阵变换,完全绕开 Measure 和 Layout,性能远好于修改 LayoutParams。”

  3. Draw:"onDraw 是高频调用方法,我坚持不在里面 new 任何对象,所有 PaintPathRectF 都在构造方法里初始化并复用,避免内存抖动触发 GC 造成 Jank。"