Android View 绘制机制完整详解

107 阅读20分钟

一、概述

1.1 什么是 View 绘制机制

View 绘制机制是 Android 将 View 从布局或代码构建出来,经过 测量(Measure)→ 布局(Layout)→ 绘制(Draw),最终显示到屏幕上的完整流程。

1.2 三个阶段与设计思想

阶段回答的问题核心方法输出
Measure多大?onMeasure()通过 setMeasuredDimension() 设置宽高
Layout在哪?onLayout()通过 layout() 设置 left/top/right/bottom
Draw画什么?onDraw()通过 Canvas 绘制内容

记忆口诀: 「多大、在哪、画什么」。

设计思想:

  • 责任链:从根 View 向下传递测量/布局约束。
  • 测量自顶向下,布局自顶向下,尺寸/位置确定后自底向上汇总。
  • 通过 requestLayout/invalidate 与 measure/layout/draw 的分离,避免重复测量与绘制。

二、整体调用链

ViewRootImpl.performTraversals()
    │
    ├─► performMeasure()  ──► View.measure()  ──► onMeasure()  ──► setMeasuredDimension()
    │
    ├─► performLayout()  ──► View.layout()   ──► onLayout()   ──► 设置四边界
    │
    └─► performDraw()    ──► View.draw()     ──► onDraw()      ──► Canvas 绘制

顺序固定: Measure → Layout → Draw,不可颠倒。

补充: 何时执行一次遍历由 Choreographer 与 VSync 信号协调,保证绘制与屏幕刷新同步,减少卡顿与撕裂。


三、MeasureSpec

3.1 含义与结构

MeasureSpec 是测量时的约束,一个 int 表示:

  • 高 2 位:Mode(测量模式)
  • 低 30 位:Size(尺寸)
int mode  = MeasureSpec.getMode(measureSpec);
int size  = MeasureSpec.getSize(measureSpec);
int spec  = MeasureSpec.makeMeasureSpec(size, mode);

3.2 三种测量模式

模式含义典型场景处理方式
EXACTLY尺寸已定,必须用该值具体 dp、match_parent(父已定尺寸)width = size
AT_MOST不超过该值,可更小wrap_contentwidth = Math.min(计算值, size)
UNSPECIFIED无限制ScrollView 子 View、list 未定高度width = 计算值

口诀: 「精确必须用、最大可更小、未指定任意大」。

3.3 父到子的 MeasureSpec 规则

子 View LayoutParams父 View Mode子 View 得到的 MeasureSpec
具体数值(如 100dp)任意EXACTLY,size = 该数值
match_parentEXACTLYEXACTLY,size = 父的 size
match_parentAT_MOSTAT_MOST,size = 父的 size
wrap_contentEXACTLYAT_MOST,size = 父的 size
wrap_contentAT_MOSTAT_MOST,size = 父的 size
match_parent / wrap_contentUNSPECIFIEDUNSPECIFIED,size = 0

总结:

  • 具体数→EXACTLY;父未定→UNSPECIFIED;父已定则 match_parent 随父得 EXACTLY/AT_MOST;wrap_content 一律 AT_MOST。

四、Measure 阶段

4.1 流程

ViewRootImpl.performMeasure()
    → DecorView.measure()
    → ViewGroup.onMeasure()
    → 遍历子 View,getChildMeasureSpec() 得到子 MeasureSpec
    → 子 View.measure(spec) → 子 onMeasure() → setMeasuredDimension()
    → 父根据子测量结果确定自身尺寸并 setMeasuredDimension()

4.2 onMeasure() 要点

  • 调用时机: 首次加入 View 树、requestLayout()、父/子尺寸变化。
  • 必须做的事: 在方法结束前调用 setMeasuredDimension(width, height),否则会抛异常。
  • 尺寸计算要算上 padding:
    width = contentWidth + getPaddingLeft() + getPaddingRight()(高度同理)。
  • 最小尺寸: getSuggestedMinimumWidth/Height() 会取 android:minWidth/minHeight 与背景 Drawable 最小宽高的较大者,自定义尺寸时可用作下限。

4.3 按模式处理的通用写法

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int w = resolveSizeAndState(
        getPaddingLeft() + getPaddingRight() + contentWidth,
        widthMeasureSpec, 0);
    int h = resolveSizeAndState(
        getPaddingTop() + getPaddingBottom() + contentHeight,
        heightMeasureSpec, 0);
    // setMeasuredDimension 需要纯尺寸,故用 resolveSize;resolveSizeAndState 的高位可带 state 供父 View 使用
    setMeasuredDimension(
        resolveSize(w, widthMeasureSpec),
        resolveSize(h, heightMeasureSpec));
}

说明: resolveSize(size, spec) 按 Mode 返回最终尺寸;resolveSizeAndState(size, spec, childMeasuredState) 还会把子 View 的测量状态(如 MEASURED_STATE_TOO_SMALL)打包进返回值,便于父 View 决策。

对自定义单一尺寸逻辑时,可显式按 Mode 处理:

int width;
switch (MeasureSpec.getMode(widthMeasureSpec)) {
    case MeasureSpec.EXACTLY:
        width = MeasureSpec.getSize(widthMeasureSpec);
        break;
    case MeasureSpec.AT_MOST:
        width = Math.min(calculatedWidth, MeasureSpec.getSize(widthMeasureSpec));
        break;
    default: // UNSPECIFIED
        width = calculatedWidth;
        break;
}
// height 同理,最后 setMeasuredDimension(width, height);

4.4 注意

  • 不要在此方法里调用 requestLayout(),可能造成测量循环。
  • 只读 MeasureSpec,不要修改。

五、Layout 阶段

5.1 流程

ViewRootImpl.performLayout()
    → DecorView.layout(l,t,r,b)
    → ViewGroup.onLayout()
    → 遍历子 View,计算子 left/top/right/bottom
    → 子 View.layout(l,t,r,b) → 子 onLayout()

5.2 onLayout() 要点

  • 调用时机: 首次布局、requestLayout()、父尺寸/位置变化。
  • View: 默认空实现(无子 View)。
  • ViewGroup: 必须重写,为每个非 GONE 子 View 调用 child.layout(l, t, r, b)

5.3 子 View 位置计算

子 View 的宽高必须用 测量结果,不能用 getWidth/getHeight(此时可能仍为 0):

int childWidth  = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
child.layout(childLeft, childTop,
             childLeft + childWidth, childTop + childHeight);

子 View 的 left/top 要加上父的 padding,right/bottom 不要超出父的可绘制区域。若子 View 有 margin,在计算 childLeft/childTop 时需加上 ((MarginLayoutParams) child.getLayoutParams()).leftMargintopMargin 等,避免重叠或越界。

5.4 注意

  • 布局时只用 getMeasuredWidth() / getMeasuredHeight()
  • 跳过 GONE 子 View。

六、Draw 阶段

6.1 View.draw() 内部顺序

1. drawBackground(canvas)
2. onDraw(canvas)           // 自定义内容
3. dispatchDraw(canvas)     // ViewGroup 绘制子 View
4. onDrawForeground(canvas) // 前景、滚动条等

6.2 onDraw() 要点

  • 调用时机: 首次显示、invalidate()、内容或动画导致需要重绘。
  • onDraw() 里用 getWidth()/getHeight()getPaddingXxx() 得到可用区域,在该区域内用 Canvas 绘制。
  • 禁止在 onDraw() 里 new 对象(如 Paint、Path),应提前创建并复用,减少 GC。
  • visibility 与绘制GONE 的 View 不参与 measure/layout,也不会被 draw;INVISIBLE 仍参与 measure 和 layout,只是 draw 时跳过自身内容,占位保留。

6.3 Canvas / Paint 常用 API 速览

  • 图形:drawCircledrawRectdrawRoundRectdrawLinedrawArcdrawPath
  • 文字:drawText
  • 位图:drawBitmap
  • 裁剪:clipRectclipPath
  • 变换:translaterotatescalesave()/restore()

Paint 常用:setColorsetStyle(FILL/STROKE)setStrokeWidthsetTextSizesetAntiAlias


七、invalidate 与 requestLayout

方法触发的阶段典型使用场景
invalidate()仅 Draw内容变化(文字、颜色、动画帧),需要重绘;须在主线程调用
postInvalidate()仅 Draw与 invalidate 相同,但可在子线程调用,内部 post 到主线程
requestLayout()Measure + Layout,之后会再 Draw尺寸或位置变化(如改 LayoutParams、动态改宽高)
  • 只改「画什么」用 invalidate()postInvalidate();改「多大」「在哪」用 requestLayout()
  • 传递机制:两者都会从当前 View 向上传递到 ViewRootImpl(父会调用父的 invalidate/requestLayout),最终由 ViewRootImpl 在下一帧统一执行一次 performTraversals();同一帧内多次 requestLayout 可能被合并为一次遍历,避免重复测量与布局。
  • postInvalidate():与 invalidate() 作用相同(只触发 Draw),但可在子线程中调用;内部通过 Handler 将重绘请求 post 到主线程执行。子线程更新 View 外观时应使用 postInvalidate(),不能直接调用 invalidate()(会抛「仅主线程可操作 View」相关异常)。

八、getWidth 与 getMeasuredWidth

方法含义可用时机
getWidth()布局后的宽度,等于 getRight() - getLeft()layout 之后
getMeasuredWidth()测量得到的宽度measure 之后
  • getHeight() = getBottom() - getTop(),与 getWidth 同理。

  • onLayout() 及布局相关逻辑里只用 getMeasuredWidth()/getMeasuredHeight()(此时 getWidth 可能仍为 0)。

  • onDraw() 里用 getWidth()/getHeight() 和 padding 计算绘制区域。

  • getWidth 为 0 时如何获取宽高? 若在 Activity/Fragment 里过早调用(如 onCreate),layout 尚未完成,可:① 使用 view.post(() -> { int w = view.getWidth(); }) 在下一帧取;② 使用 ViewTreeObserver.addOnGlobalLayoutListener 在首次 layout 完成后回调里取;③ 用 view.getMeasuredWidth() 仅当 measure 已执行完时才有值。

  • scrollTo/scrollBy 与 layout 的区别? scrollTo/scrollBy 只改变 View 的 mScrollX/mScrollY(内容偏移),不触发 measure 和 layout,只影响绘制时 Canvas 的偏移;子 View 的 left/top/right/bottom 不变。改「在哪」且希望触发重新布局用 requestLayout();只改「视觉偏移」用 scrollTo/scrollBy 即可。


九、自定义 View 要点

9.1 步骤

  1. 继承 View(或 ViewGroup)。
  2. 重写 onMeasure():处理三种 Mode,加上 padding,最后 setMeasuredDimension()
  3. 若为 ViewGroup,重写 onLayout():用 getMeasuredWidth/Height 为子 View 算位置并 child.layout(),若有 margin 需一并参与计算。
  4. 重写 onDraw():在可用区域内用 Canvas 绘制,不 new 对象。
  5. 按需:在构造中解析自定义属性(如 values/attrs.xml 声明属性,obtainStyledAttributes 读取)、重写 onTouchEvent 等。

9.2 常见问题与修正

现象原因修正
View 不显示未调用 setMeasuredDimension 或宽高为 0保证 onMeasure 末尾调用并给合法宽高
wrap_content 无效未处理 AT_MOST,直接用了父给的 size在 AT_MOST 下用「内容尺寸」与 size 取 min
padding 无效测量/绘制未考虑 padding测量时加上 padding;绘制时在 getPaddingXxx() 内画
在 onLayout 里用 getWidth 为 0getWidth 在 layout 完成前未赋值一律用 getMeasuredWidth/Height 布局子 View
子 View margin 无效 / 重叠计算子 View 位置时未加上 margin用 MarginLayoutParams 的 leftMargin/topMargin 等参与 childLeft/childTop 计算

9.3 自定义 ViewGroup 的 onMeasure 模板思路

1. 遍历子 View(跳过 GONE),用 getChildMeasureSpec() 生成子 MeasureSpec,调用 child.measure()。
2. 根据子 View 的 getMeasuredWidth/Height 和自身 padding/margin 规则,算出自身 width/height。
3. 用 resolveSize(width, widthMeasureSpec) 等与父约束对齐,再 setMeasuredDimension()。

十、绘制与性能优化

10.1 过度绘制(Overdraw)

含义: 同一像素被绘制多次,造成 GPU 浪费。

检测方式: 开发者选项 → 「显示过度绘制」:

  • 蓝色:绘制 1 次(正常)
  • 绿色:绘制 2 次(可接受)
  • 浅红:绘制 3 次(需优化)
  • 深红:绘制 4 次及以上(必须优化)

减少过度绘制:

  1. 移除不必要的背景
// 若与父 View 背景相同,可移除
view.setBackground(null);
  1. 用 clipRect 限制绘制区域
canvas.save();
canvas.clipRect(left, top, right, bottom);
canvas.drawBitmap(bitmap, x, y, paint);
canvas.restore();
  1. 用 ViewStub 延迟加载
<ViewStub android:id="@+id/stub" android:layout="@layout/heavy_layout" />
  1. 用 merge 减少布局层级
<merge>
    <TextView ... />
    <ImageView ... />
</merge>
  1. 用 ConstraintLayout 扁平化布局:减少嵌套,替代多层 LinearLayout/RelativeLayout,从源头降低 measure/layout 次数与过度绘制。

10.2 绘制性能优化

  1. 禁止在 onDraw() 中创建对象
// ❌ 错误
protected void onDraw(Canvas canvas) {
    Paint paint = new Paint();
    canvas.drawCircle(100, 100, 50, paint);
}
// ✅ 正确:成员变量复用
private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
protected void onDraw(Canvas canvas) {
    canvas.drawCircle(100, 100, 50, paint);
}
  1. 启用硬件加速(默认已开启)
<application android:hardwareAccelerated="true">
  1. 复杂静态内容做 Bitmap 缓存
private Bitmap cacheBitmap;
private Canvas cacheCanvas;
// 首次或尺寸变化时绘制到 cacheBitmap,onDraw 中 drawBitmap(cacheBitmap, ...)
  1. 只重绘脏区域
invalidate(left, top, right, bottom);  // 仅重绘该矩形区域

10.3 最佳实践小结

阶段建议
Measure正确处理 EXACTLY/AT_MOST/UNSPECIFIED;支持 wrap_content;算上 padding;必须 setMeasuredDimension()
Layout用 getMeasuredWidth/Height;考虑 padding 和 margin;跳过 GONE 子 View
Draw不在 onDraw 里 new 对象;复用 Paint/Path;减少过度绘制;必要时用硬件加速
整体减少布局层级(如用 ConstraintLayout 扁平化);用 ViewStub 延迟加载;用 merge;合理使用缓存

十一、常见问题与解决方案

11.1 View 不显示

可能原因: 未调用 setMeasuredDimension()、宽高为 0、被其他 View 遮挡。

解决:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int width = ...;  // 确保 > 0
    int height = ...;
    setMeasuredDimension(width, height);  // 必须调用
}

11.2 wrap_content 不生效

原因: 未处理 AT_MOST,直接用了父给的 size,表现像 match_parent。

解决:

if (widthMode == MeasureSpec.AT_MOST) {
    width = calculateContentWidth();
    width = Math.min(width, widthSize);
} else if (widthMode == MeasureSpec.EXACTLY) {
    width = widthSize;
}
setMeasuredDimension(width, height);

11.3 padding 不生效

原因: 测量和绘制时未考虑 padding。

解决:

// onMeasure
int width  = contentWidth + getPaddingLeft() + getPaddingRight();
int height = contentHeight + getPaddingTop() + getPaddingBottom();
setMeasuredDimension(width, height);

// onDraw:在扣除 padding 的区域内绘制
int left   = getPaddingLeft();
int top    = getPaddingTop();
int right  = getWidth() - getPaddingRight();
int bottom = getHeight() - getPaddingBottom();

11.4 内存泄漏

原因: Paint/Path 等持有 Context;Bitmap 未回收;Handler 未移除。

解决:

// 优先用 ApplicationContext
Context appContext = getContext().getApplicationContext();

// Bitmap 及时回收
if (bitmap != null && !bitmap.isRecycled()) {
    bitmap.recycle();
    bitmap = null;
}
// 在 onDetachedFromWindow 中移除 Handler 回调等

11.5 常见错误对比

错误 1:onMeasure 未调用 setMeasuredDimension

// ❌
protected void onMeasure(...) {
    int width = 200, height = 200;
    // 忘记调用
}
// ✅
setMeasuredDimension(width, height);

错误 2:未处理 AT_MOST 导致 wrap_content 无效

// ❌ 固定宽高
int width = 200;
// ✅ 按 Mode 处理,AT_MOST 时用 Math.min(计算值, widthSize)

错误 3:在 onDraw 中 new 对象

// ❌ Paint paint = new Paint();
// ✅ 成员变量 private Paint paint; 在 init 中初始化,onDraw 中复用

错误 4:未考虑 padding

// ❌ width = contentWidth;
// ✅ width = contentWidth + getPaddingLeft() + getPaddingRight();

错误 5:onLayout 中用 getWidth()

// ❌ int w = child.getWidth();   // 可能为 0
// ✅ int w = child.getMeasuredWidth();

十二、快速对照表

12.1 三阶段与三方法

阶段方法必须做/使用
MeasureonMeasure()调用 setMeasuredDimension();用 MeasureSpec.getMode/Size
LayoutonLayout()ViewGroup 内对子 View 调用 layout();用 getMeasuredWidth/Height
DrawonDraw()用 Canvas 绘制;用 getWidth/getHeight 和 padding

12.2 LayoutParams 与子 View MeasureSpec

子 layout 参数子得到的 Mode
具体数值EXACTLY
match_parent与父 Mode 一致(EXACTLY / AT_MOST / UNSPECIFIED)
wrap_content父为 EXACTLY/AT_MOST 时 AT_MOST;父为 UNSPECIFIED 时 UNSPECIFIED

12.3 常用 API 归类

分类方法 / API说明
Measuremeasure()onMeasure()setMeasuredDimension()测量入口与必须调用
getMeasuredWidth() / getMeasuredHeight()测量结果,layout 前用
getSuggestedMinimumWidth() / getSuggestedMinimumHeight()建议最小尺寸(minWidth、背景等)
getChildMeasureSpec()resolveSize()resolveSizeAndState()子 MeasureSpec、与父约束对齐
Layoutlayout()onLayout()布局入口;ViewGroup 内对子 View 调用
getWidth() / getHeight()getLeft() / getTop() / getRight() / getBottom()布局后宽高与四边界
Drawdraw()onDraw()dispatchDraw()绘制入口;自定义内容;ViewGroup 绘制子 View
invalidate()postInvalidate()触发重绘;postInvalidate 可在子线程
触发布局/重绘requestLayout()向上传递,触发 Measure + Layout + Draw
invalidate() / postInvalidate()向上传递,仅触发 Draw

十三、面试题大全

一、基础概念

1. View 的绘制流程是什么?
答: 三阶段,顺序固定:

  • Measure:确定宽高。入口 ViewRootImpl.performTraversals()performMeasure(),最终到各 View 的 onMeasure(),必须调用 setMeasuredDimension()
  • Layout:确定位置。performLayout() → 根 layout(l,t,r,b) → 各 onLayout(),设置四边界。
  • Draw:绘制内容。performDraw()View.draw()onDraw(),用 Canvas 绘制。

记忆:多大(Measure)→ 在哪(Layout)→ 画什么(Draw)

2. onMeasure()、onLayout()、onDraw() 分别做什么?
答:

  • onMeasure:根据父传入的 MeasureSpec 测量宽高,结束时必须调用 setMeasuredDimension(width, height)
  • onLayout:确定自己在父容器中的位置(left/top/right/bottom);ViewGroup 中还需为每个子 View 调用 child.layout(l,t,r,b)
  • onDraw:在 Canvas 上绘制自身内容(以及 ViewGroup 通过 dispatchDraw 绘制子 View)。

3. View 的 MeasureSpec 是什么?三种测量模式分别是什么?
答: MeasureSpec 是测量时的约束,由一个 int 表示:高 2 位为 Mode,低 30 位为 Size。

三种模式:

  • EXACTLY:尺寸已定,必须用该值(如具体 dp、match_parent 且父已定尺寸)。
  • AT_MOST:最大不超过该值,可更小(如 wrap_content)。
  • UNSPECIFIED:无限制(如 ScrollView 子 View、列表未定高)。

4. View 的布局流程?
答: ViewRootImpl.performLayout() → 根 View 的 layout(l,t,r,b)onLayout()。ViewGroup 在 onLayout() 中遍历子 View,根据布局规则计算每个子的 left/top/right/bottom,再调用 child.layout(l,t,r,b) 确定子 View 位置。

5. invalidate() 和 requestLayout() 的区别?
答:

  • invalidate():只触发 Draw,且必须在主线程调用。用于内容变化(文字、颜色、动画帧)需要重绘时。
  • requestLayout():触发 Measure + Layout,之后会再触发一次 Draw。用于尺寸或位置变化(如改 LayoutParams、动态改宽高)。

口诀:只改「画什么」用 invalidate(子线程用 postInvalidate);改「多大」「在哪」用 requestLayout。

6. getWidth() 和 getMeasuredWidth() 的区别?onLayout 里该用哪个?
答:

  • getWidth():布局后的宽度,等于 right - leftlayout 完成后才有值。
  • getMeasuredWidth():测量得到的宽度,measure 完成后就有值。

onLayout() 及布局相关逻辑里必须用 getMeasuredWidth()/getMeasuredHeight(),因为此时子 View 的 layout 可能尚未执行,getWidth() 可能仍为 0。

7. View.draw() 的绘制顺序(从背景到前景)?
答: 固定顺序:

  • drawBackground()
  • onDraw()(自定义内容)
  • dispatchDraw()(ViewGroup 绘制子 View)
  • onDrawForeground()(前景、滚动条等)

8. ViewGroup 和 View 在绘制机制上的区别?
答:

  • View:叶子节点。onMeasure 只决定自身宽高;onLayout 空实现;onDraw 只画自己。
  • ViewGroup:在 onMeasure 中测量所有子 View 并据此确定自身尺寸;在 onLayout 中为每个子 View 调用 child.layout() 排布位置;通过 dispatchDraw() 绘制子 View。

9. 为什么 View 的 onLayout() 是空实现?
答: View 没有子 View,不需要给子 View 排布位置;只有 ViewGroup 需要重写 onLayout,在内部为每个子 View 调用 child.layout(l,t,r,b)


二、MeasureSpec 与传参

10. 三种 MeasureSpec 模式分别对应什么场景?
答:

  • EXACTLY:具体 dp、match_parent 且父已定尺寸。
  • AT_MOST:wrap_content。
  • UNSPECIFIED:ScrollView 子 View、列表未定高、需要测量「最大可能尺寸」时。

11. 父 View 的 MeasureSpec 如何传给子 View?
答: 根据子的 LayoutParams(宽/高)与父的 MeasureSpec 组合:

  • 具体数值 → 子得到 EXACTLY,size 为该数值。
  • match_parent → 子 Mode 与父一致(EXACTLY 或 AT_MOST),size 为父的 size。
  • wrap_content → 子得到 AT_MOST,size 为父的 size。

API:ViewGroup.getChildMeasureSpec(parentSpec, padding, childDimension)

12. 如何从 MeasureSpec 取 Mode/Size?如何创建 MeasureSpec?
答:

  • MeasureSpec.getMode(spec)MeasureSpec.getSize(spec)
  • 创建MeasureSpec.makeMeasureSpec(size, mode)

13. 为什么 wrap_content 要处理 AT_MOST?自定义 View 如何正确支持 wrap_content?
答:

  • 原因:子声明 wrap_content 时,父传下来的就是 AT_MOST。若不按 Mode 处理,直接使用父给的 size,效果就会和 match_parent 一样占满父容器。
  • 正确做法:在 onMeasure 中当 Mode 为 AT_MOST 时,用「内容实际需要的尺寸」与 MeasureSpec.getSize(spec)min,再 setMeasuredDimension(),并加上 padding。

14. UNSPECIFIED 的典型使用场景?
答: ScrollView 测量子 View 高度、列表 item 高度未定时需要先测量出「理想尺寸」等,父会传 UNSPECIFIED,子可返回任意合理尺寸。


三、onMeasure 详解

15. onMeasure 的调用时机?
答: 首次加入 View 树、调用 requestLayout()、父或子尺寸变化导致需要重新测量时。

16. onMeasure 中必须做什么?
答: 在方法结束前必须调用 setMeasuredDimension(width, height),否则会抛异常。且宽高要合法(不能为负、要包含 padding)。

17. 自定义 View 的 onMeasure 如何实现(含 padding)?
答:

  • ① 取 Mode:MeasureSpec.getMode(widthMeasureSpec),取 Size:MeasureSpec.getSize(widthMeasureSpec)
  • ② 按 Mode:EXACTLY 用 size;AT_MOST 用 Math.min(内容宽 + padding, size);UNSPECIFIED 用内容宽 + padding。
  • ③ 高度同理。
  • ④ 最后 setMeasuredDimension(width, height)。内容宽/高需包含 getPaddingLeft/RightgetPaddingTop/Bottom

18. 为什么不能在 onMeasure 里调用 requestLayout()?
答: 可能造成测量循环:requestLayout() → performTraversals → measure → onMeasure() 里又 requestLayout() → 再次 measure,导致死循环或性能问题。

19. View 默认的 onMeasure 做了什么?为什么默认 wrap_content 会像 match_parent?
答: 调用了 setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthSpec), getDefaultSize(getSuggestedMinimumHeight(), heightSpec))getDefaultSize 在 EXACTLY 和 AT_MOST 时都返回 specSize,即父给的 size,所以未重写时 wrap_content 会直接使用父的最大可用尺寸,表现像 match_parent。

20. getSuggestedMinimumWidth() 的作用?
答: 返回 View 的「建议最小宽度」:取背景 Drawable 的最小宽度与 minWidth 属性中的较大者。用于默认尺寸计算或 UNSPECIFIED 时给出一个合理最小值。

21. ViewGroup 的 onMeasure 如何实现(模板)?GONE 的子 View 要测量/布局吗?
答:

  • ① 遍历子 View,跳过 visibility 为 GONE 的
  • ② 用 getChildMeasureSpec(parentSpec, padding, childDimension) 得到子 MeasureSpec,调用 child.measure()
  • ③ 根据所有子 View 的 getMeasuredWidth/Height 和自身 padding、布局规则,计算自身宽高。
  • ④ 用 resolveSize() 与父约束对齐后 setMeasuredDimension()

GONE 子 View 不参与测量和布局,直接跳过即可。


四、onLayout 详解

22. onLayout 的调用时机?
答: 首次布局、requestLayout()、父尺寸或位置变化导致需要重新布局时。

23. onLayout 的参数含义?
答: onLayout(boolean changed, int left, int top, int right, int bottom):changed 表示本次布局相对上次是否有变化;left/top/right/bottom 为当前 View 在父容器中的四边界(已由父在调用 layout(l,t,r,b) 时传入)。

24. 自定义 View 要重写 onLayout 吗?ViewGroup 的 onLayout 如何实现?
答:

  • 自定义 View:无子 View 时一般不需要重写(默认空实现)。
  • 自定义 ViewGroup:必须重写。遍历子 View(跳过 GONE),用 getMeasuredWidth/Height 计算每个子的 left/top/right/bottom,再调用 child.layout(l, t, r, b)。子 View 的 left/top 要从父的 getPaddingLeft/Top 起算,right/bottom 不超出父的可绘制区域。

25. layout() 方法的作用?
答: 设置 View 的 mLeft/mTop/mRight/mBottom,并调用 onLayout()。在 ViewGroup 中由父对每个子 View 调用 child.layout(l,t,r,b),把计算好的四边界传给子 View,完成子 View 的定位。

26. onLayout 里如何考虑 padding?
答: 子 View 的可用区域 = 父的尺寸减去父的 padding。子 View 的 left/top 从 getPaddingLeft()getPaddingTop() 起算;right/bottom 不超过 getWidth() - getPaddingRight()getHeight() - getPaddingBottom()


五、onDraw 详解

27. onDraw 的调用时机?
答: 首次显示、调用 invalidate()/postInvalidate()、内容或动画导致需要重绘时。

28. 自定义 View 的 onDraw 如何实现?
答:

  • 在扣除 padding 的区域内用 Canvas 绘制:left = getPaddingLeft(),top = getPaddingTop(),right = getWidth() - getPaddingRight(),bottom = getHeight() - getPaddingBottom()。
  • 使用 drawCircle、drawRect、drawText 等,复用成员变量 Paint/Path,禁止在 onDraw 里 new 对象。

29. 为什么不能在 onDraw 里 new 对象?
答: onDraw 会被频繁调用(每帧或每次 invalidate),在内部 new Paint/Path 等会增加 GC 压力,导致卡顿。应在构造或 init 中创建,在 onDraw 中只改属性并复用。

30. Canvas 和 Paint 常用 API?save()/restore() 的作用?
答:

  • Canvas:drawCircle/Rect/Line/Path/Text/Bitmap;clipRect/clipPath;translate/rotate/scale。save() 保存当前变换和裁剪状态,restore() 恢复,用于局部变换后不影响后续绘制。
  • Paint:setColor、setStyle(FILL/STROKE)、setStrokeWidth、setTextSize、setAntiAlias 等,同样应提前创建并复用。

31. 自定义 View 动画如何配合绘制?ViewGroup 如何绘制子 View?
答:

  • 动画配合绘制:用 ValueAnimator/ObjectAnimator 更新状态(如 alpha、位置),在动画回调里调用 invalidate(),在 onDraw 里根据当前状态绘制。
  • ViewGroup 绘制子 View:在 View.draw() 中会调用 dispatchDraw(),由 ViewGroup 实现:遍历子 View 并调用子 View 的 draw(),从而完成子 View 的绘制。

六、绘制与性能优化(合并)

32. 什么是过度绘制?如何检测与减少?
答:

  • 过度绘制:同一像素被绘制多次,浪费 GPU。
  • 检测:开发者选项 →「显示过度绘制」:蓝 1 次、绿 2 次、浅红 3 次、深红 4 次及以上。
  • 减少:移除多余背景、用 clipRect 限制绘制区域、ViewStub 延迟加载、merge 减少层级、invalidate(left, top, right, bottom) 只重绘脏区。

33. 自定义 View / onDraw 性能优化有哪些?
答:

  • ① 不在 onDraw 里 new 对象,复用 Paint/Path。
  • ② 用 clipRect 限制绘制区域。
  • ③ 复杂静态内容可做 Bitmap 缓存,onDraw 中只 drawBitmap。
  • ④ 需要时 invalidate(rect) 只重绘脏区域。
  • ⑤ 减少过度绘制(见上题)。
  • ⑥ 默认已开启硬件加速,必要时在 manifest 中 android:hardwareAccelerated="true"

34. 图片与动画的绘制优化?
答:

  • 图片:合适格式与压缩、LruCache、缩略图、异步加载避免阻塞主线程。
  • 动画:使用属性动画 + 硬件加速、ViewPropertyAnimator、降低每帧绘制复杂度,避免在动画回调里 new 对象。

35. 如何监控绘制性能?
答: 常用方式:

  • Systrace
  • 开发者选项中的「GPU 渲染模式」
  • Layout Inspector 看层级
  • 过度绘制工具
  • Android Profiler 的 CPU/GPU 分析

36. 硬件加速的作用与如何启用?
答:

  • 作用:由 GPU 执行绘制,减轻 CPU 负担,提高绘制性能。
  • 启用:默认已开启;可在 AndroidManifest 的 <application> 中显式设置 android:hardwareAccelerated="true",也可在 Activity 或 View 级别单独配置。

七、自定义 View 实践

37. 自定义 View 有哪几种方式?
答:

  • ① 继承 View 完全自定义。
  • ② 继承 ViewGroup 自定义容器。
  • ③ 组合现有 View(自定义 ViewGroup 包一层)。
  • ④ 继承现有 View(如 TextView)做扩展。

38. 自定义 View 的步骤?
答:

  • ① 继承 View 或 ViewGroup。
  • ② 重写 onMeasure:处理三种 MeasureSpec、加上 padding、最后 setMeasuredDimension。
  • ③ 若为 ViewGroup,重写 onLayout:用 getMeasuredWidth/Height 为子 View 算位置并 child.layout()。
  • ④ 重写 onDraw:在可用区域内用 Canvas 绘制,不 new 对象。
  • ⑤ 按需重写 onTouchEvent、构造里解析自定义属性等。

39. 如何避免自定义 View 导致的内存泄漏?
答:

  • 使用 ApplicationContext 代替 Activity 引用(若不需要 Activity)。
  • Bitmap 使用完及时 recycle() 并置 null。
  • onDetachedFromWindow 中移除 Handler 回调和监听。
  • 避免在 View 中长时间持有 Activity 引用。

40. 触摸事件怎么处理?
答: 重写 onTouchEvent(),根据 MotionEvent.getAction() 处理 ACTION_DOWN/MOVE/UP 等。若在 ViewGroup 中需要拦截子 View 事件,重写 onInterceptTouchEvent(),在合适时机返回 true 将事件交给当前 ViewGroup 的 onTouchEvent。

41. 自定义 View 常见问题与修正?
答:

  • 不显示:未调用 setMeasuredDimension 或宽高为 0 → 保证 onMeasure 末尾调用并给合法宽高。
  • wrap_content 无效:未处理 AT_MOST → 在 AT_MOST 下用内容尺寸与 size 取 min。
  • padding 无效:测量/绘制未考虑 padding → 测量加 padding,绘制在 getPaddingXxx 范围内画。
  • onLayout 里 getWidth 为 0:应用 getMeasuredWidth/Height 布局子 View。
  • 子 View margin 无效/重叠:计算子 View 位置时未加 margin → 用 MarginLayoutParams 的 leftMargin/topMargin 等参与 childLeft/childTop 计算。

42. 如何测试自定义 View?
答:

  • 逻辑:单元测试测 onMeasure/onLayout 的尺寸与位置计算、触摸逻辑。
  • 显示:UI 测试或真机查看不同尺寸/padding 下的显示是否正确。
  • 性能:过度绘制、帧率、内存占用。
  • 兼容性:不同 API 与机型上的表现。

43. invalidate() 和 postInvalidate() 的区别?子线程如何触发重绘?
答: invalidate() 必须在主线程调用,否则会抛异常;postInvalidate() 可在子线程调用,内部通过 Handler 将重绘请求投递到主线程执行。子线程中需要刷新 View 显示时(如异步加载后更新 UI),应使用 postInvalidate()。

44. 在 onCreate/onResume 里 getWidth()、getHeight() 为 0 怎么办?
答: 此时 View 尚未完成 measure 和 layout,getWidth/getHeight 为 0。可用:

  • view.post(() -> { int w = view.getWidth(); }) 在下一帧布局完成后取。
  • view.getViewTreeObserver().addOnGlobalLayoutListener(() -> { int w = view.getWidth(); }) 在首次 layout 完成后回调中取,用完记得 removeOnGlobalLayoutListener

45. GONE 和 INVISIBLE 在 measure/layout/draw 上的区别?
答:

  • GONE:不参与 measure 和 layout(父会跳过),也不会被绘制,不占空间。
  • INVISIBLE:与 VISIBLE 一样参与 measure 和 layout,占位保留,只是 draw 时跳过绘制自身内容。

因此布局计算「可见」子 View 时通常只跳过 GONE,不跳过 INVISIBLE。

46. scrollTo/scrollBy 和 requestLayout 的区别?
答:

  • scrollTo/scrollBy:只改变 View 内容的绘制偏移(mScrollX/mScrollY),不触发 measure 和 layout,子 View 的几何位置不变,仅视觉上平移。
  • requestLayout:会触发整条链的 measure + layout,用于尺寸或位置真正改变时。

只做「滑动偏移」用 scroll;要改子 View 的 left/top 等用 layout + requestLayout。