一、概述
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_content | width = Math.min(计算值, size) |
| UNSPECIFIED | 无限制 | ScrollView 子 View、list 未定高度 | width = 计算值 |
口诀: 「精确必须用、最大可更小、未指定任意大」。
3.3 父到子的 MeasureSpec 规则
| 子 View LayoutParams | 父 View Mode | 子 View 得到的 MeasureSpec |
|---|---|---|
| 具体数值(如 100dp) | 任意 | EXACTLY,size = 该数值 |
| match_parent | EXACTLY | EXACTLY,size = 父的 size |
| match_parent | AT_MOST | AT_MOST,size = 父的 size |
| wrap_content | EXACTLY | AT_MOST,size = 父的 size |
| wrap_content | AT_MOST | AT_MOST,size = 父的 size |
| match_parent / wrap_content | UNSPECIFIED | UNSPECIFIED,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()).leftMargin、topMargin 等,避免重叠或越界。
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 速览
- 图形:
drawCircle、drawRect、drawRoundRect、drawLine、drawArc、drawPath - 文字:
drawText - 位图:
drawBitmap - 裁剪:
clipRect、clipPath - 变换:
translate、rotate、scale、save()/restore()
Paint 常用:setColor、setStyle(FILL/STROKE)、setStrokeWidth、setTextSize、setAntiAlias。
七、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 步骤
- 继承
View(或ViewGroup)。 - 重写
onMeasure():处理三种 Mode,加上 padding,最后setMeasuredDimension()。 - 若为 ViewGroup,重写
onLayout():用getMeasuredWidth/Height为子 View 算位置并child.layout(),若有 margin 需一并参与计算。 - 重写
onDraw():在可用区域内用 Canvas 绘制,不 new 对象。 - 按需:在构造中解析自定义属性(如
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 为 0 | getWidth 在 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 次及以上(必须优化)
减少过度绘制:
- 移除不必要的背景
// 若与父 View 背景相同,可移除
view.setBackground(null);
- 用 clipRect 限制绘制区域
canvas.save();
canvas.clipRect(left, top, right, bottom);
canvas.drawBitmap(bitmap, x, y, paint);
canvas.restore();
- 用 ViewStub 延迟加载
<ViewStub android:id="@+id/stub" android:layout="@layout/heavy_layout" />
- 用 merge 减少布局层级
<merge>
<TextView ... />
<ImageView ... />
</merge>
- 用 ConstraintLayout 扁平化布局:减少嵌套,替代多层 LinearLayout/RelativeLayout,从源头降低 measure/layout 次数与过度绘制。
10.2 绘制性能优化
- 禁止在 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);
}
- 启用硬件加速(默认已开启)
<application android:hardwareAccelerated="true">
- 复杂静态内容做 Bitmap 缓存
private Bitmap cacheBitmap;
private Canvas cacheCanvas;
// 首次或尺寸变化时绘制到 cacheBitmap,onDraw 中 drawBitmap(cacheBitmap, ...)
- 只重绘脏区域
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 三阶段与三方法
| 阶段 | 方法 | 必须做/使用 |
|---|---|---|
| Measure | onMeasure() | 调用 setMeasuredDimension();用 MeasureSpec.getMode/Size |
| Layout | onLayout() | ViewGroup 内对子 View 调用 layout();用 getMeasuredWidth/Height |
| Draw | onDraw() | 用 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 | 说明 |
|---|---|---|
| Measure | measure()、onMeasure()、setMeasuredDimension() | 测量入口与必须调用 |
getMeasuredWidth() / getMeasuredHeight() | 测量结果,layout 前用 | |
getSuggestedMinimumWidth() / getSuggestedMinimumHeight() | 建议最小尺寸(minWidth、背景等) | |
getChildMeasureSpec()、resolveSize()、resolveSizeAndState() | 子 MeasureSpec、与父约束对齐 | |
| Layout | layout()、onLayout() | 布局入口;ViewGroup 内对子 View 调用 |
getWidth() / getHeight()、getLeft() / getTop() / getRight() / getBottom() | 布局后宽高与四边界 | |
| Draw | draw()、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 - left,layout 完成后才有值。 - 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/Right、getPaddingTop/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。