View 的绘制流程
屏幕上每一个按钮、文字、图片的显示,都经历了相同的过程:测量(Measure)→ 布局(Layout)→ 绘制(Draw)。理解这三个阶段的源码逻辑,是掌握自定义 View 和性能优化的基础。
绘制的源头与动力机制
一切的绘制操作最终都会收束于 ViewRootImpl.performTraversals()——它是 View 树绘制的总调度。但它是怎么被触发的?后续的绘制又是如何源源不断地进行的?这背后依赖于一套精密的消息调度与 VSync(垂直同步)机制。
第一次绘制的消息是怎么发出来的?
Activity 启动后,第一次将 View 树绘制到屏幕上,这个消息的源头可以追溯到 onResume() 生命周期之后。
这就像是一家新开业的餐厅,厨房(ViewRootImpl)接到了第一单(显示窗口),开始准备做菜(绘制)。
- 入口:
addView在ActivityThread.handleResumeActivity中,Activity 的生命周期走到onResume后,系统会调用WindowManager.addView()将 DecorView 添加到窗口中。 - 建联:
ViewRootImpl.setView在这个方法内部,系统创建了ViewRootImpl,它负责管理整个 View 树与系统的交互,并调用setView()准备将其显示出来。 - 请求布局:
requestLayoutsetView()内部会调用requestLayout(),标记需要进行一次彻底的测量和布局。 - 核心调度:
scheduleTraversalsrequestLayout()会立刻触发scheduleTraversals(),这是绘制前最重要的准备工作。它做了两件关键的事:- 开启同步屏障(Sync Barrier):向主线程的
MessageQueue投递一个特殊的“VIP 拦截栅栏”,它会阻塞后续所有普通的同步消息,确保接下来的绘制任务(异步消息)拥有最高优先级。 - 向 Choreographer 注册回调:把真正的绘制任务(
mTraversalRunnable)交给系统的“编舞者”Choreographer,并向底层申请下一次的 VSync 信号。
- 开启同步屏障(Sync Barrier):向主线程的
源码层面非常清晰地体现了这一过程:
// ViewRootImpl.java
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
// 1. 发送同步屏障,保证 UI 绘制优先,阻挡普通的同步消息
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// 2. 向 Choreographer 提交一个 CALLBACK_TRAVERSAL 的回调任务
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
}
第一次绘制调用的整体链路图解如下:
sequenceDiagram
participant AT as ActivityThread
participant WM as WindowManager
participant VR as ViewRootImpl
participant MQ as MessageQueue
participant CH as Choreographer
AT->>WM: addView(DecorView)
WM->>VR: setView()
VR->>VR: requestLayout()
VR->>VR: scheduleTraversals()
VR->>MQ: postSyncBarrier() (开启同步屏障)
VR->>CH: postCallback(TRAVERSAL)
Note over CH: 等待下一次 VSync 信号
当系统底层发出下一次 VSync 信号时,Choreographer 就会收到通知,从而触发第一次的 performTraversals()。
后边的绘制消息是怎么循环的?
第一次绘制完成后,App 就进入了与用户交互的阶段。此时,绘制消息的循环不再是凭空产生的,而是由 UI 更新需求 + VSync 信号 共同驱动的。
我们可以把它比作火车站发车:
VSync 信号就是列车的发车时刻表(对于 60Hz 屏幕,每 16.6ms 一班)。 应用中的重绘请求(
invalidate或requestLayout)就是乘客买票。 只有有乘客买票,且发车时间到了,列车才会开动。如果没有人买票,列车到了时间也不会发车(不空耗资源)。
- 产生重绘需求(买票)
当代码调用
invalidate()、requestLayout()或者有属性动画正在运行时,会沿着 View 树向上传递,最终再次调用到ViewRootImpl.scheduleTraversals()。 - 注册回调并等待发车
scheduleTraversals()再次向Choreographer注册一个绘制任务(CALLBACK_TRAVERSAL),并向底层调用scheduleVsync()申请下一个 VSync 信号。 - VSync 信号到达(发车时间到)
底层 SurfaceFlinger(负责图形合成的系统服务)通过硬件产生 VSync 信号,通过 Binder 传递给应用层的
DisplayEventReceiver.onVsync()。 - 投递特权消息
onVsync()收到信号后,会向主线程投递一个异步消息(MSG_DO_FRAME)。因为之前scheduleTraversals已经设置了同步屏障,所以主线程会越过其他排队的消息,优先执行这个特权消息。 - 执行 doFrame(开车)
主线程执行
Choreographer.doFrame()。这个方法相当于列车长检票,它会严格按时间戳顺序执行几类任务:- 处理输入事件(INPUT)
- 执行动画计算(ANIMATION)
- 执行视图遍历(TRAVERSAL,即调用
mTraversalRunnable触发performTraversals,进而执行 Measure / Layout / Draw)
- 循环流转(连续动画的奥秘)
- 如果只是点了一下按钮变个色,画完后没有新的重绘请求,下一次 VSync 信号到来时应用层会被忽略,进入休眠(省电)。
- 如果正在播放连续动画,由于动画引擎会在
ANIMATION阶段计算属性,并在每一帧结束后再次发起invalidate(),这就相当于“刚下车又立刻买了下一班的票”。于是下一个 VSync 信号到来时,又会再次触发doFrame,这就形成了源源不断、如丝般顺滑的绘制循环。
整个 VSync 驱动的绘制循环链路如下图所示:
sequenceDiagram
participant UI as View (UI层)
participant VR as ViewRootImpl
participant CH as Choreographer
participant SF as SurfaceFlinger (硬件)
participant MQ as MessageQueue
UI->>VR: invalidate() / requestLayout()
VR->>CH: postCallback(TRAVERSAL)
CH->>SF: scheduleVsync() (申请发车)
Note over SF: 16.6ms 后硬件产生 VSync
SF-->>CH: onVsync() (信号到达)
CH->>MQ: sendMessage(MSG_DO_FRAME) (异步消息,无视屏障)
MQ-->>CH: 执行 doFrame()
CH->>CH: 1. doCallbacks(INPUT)
CH->>CH: 2. doCallbacks(ANIMATION)
CH->>CH: 3. doCallbacks(TRAVERSAL)
CH->>VR: 执行 mTraversalRunnable
VR->>VR: performTraversals()
核心的 doFrame 和 mTraversalRunnable 的源码实现:
// Choreographer.java
void doFrame(long frameTimeNanos, int frame) {
// 按时间戳顺序严格执行各个阶段的回调
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos); // 触发 performTraversals
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
}
// ViewRootImpl.java
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
// 移除同步屏障,让被阻塞的普通同步消息恢复执行
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
// 开始真正的绘制三大阶段
performTraversals();
}
}
通过这套“按需申请 + VSync 定时”的机制,Android 保证了 UI 的绘制节奏与屏幕的刷新频率完美对齐,彻底告别了早期版本的画面撕裂(Tearing),也避免了无意义的 CPU/GPU 浪费。
performTraversals:执行三大阶段
当 doFrame 终于把执行权交给了 performTraversals() 时,真正的绘制之旅才刚刚开始:
ViewRootImpl.performTraversals()
├── performMeasure(childWidthMeasureSpec, childHeightMeasureSpec)
│ └── mView.measure(widthSpec, heightSpec)
│ └── onMeasure(widthSpec, heightSpec)
│
├── performLayout(lp, desiredWindowWidth, desiredWindowHeight)
│ └── mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight())
│ └── onLayout(changed, l, t, r, b)
│
└── performDraw()
└── draw(fullRedrawNeeded)
└── drawSoftware(surface, ...)
└── mView.draw(canvas)
└── onDraw(canvas)
mView 是 DecorView(整个 Activity 窗口的顶层 View),从它开始自顶向下递归遍历整棵 View 树。
第一阶段:Measure(测量)
目标:确定每个 View 的宽和高(即得到 measuredWidth 和 measuredHeight)。
整个测量过程,其实就像是公司里“上级向下级批复预算”的流程。
ViewGroup(上级)知道自己有多少空间。View(下级)在LayoutParams里写明了自己想要的尺寸(比如固定 100dp,或者match_parent/wrap_content)。- 最终的尺寸,必须由“上级的可用空间”和“下级的自身要求”共同协商决定。
核心载体:MeasureSpec(预算批复单)
在源码中,上级下发给下级的“预算批复单”叫 MeasureSpec。为了追求极致的性能,Android 将它设计为一个 32 位的 int 值:高 2 位表示测量模式(Mode),低 30 位表示尺寸大小(Size)。
// MeasureSpec 的位运算结构
// |-- 2 bit mode --|------ 30 bit size ------|
// | EXACTLY(01) | 500px |
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
// 三种模式
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
public static int makeMeasureSpec(int size, int mode) {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
| 测量模式(Mode) | 含义与比喻 | 常见触发条件 |
|---|---|---|
| EXACTLY | 精确值(死命令):上级说“你的预算就是精确的 size 这么多,不许变”。 | 设置了具体 dp 值,或者 match_parent(且父容器也是精确值) |
| AT_MOST | 最大值(限额凭票报销):上级说“你的预算最多只有 size 这么多,自己看着办”。 | 设置了 wrap_content |
| UNSPECIFIED | 无限制(无限额度):上级说“你要多少我都给”。 | ScrollView 或 ListView 对子 View 的纵向测量(因为可以无限滚动) |
溯源拷问:顶级 View 的 MeasureSpec 是哪来的?
既然子 View 的 MeasureSpec 是由父容器根据它自己的 MeasureSpec 决定的,那这棵 View 树最顶级的节点(DecorView)根本没有父 View,它的第一笔“预算”是谁批的?
答案是:由 Window 窗口的物理尺寸和 DecorView 自身的 LayoutParams 共同决定。
这笔“天使投资”是由大管家 ViewRootImpl 在执行 performMeasure 时,通过 getRootMeasureSpec() 函数强行计算出来的。
// ViewRootImpl.java
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// 窗口多大,顶级 View 就精确等于多大
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// 顶级 View 想自己定大小,但不能超过窗口大小
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// 顶级 View 指定了精确的 dp 大小
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
这也就是说,DecorView 的尺寸从一开始就被 Window 的大小给框死了。拿着这第一笔“预算单”,DecorView 才开始沿着 View 树,一层一层地通过 getChildMeasureSpec 把预算往下发。
核心逻辑:getChildMeasureSpec(预算审批规则)
子 View 的 MeasureSpec 到底是怎么计算出来的?这归功于 ViewGroup 中的一个极其重要的方法:getChildMeasureSpec()。
它的核心逻辑用比喻来说就是:上级(父容器)根据自己的预算(父 MeasureSpec),再看下级的申请(子 LayoutParams),最终拍板决定下级的预算批复单(子 MeasureSpec)。
graph TD
A[父容器的 MeasureSpec] --> C(getChildMeasureSpec 函数)
B[子 View 的 LayoutParams] --> C
C --> D[子 View 的 MeasureSpec]
这套复杂的“拍板规则”被封装成了以下这张经典的表格:
| 父容器的 MeasureSpec | 子 View 申请:具体 dp 值 | 子 View 申请:match_parent |
子 View 申请:wrap_content |
|---|---|---|---|
| EXACTLY (死命令) | EXACTLY (申请的 dp 值) |
EXACTLY (父级 Size) |
AT_MOST (父级 Size) |
| AT_MOST (限额) | EXACTLY (申请的 dp 值) |
AT_MOST (父级 Size) |
AT_MOST (父级 Size) |
| UNSPECIFIED (无限) | EXACTLY (申请的 dp 值) |
UNSPECIFIED (0) |
UNSPECIFIED (0) |
我们来看看源码是如何实现这个表格的:
// ViewGroup.java
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding); // 扣除父容器的 padding
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
case MeasureSpec.EXACTLY: // 父容器是死命令
if (childDimension >= 0) { // 子 View 申请了具体数值 (比如 100dp)
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY; // 满足它,给精确值
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.EXACTLY; // 把剩下的空间全给它,作为精确值
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST; // 它想自己定大小,但不能超过剩下的空间
}
break;
// ... AT_MOST 和 UNSPECIFIED 逻辑类似,对应上面的表格
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
灵魂拷问:如果子 View 强行要的尺寸比父容器大,会怎样?
这是一个极具穿透力的边缘测试问题。假设父容器(上级)只有 500dp 的可用宽度(Mode 为 EXACTLY),但你在 XML 里给子 View 写死 android:layout_width="1000dp"(下级非要 1000dp)。此时会发生什么?
我们再看一眼刚刚 getChildMeasureSpec 中的这段源码:
case MeasureSpec.EXACTLY: // 父容器是死命令(比如只有 500dp)
if (childDimension >= 0) { // 子 View 强行申请了具体数值 (1000dp)
resultSize = childDimension; // 结果直接给 1000dp!
resultMode = MeasureSpec.EXACTLY; // 模式是 EXACTLY
}
答案是:上级会直接批准!
父容器会生成一个 size=1000, mode=EXACTLY 的批复单给子 View。子 View 也会心安理得地把自己测量为 1000dp,并且在 Layout 阶段被排版为 1000dp 宽。
那为什么在屏幕上看起来,它依然没有撑破父容器呢?
这是因为到了第三阶段 Draw(绘制) 时,ViewGroup 默认开启了 clipChildren = true(裁剪子 View)属性。系统在绘制子 View 时,会用 Canvas.clipRect() 将画布硬生生裁剪到父容器的 500dp 边界内。
也就是说,子 View 确实长到了 1000dp,但超出父容器 500dp 范围的那部分,在渲染时被“物理切除”了,用户根本看不见。如果把父容器的 android:clipChildren="false" 属性设置上,你就能看到子 View 骄傲地突破父级边界显示出来了。
测量的执行链路:measure -> onMeasure -> setMeasuredDimension
当父容器计算出子 View 的 MeasureSpec 后,就会调用子 View 的 measure() 方法。
sequenceDiagram
participant P as ViewGroup (父容器)
participant V as View (子 View)
P->>V: measure(widthMeasureSpec, heightMeasureSpec)
Note over V: measure() 是 final 方法,处理缓存等通用逻辑
V->>V: onMeasure(widthSpec, heightSpec)
Note over V: 开发者重写 onMeasure 进行定制化计算
V->>V: setMeasuredDimension(width, height)
Note over V: 必须调用此方法,保存最终的测量结果
V-->>P: 测量完成
measure():这是 View 的final方法,负责处理缓存优化。如果预算单没变,它就不必重测。开发者不能重写此方法。onMeasure():真正的测量逻辑都在这里。View 会根据收到的 MeasureSpec,计算出自己的实际大小。setMeasuredDimension():测量完成后,必须调用此方法把结果保存到mMeasuredWidth和mMeasuredHeight中,否则会抛出异常。
自定义 View 时:为什么 wrap_content 会失效?
我们在直接继承 View 来写自定义控件时,如果不在 onMeasure 里做处理,那么在使用 wrap_content 时,它会表现得跟 match_parent 一模一样(占满父容器)。
为什么?看 View 类的默认源码:
// View.java 的默认实现
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)
);
}
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST: // 注意这里!
case MeasureSpec.EXACTLY:
result = specSize; // AT_MOST 和 EXACTLY 都直接返回了 specSize!
break;
}
return result;
}
从源码可以看到,当子 View 写了 wrap_content 时,父容器给它的 Mode 会是 AT_MOST。但在 View.getDefaultSize() 的默认逻辑中,AT_MOST 竟然和 EXACTLY 一样,直接返回了父容器给的最大可用尺寸(specSize)!这就是 wrap_content 失效的根本原因。
正确的重写方式:我们需要自己计算“想要多大”,并在 AT_MOST 模式下加以限制。
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
// 1. 根据内容计算"期望尺寸"(比如文字宽度、图片大小)
val desiredWidth = calculateContentWidth() + paddingLeft + paddingRight
val desiredHeight = calculateContentHeight() + paddingTop + paddingBottom
// 2. 根据模式决定最终尺寸
val width = when (widthMode) {
MeasureSpec.EXACTLY -> widthSize // 死命令:直接用给定的尺寸
MeasureSpec.AT_MOST -> min(desiredWidth, widthSize) // 限额:用期望尺寸,但不能超标
else -> desiredWidth // 无限制:想要多大给多大
}
val height = when (heightMode) {
MeasureSpec.EXACTLY -> heightSize
MeasureSpec.AT_MOST -> min(desiredHeight, heightSize)
else -> desiredHeight
}
// 3. 提交最终结果
setMeasuredDimension(width, height)
}
ViewGroup 的递归测量
ViewGroup 在测量时,不仅要测自己,还要负责指挥所有子 View 进行测量:
// ViewGroup.java
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
// 挨个让子 View 测量
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
// 把父容器的 padding 扣掉,算出子 View 的 MeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
// 通知下属去执行测量逻辑
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
第二阶段:Layout(布局)
目标:确定每个 View 在父容器中的位置(left, top, right, bottom)。
// View.java
public void layout(int l, int t, int r, int b) {
// 先判断是否需要重新测量
if (...needsMeasure...) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
}
int oldL = mLeft, oldT = mTop, oldR = mRight, oldB = mBottom;
boolean changed = setFrame(l, t, r, b); // 设置四个顶点坐标
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) != 0) {
onLayout(changed, l, t, r, b); // 回调给子类
}
}
对于普通 View,onLayout 为空(没有子 View 需要布局)。对于ViewGroup,必须实现 onLayout 来安排子 View 的位置:
// 自定义纵向布局示例
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
var currentTop = paddingTop
for (i in 0 until childCount) {
val child = getChildAt(i)
if (child.visibility == View.GONE) continue
val childWidth = child.measuredWidth
val childHeight = child.measuredHeight
val childLeft = paddingLeft
child.layout(childLeft, currentTop, childLeft + childWidth, currentTop + childHeight)
currentTop += childHeight // 下一个子 View 紧跟其后
}
}
getWidth() vs getMeasuredWidth() 的区别:
getMeasuredWidth():measure 阶段确定,来自setMeasuredDimension()getWidth():layout 阶段确定,等于mRight - mLeft- 通常二者相等,但如果
layout()实参与测量值不同就会不等
第三阶段:Draw(绘制)
目标:将 View 的内容绘制到 Canvas 上。
View.draw() 的源码注释清楚地列出了 6 个步骤:
// View.java draw() 方法
public void draw(Canvas canvas) {
// Step 1: 绘制背景
drawBackground(canvas);
// Step 2: 保存 Canvas 图层(为了 fading edge,通常跳过)
// Step 3: 绘制自身内容
onDraw(canvas);
// Step 4: 绘制子 View(ViewGroup 才有效)
dispatchDraw(canvas);
// Step 5: 绘制 fading edge(通常跳过)
// Step 6: 绘制前景(滚动条等装饰)
onDrawForeground(canvas);
}
自定义 View 主要重写 onDraw():
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 绘制一个渐变圆
val shader = RadialGradient(
centerX, centerY, radius,
intArrayOf(Color.RED, Color.BLUE),
null, Shader.TileMode.CLAMP
)
paint.shader = shader
canvas.drawCircle(centerX, centerY, radius, paint)
}
requestLayout vs invalidate
这两个方法是触发重绘的唯一途径,必须理解它们的区别:
requestLayout() invalidate()
│ │
▼ ▼
标记 PFLAG_FORCE_LAYOUT 标记 dirty 区域
向上传递到 ViewRootImpl 向上传递到 ViewRootImpl
│ │
▼ ▼
performTraversals() performTraversals()
├── performMeasure ✅ ├── performMeasure ❌(跳过)
├── performLayout ✅ ├── performLayout ❌(跳过)
└── performDraw ✅ └── performDraw ✅
| 方法 | 触发阶段 | 使用场景 | 线程要求 |
|---|---|---|---|
requestLayout() |
measure + layout + draw | 尺寸或位置发生变化 | 主线程 |
invalidate() |
仅 draw | 内容变化(颜色、文字、动画帧) | 主线程 |
postInvalidate() |
仅 draw | 子线程触发重绘 | 任意线程 |
View.post(Runnable) 能获取宽高的原因
在 onCreate 中直接调 view.getWidth() 返回 0,因为此时还没执行过 measure/layout。但 view.post { } 可以:
// onCreate 中
textView.post {
Log.d("TAG", "width = ${textView.width}") // 正确获取到宽高
}
原因分析:
- View 被 attach 到窗口时,
post()将 Runnable 提交到主线程的Handler performTraversals()(measure/layout/draw)也是主线程 Handler 中的一条消息view.post()的 Runnable 排在performTraversals()之后执行- 所以执行时 View 已经完成测量和布局
如果 View 尚未 attach,post() 的 Runnable 会被暂存在 View.mRunQueue 中,等 dispatchAttachedToWindow() 时再提交到 Handler。
硬件加速与软件渲染
默认情况下(API 14+),Android 开启硬件加速:
| 特性 | 软件渲染 | 硬件加速 |
|---|---|---|
| 渲染实现 | CPU + Skia | GPU + OpenGL/Vulkan |
| Canvas 实现 | Canvas |
DisplayListCanvas(录制 RenderNode) |
invalidate() 效果 |
重绘整个 View | 仅重放受影响的 DisplayList |
| 性能特点 | 灵活但慢 | 大多数场景更快 |
硬件加速下的绘制过程:
onDraw()中的 Canvas 操作被录制为 RenderNode(而非立即执行)- RenderThread(独立线程)将 RenderNode 转换为 GPU 指令
- GPU 执行渲染
这也解释了为什么 invalidate() 在硬件加速下更高效——只需要重放变化部分的 DisplayList,不需要重新遍历整个 View 树。
自定义 View 的性能要点
onDraw()中不要创建对象(如Paint、Path)——因为onDraw可能被频繁调用,应在构造函数中初始化- 避免过度绘制(Overdraw)——减少不必要的背景色叠加
- 使用
clipRect()限制绘制区域——只绘制可见部分 requestLayout()比invalidate()代价大得多——仅内容变化时用invalidate()- 多次属性变更只需一次刷新——属性变更后不要立即调用
invalidate(),等所有属性都设好再调一次