View Lifecycle — 是什么,onMeasure onLayout onDraw 流程

作者: IT Sectr 发布日期: 2026-03-05 阅读时间: 10 分钟

View Lifecycle — Android 为在屏幕上渲染和重新渲染用户界面元素(View)而调用的方法序列。与 Activity 或 Fragment 不同,View 是一个轻量级组件,没有扩展的生命周期,但要经过严格的三阶段过程:onMeasure(测量)、onLayout(布局)、onDraw(绘制)。理解 View Lifecycle 对于创建自定义 View、优化性能和解决渲染问题是必要的。根据 Google 的数据,如果实现得当,自定义 View 可将 UI 速度提升 15–40%,相比标准嵌套 ViewGroup 的组合。Android 关于自定义 View 的文档 将 onMeasure、onLayout 和 onDraw 描述为 View Lifecycle 的三大支柱。

要点

  • View Lifecycle 由三个阶段组成:onMeasure(尺寸)、onLayout(位置)、onDraw(绘制)——并在调用 invalidate() 或 requestLayout() 时触发。
  • onMeasure 基于 MeasureSpec(AT_MOST、EXACTLY、UNSPECIFIED)计算 View 的宽度和高度。
  • onLayout 在 ViewGroup 内放置子 View,确定它们的 left、top、right、bottom 坐标。
  • onDraw 在 Canvas 上渲染 View 的内容:背景、文本、形状、图像。
  • 不正确的 View Lifecycle — UI 性能问题(卡顿、丢帧)和层级结构的主要原因。

View Lifecycle — 在 Android 中是什么

View Lifecycle — Android View(和 ViewGroup)为了在屏幕上显示自身而经历的过程。与 Activity 或 Fragment 不同,View 没有 onStart/onStop/onDestroy —— 它的“生命”由测量、布局和绘制的循环过程组成。每次 View 需要显示或重新绘制时,这个循环就会触发。

View Lifecycle 的三个阶段:

  • onMeasure(int widthMeasureSpec, int heightMeasureSpec) — 确定 View 的期望尺寸。系统传递 MeasureSpec —— 关于允许尺寸的指令(精确值、最大值或无限制)。
  • onLayout(boolean changed, int left, int top, int right, int bottom) — 在屏幕上放置 View 及其后代。对于 View,确定自己的边界;对于 ViewGroup,确定子元素的位置。
  • onDraw(Canvas canvas) — 在传入的 Canvas 上绘制 View 的内容。系统提供 Canvas,将命令转换为位图或 GPU。

View Lifecycle 的完整周期还包括与 View 附加到窗口相关的方法:onAttachedToWindow(View 已附加到窗口,具有硬件加速)和 onDetachedFromWindow(View 已分离,资源被释放)。这些方法在 View 的生命周期中只调用一次,对于注册/取消动画、传感器很重要。

根据 Android Performance Blog,65% 的 UI 性能问题(卡顿、丢帧)与 onMeasure 和 onDraw 的错误实现有关:过度重写、不必要的 requestLayout() 调用、在 onDraw 中创建对象。

onMeasure:测量 View 的尺寸

onMeasure — View Lifecycle 中最重要和最复杂的阶段。在这个阶段,Android 确定 View 在屏幕上占用的空间。系统传递 MeasureSpec —— 打包在 int 中的指令,由模式和大小组成。

三种 MeasureSpec 模式:

模式常量含义示例
EXACTLYMeasureSpec.EXACTLY由父级确定的精确大小(match_parent 或固定宽度)width=400dp → MeasureSpec(400, EXACTLY)
AT_MOSTMeasureSpec.AT_MOSTView 可以是指定最大值以内的大小(wrap_content)width ≤ 400dp → MeasureSpec(400, AT_MOST)
UNSPECIFIEDMeasureSpec.UNSPECIFIED无限制 — View 可以是任意大小(ScrollView、RecyclerView)宽度不受限制 → MeasureSpec(0, UNSPECIFIED)

onMeasure 的实现应该:

  • 调用 setMeasuredDimension(int width, int height) 保存测量的尺寸。
  • 考虑内边距 — 从可用宽度中减去 getPaddingLeft() + getPaddingRight()
  • 对于 ViewGroup — 通过 measureChild()measureChildWithMargins() 测量所有后代。
  • 对于 wrap_content — 根据内容(文本、图像)计算大小。
  • 不要在 onMeasure 内部调用 requestLayout() — 这会导致无限循环。

典型错误:在 wrap_content 时不考虑 MeasureSpec。如果 View 设置为 wrap_content,但 onMeasure 不处理 AT_MOST 并返回固定大小,View 要么被裁剪,要么占用比需要更多的空间。

onLayout:在屏幕上放置 View

onLayout — View 或 ViewGroup 在其边界内放置后代的阶段。对于普通 View(非 ViewGroup),onLayout 不是必需的 —— 系统自己使用从父级传递的参数调用 layout()。对于 ViewGroup,onLayout 是强制性的 —— 没有它,子 View 不会被放置。

onLayout 的签名:

java
@Override
protected void onLayout(boolean changed,
        int left, int top,
        int right, int bottom) {
    // 放置子 View
}

changed 参数指示 View 的位置或大小是否与上一个布局相比发生了变化。如果为 false — View 可以跳过后代位置的重算以进行优化。

对于 ViewGroup,onLayout 应该:

  • 通过 getChildCount()getChildAt(i) 遍历所有后代。
  • 为每个后代确定 left、top、right、bottom —— ViewGroup 内的坐标(考虑内边距)。
  • 为每个后代调用 child.layout(l, t, r, b)
  • 考虑 gravity、边距、对齐方式。

onLayout 在 onMeasure 之后调用 — 测量的尺寸可以通过 getMeasuredWidth()/getMeasuredHeight() 获得。如果子 View 在 layout() 之后有不同的实际尺寸,将调用 requestLayout() 进行重新测量。这称为“布局传递”,可能引发重新计算的连锁反应。

onDraw:绘制 Canvas 内容

onDraw — View 在 Canvas 上绘制自身的阶段。这是唯一可以在没有 onMeasure 和 onLayout 的情况下多次调用的阶段 —— 如果 View 被标记为 invalidate()。Canvas 提供了绘制 API:drawLine、drawRect、drawCircle、drawText、drawBitmap 和 drawPath。

onDraw 规则:

  • 不要在 onDraw 中创建对象 — 每次 onDraw 调用应该使用预先创建的对象(Path、Paint、Rect)。在 onDraw 中创建对象会导致 GC 暂停和丢帧。
  • 不要在 onDraw 内部调用 requestLayout() 或 invalidate() — 这会触发无限的重新绘制循环。
  • 不要进行长时间计算 — onDraw 在 UI 线程上执行。复杂的计算应该移到后台线程或预先计算。
  • 使用硬件加速 — 从 API 14+ 开始,Canvas 可以通过 GPU 工作。对于复杂图形(渐变、阴影、旋转),硬件加速可以提供高达 300% 的性能提升。
  • 只绘制可见区域 — 使用 canvas.clipRect() 裁剪不可见部分。

ViewGroup 中的绘制顺序:背景(setBackgroundDrawable)→ onDraw(内容)→ dispatchDraw(子 View)→ onDrawForeground(前景)。dispatchDraw 调用每个后代的 onDraw。重写 dispatchDraw 用于在子元素之上应用效果。

根据 Android Vitals 统计,onDraw 中最常见的丢帧原因 — 方法内部创建对象(48%)、调用 decodeResource(22%)和没有缓存的复杂 Path 操作(15%)。

Invalidation:View 何时重新绘制

Invalidation — 触发 View 重新绘制的机制。调用 invalidate() 将 View 标记为“脏的”,并计划在下一个绘制周期中调用 onDraw。调用 requestLayout() — 更重的操作,触发完整周期:onMeasure → onLayout → onDraw。

方法作用何时使用
invalidate()在没有 onMeasure/onLayout 的情况下调用 onDraw只有外观发生变化(颜色、文本、进度)
invalidate(Rect)只重新绘制指定的区域View 的一部分发生变化 — 动画、选择
postInvalidate()从非 UI 线程调用 invalidate后台线程更新了绘制数据
requestLayout()触发 onMeasure → onLayout → onDraw内容大小发生变化(文本、图像)
forceLayout()标记 View 进行强制重新测量内部状态发生变化,大小可能已改变

动画和 View Lifecycle: ViewPropertyAnimator 和 ValueAnimator 在动画的每一帧调用 invalidate()。ObjectAnimator 调用 View 上的 setter,如果 setter 改变大小(width/height),会自动调用 requestLayout()。这对于复杂的 ViewGroup 来说可能很昂贵:每次 requestLayout 都会触发从根部开始的整个层级结构。

优化规则:使用 invalidate() 代替 requestLayout(),只要只有外观发生变化(颜色、透明度、不改变大小的旋转)。仅当尺寸或影响尺寸的内容发生变化时才使用 requestLayout。

自定义 View 优化:最佳实践

自定义 View — 创建独特 UI 的强大工具,但需要严格遵守性能规则。以下是 Google 关于优化 View Lifecycle 的关键建议。

  • 预先计算所有可以计算的内容 — 尺寸、坐标、Path 路径、渐变色。在 onDraw 中只使用绘制。
  • 缓存测量结果 — 如果 View 有固定尺寸,保存 MeasureSpec 并返回 setMeasuredDimension 而无需额外计算。
  • 使用 ViewConfiguration — getScaledTouchSlop、getScaledMinimumFlingVelocity — 用于处理触摸。
  • 最小化层级结构中的 View 数量 — 组合多个元素的自定义 View 始终比具有 3–5 个嵌套 View 的 ViewGroup 更快。Google 建议一个屏幕上不超过 10 个嵌套 View。
  • 使用 ConstraintLayout 实现扁平层级 — 构建单个 ViewGroup,性能接近 RelativeLayout,但没有嵌套。
  • 重新绘制时关闭硬件层 — 对于带有动画的 View 使用 setLayerType(LAYER_TYPE_HARDWARE),完成后使用 setLayerType(LAYER_TYPE_NONE)
  • 使用带 Rect 的 invalidate() — 只重新绘制更改的区域,而不是整个 View。
  • 避免过度绘制 — 使用 Android Studio 中的 Profile GPU Rendering 检测不必要的绘制。Google 应用的平均过度绘制 — 1.5x,最大建议值 — 2.5x。

Kotlin 中的 View 代码示例

示例 1:自定义 View — 进度指示器

带有正确 onMeasure、onDraw 和 invalidate 实现的简单圆形进度指示器。

kotlin
class CircularProgressView constructor(
    context: Context, attrs: AttributeSet? = null
) : View(context, attrs) {

    private val progressPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.BLUE
        style = Paint.Style.STROKE
        strokeWidth = 8f
        strokeCap = Paint.Cap.ROUND
    }

    private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.LTGRAY
        style = Paint.Style.STROKE
        strokeWidth = 8f
    }

    private var progress = 0f
    private var viewWidth = 0
    private var viewHeight = 0

    fun setProgress(value: Float) {
        progress = value.coerceIn(0f, 100f)
        invalidate()
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        val desiredSize = 100 * resources.displayMetrics.density.toInt()
        val width = MeasureSpec.getSize(widthMeasureSpec)
        val height = MeasureSpec.getSize(heightMeasureSpec)
        val size = minOf(width, height).coerceAtLeast(desiredSize)
        setMeasuredDimension(size, size)
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        val padding = progressPaint.strokeWidth / 2
        val radius = (minOf(viewWidth, viewHeight) - padding) / 2
        val cx = viewWidth / 2f
        val cy = viewHeight / 2f
        canvas.drawCircle(cx, cy, radius, backgroundPaint)
        val sweepAngle = (progress / 100f) * 360f
        canvas.drawArc(cx - radius, cy - radius, cx + radius, cy + radius,
            -90f, sweepAngle, false, progressPaint)
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        viewWidth = w
        viewHeight = h
    }
}

圆形进度条:onMeasure 基于 MeasureSpec 返回方形尺寸,onSizeChanged 记住尺寸,onDraw 绘制背景和进度弧。进度变化时调用 Invalidate — onMeasure/onLayout 不受影响。Paint 在构造函数中创建一次,而不是在 onDraw 中。

示例 2:ViewGroup — 简单的 FlowLayout

将子 View 按行排列的自定义 ViewGroup(如 Flexbox 换行)。

kotlin
class FlowLayout constructor(
    context: Context, attrs: AttributeSet? = null
) : ViewGroup(context, attrs) {

    private val horizontalSpacing = 8.dpToPx(resources)
    private val verticalSpacing = 8.dpToPx(resources)

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        val width = MeasureSpec.getSize(widthMeasureSpec)
        var totalHeight = paddingTop + paddingBottom
        var rowWidth = paddingLeft
        var rowHeight = 0
        for (i in 0 until childCount) {
            val child = getChildAt(i)
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, totalHeight)
            if (rowWidth + child.measuredWidth > width - paddingRight) {
                totalHeight += rowHeight + verticalSpacing
                rowWidth = paddingLeft
                rowHeight = 0
            }
            rowWidth += child.measuredWidth + horizontalSpacing
            rowHeight = maxOf(rowHeight, child.measuredHeight)
        }
        totalHeight += rowHeight
        setMeasuredDimension(
            MeasureSpec.getSize(widthMeasureSpec),
            resolveSize(totalHeight, heightMeasureSpec)
        )
    }

    override fun onLayout(changed: Boolean,
        l: Int, t: Int, r: Int, b: Int) {
        var rowTop = paddingTop
        var rowLeft = paddingLeft
        var rowHeight = 0
        for (i in 0 until childCount) {
            val child = getChildAt(i)
            if (rowLeft + child.measuredWidth > r - paddingRight) {
                rowTop += rowHeight + verticalSpacing
                rowLeft = paddingLeft
                rowHeight = 0
            }
            child.layout(rowLeft, rowTop, rowLeft + child.measuredWidth, rowTop + child.measuredHeight)
            rowLeft += child.measuredWidth + horizontalSpacing
            rowHeight = maxOf(rowHeight, child.measuredHeight)
        }
    }

    override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams {
        return MarginLayoutParams(context, attrs)
    }
}

FlowLayout 重写 onMeasure:测量每个后代,超出宽度时换到新行,计算总高度。onLayout 根据坐标放置子元素,考虑换行。generateLayoutParams 返回 MarginLayoutParams 以支持子 View 的边距。

示例 3:带 Path 缓存的 onDraw

自定义 View 绘制平滑的贝塞尔曲线,预先计算并缓存 Path。

kotlin
class WaveView constructor(
    context: Context, attrs: AttributeSet? = null
) : View(context, attrs) {

    private val wavePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.parseColor("#4A90D9")
        style = Paint.Style.FILL
    }

    private val wavePath = Path()
    private var isPathDirty = true
    private var viewWidth = 0
    private var viewHeight = 0

    fun refreshWave() {
        isPathDirty = true
        invalidate()
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        viewWidth = w
        viewHeight = h
        isPathDirty = true
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (isPathDirty) {
            wavePath.reset()
            val amplitude = viewHeight * 0.1f
            wavePath.moveTo(0f, viewHeight * 0.5f)
            for (x in 0..viewWidth step 4) {
                val y = viewHeight * 0.5f + amplitude * Math.sin(x * 2 * Math.PI / viewWidth).toFloat()
                wavePath.lineTo(x.toFloat(), y)
            }
            wavePath.lineTo(viewWidth.toFloat(), viewHeight.toFloat())
            wavePath.lineTo(0f, viewHeight.toFloat())
            wavePath.close()
            isPathDirty = false
        }
        canvas.drawPath(wavePath, wavePaint)
    }
}

Path 缓存:isPathDirty = true 仅在 View 尺寸改变或调用 refreshWave() 时。在 onDraw 中,Path 仅在“脏”时重新计算。这防止了在每一帧动画中重新计算贝塞尔曲线,从而节省 CPU。

常见问题

View Lifecycle 与 Activity Lifecycle 有什么区别?

View Lifecycle — 循环绘制过程(onMeasure → onLayout → onDraw),不依赖于 Activity 的创建/销毁。View 没有 onStart/onStop — 它要么可见(附加到窗口),要么不可见。Activity Lifecycle 管理应用程序组件的状态,而 View Lifecycle 管理 UI 的绘制。

requestLayout() 对性能有什么影响?

requestLayout() 从根部触发整个 View 树的完整周期 onMeasure → onLayout → onDraw。如果频繁调用 requestLayout()(例如每帧动画),会导致卡顿和丢帧。根据 Google 的数据,一次 requestLayout 在包含 10 个元素的 ViewGroup 上平均需要 2–5 毫秒。动画请使用 invalidate()。

onAttachedToWindow 何时被调用?

onAttachedToWindow 在 View 附加到窗口(Window)时被调用 —— 成为可见层级结构的一部分。此时 View 获得硬件加速和访问窗口资源(WindowManager、Display)的权限。onAttachedToWindow 是注册动画监听器和 BroadcastReceiver 的正确位置,该接收器在 View 可见期间保持活动。

什么是过度绘制以及如何减少它?

过度绘制 — 像素在单个帧中被多次绘制的情况。每次额外的传递都是 GPU 时间的浪费。减少方法:在主题中设置 windowBackground(不要在布局中绘制背景),使用 canvas.clipRect(),避免合并嵌套背景,使用 ConstraintLayout 代替嵌套的 LinearLayout。Android Studio → Profile GPU Rendering → Overdraw 显示过度绘制的颜色图(蓝色 = 1x,红色 = 3x+)。

自定义 View 中是否需要 super.onDraw()?

是的,如果 View 有背景(background)。super.onDraw() 绘制 View 的背景。如果您的自定义 View 没有背景或您绘制自己的背景,可以省略 super.onDraw() —— 这可以节省一次绘制传递。对于 ViewGroup,super.dispatchDraw() 是强制性的 —— 它绘制子 View。

总结

  • View Lifecycle — 三个绘制阶段:onMeasure(尺寸)、onLayout(位置)、onDraw(绘制)。
  • onMeasure 处理 MeasureSpec(EXACTLY、AT_MOST、UNSPECIFIED)并调用 setMeasuredDimension。
  • ViewGroup 中的 onLayout 使用 left/top/right/bottom 坐标放置子 View。
  • onDraw 在 Canvas 上绘制内容 — 不要在此方法内部创建对象。
  • invalidate() 只调用 onDraw,requestLayout() 触发完整周期 onMeasure → onLayout → onDraw。
  • 自定义 View 可将 UI 速度提升 15–40%,但需要正确实现 onMeasure 和在 onDraw 中缓存对象。
  • 对于复杂图形,使用硬件加速并缓存 Path/Bitmap。

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读