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 — Android View(和 ViewGroup)为了在屏幕上显示自身而经历的过程。与 Activity 或 Fragment 不同,View 没有 onStart/onStop/onDestroy —— 它的“生命”由测量、布局和绘制的循环过程组成。每次 View 需要显示或重新绘制时,这个循环就会触发。
View Lifecycle 的三个阶段:
View Lifecycle 的完整周期还包括与 View 附加到窗口相关的方法:onAttachedToWindow(View 已附加到窗口,具有硬件加速)和 onDetachedFromWindow(View 已分离,资源被释放)。这些方法在 View 的生命周期中只调用一次,对于注册/取消动画、传感器很重要。
根据 Android Performance Blog,65% 的 UI 性能问题(卡顿、丢帧)与 onMeasure 和 onDraw 的错误实现有关:过度重写、不必要的 requestLayout() 调用、在 onDraw 中创建对象。
onMeasure — View Lifecycle 中最重要和最复杂的阶段。在这个阶段,Android 确定 View 在屏幕上占用的空间。系统传递 MeasureSpec —— 打包在 int 中的指令,由模式和大小组成。
三种 MeasureSpec 模式:
| 模式 | 常量 | 含义 | 示例 |
|---|---|---|---|
| EXACTLY | MeasureSpec.EXACTLY | 由父级确定的精确大小(match_parent 或固定宽度) | width=400dp → MeasureSpec(400, EXACTLY) |
| AT_MOST | MeasureSpec.AT_MOST | View 可以是指定最大值以内的大小(wrap_content) | width ≤ 400dp → MeasureSpec(400, AT_MOST) |
| UNSPECIFIED | MeasureSpec.UNSPECIFIED | 无限制 — View 可以是任意大小(ScrollView、RecyclerView) | 宽度不受限制 → MeasureSpec(0, UNSPECIFIED) |
onMeasure 的实现应该:
setMeasuredDimension(int width, int height) 保存测量的尺寸。getPaddingLeft() + getPaddingRight()。measureChild() 或 measureChildWithMargins() 测量所有后代。典型错误:在 wrap_content 时不考虑 MeasureSpec。如果 View 设置为 wrap_content,但 onMeasure 不处理 AT_MOST 并返回固定大小,View 要么被裁剪,要么占用比需要更多的空间。
onLayout — View 或 ViewGroup 在其边界内放置后代的阶段。对于普通 View(非 ViewGroup),onLayout 不是必需的 —— 系统自己使用从父级传递的参数调用 layout()。对于 ViewGroup,onLayout 是强制性的 —— 没有它,子 View 不会被放置。
onLayout 的签名:
@Override
protected void onLayout(boolean changed,
int left, int top,
int right, int bottom) {
// 放置子 View
}
changed 参数指示 View 的位置或大小是否与上一个布局相比发生了变化。如果为 false — View 可以跳过后代位置的重算以进行优化。
对于 ViewGroup,onLayout 应该:
getChildCount() 和 getChildAt(i) 遍历所有后代。child.layout(l, t, r, b)。onLayout 在 onMeasure 之后调用 — 测量的尺寸可以通过 getMeasuredWidth()/getMeasuredHeight() 获得。如果子 View 在 layout() 之后有不同的实际尺寸,将调用 requestLayout() 进行重新测量。这称为“布局传递”,可能引发重新计算的连锁反应。
onDraw — View 在 Canvas 上绘制自身的阶段。这是唯一可以在没有 onMeasure 和 onLayout 的情况下多次调用的阶段 —— 如果 View 被标记为 invalidate()。Canvas 提供了绘制 API:drawLine、drawRect、drawCircle、drawText、drawBitmap 和 drawPath。
onDraw 规则:
canvas.clipRect() 裁剪不可见部分。ViewGroup 中的绘制顺序:背景(setBackgroundDrawable)→ onDraw(内容)→ dispatchDraw(子 View)→ onDrawForeground(前景)。dispatchDraw 调用每个后代的 onDraw。重写 dispatchDraw 用于在子元素之上应用效果。
根据 Android Vitals 统计,onDraw 中最常见的丢帧原因 — 方法内部创建对象(48%)、调用 decodeResource(22%)和没有缓存的复杂 Path 操作(15%)。
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 — 创建独特 UI 的强大工具,但需要严格遵守性能规则。以下是 Google 关于优化 View Lifecycle 的关键建议。
setLayerType(LAYER_TYPE_HARDWARE),完成后使用 setLayerType(LAYER_TYPE_NONE)。带有正确 onMeasure、onDraw 和 invalidate 实现的简单圆形进度指示器。
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 中。
将子 View 按行排列的自定义 ViewGroup(如 Flexbox 换行)。
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 的边距。
自定义 View 绘制平滑的贝塞尔曲线,预先计算并缓存 Path。
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 — 循环绘制过程(onMeasure → onLayout → onDraw),不依赖于 Activity 的创建/销毁。View 没有 onStart/onStop — 它要么可见(附加到窗口),要么不可见。Activity Lifecycle 管理应用程序组件的状态,而 View Lifecycle 管理 UI 的绘制。
requestLayout() 从根部触发整个 View 树的完整周期 onMeasure → onLayout → onDraw。如果频繁调用 requestLayout()(例如每帧动画),会导致卡顿和丢帧。根据 Google 的数据,一次 requestLayout 在包含 10 个元素的 ViewGroup 上平均需要 2–5 毫秒。动画请使用 invalidate()。
onAttachedToWindow 在 View 附加到窗口(Window)时被调用 —— 成为可见层级结构的一部分。此时 View 获得硬件加速和访问窗口资源(WindowManager、Display)的权限。onAttachedToWindow 是注册动画监听器和 BroadcastReceiver 的正确位置,该接收器在 View 可见期间保持活动。
过度绘制 — 像素在单个帧中被多次绘制的情况。每次额外的传递都是 GPU 时间的浪费。减少方法:在主题中设置 windowBackground(不要在布局中绘制背景),使用 canvas.clipRect(),避免合并嵌套背景,使用 ConstraintLayout 代替嵌套的 LinearLayout。Android Studio → Profile GPU Rendering → Overdraw 显示过度绘制的颜色图(蓝色 = 1x,红色 = 3x+)。
是的,如果 View 有背景(background)。super.onDraw() 绘制 View 的背景。如果您的自定义 View 没有背景或您绘制自己的背景,可以省略 super.onDraw() —— 这可以节省一次绘制传递。对于 ViewGroup,super.dispatchDraw() 是强制性的 —— 它绘制子 View。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。