14、View绘制流程(上):Measure、Layout、Draw三大流程,ViewRootImpl与Choreographer
各位同学,今天我们来聊聊Android View绘制的核心流程。说实话,这部分内容我当年刚入行时也啃了很久。你想想看,一个App从代码到屏幕上的像素,中间到底经历了什么?为什么有时候布局写得不对,界面就卡成PPT?
我个人习惯把View绘制拆成三个步骤:测量(Measure)、布局(Layout)、绘制(Draw)。这三个词你肯定听过,但背后的机制和协作方式,才是真正决定App流畅度的关键。今天我们就从ViewRootImpl和Choreographer这两个“幕后黑手”讲起,把整个流程串起来。
核心一句话:View绘制不是随随便便就开始的,它由Choreographer驱动,由ViewRootImpl调度,最终在CPU/GPU上完成。任何一个环节出问题,都会导致掉帧。
1. 从ViewRootImpl说起:绘制的总指挥
ViewRootImpl是什么?说白了,它是连接WindowManager和DecorView的桥梁。每个Window都对应一个ViewRootImpl,它负责处理输入事件、执行布局、触发绘制。
我记得有一次排查一个界面启动白屏问题,发现是ViewRootImpl的performTraversals()方法被阻塞了。这个方法就是整个绘制流程的入口,它内部会依次调用performMeasure()、performLayout()、performDraw()。
// ViewRootImpl.java (简化版)
private void performTraversals() {
// 1. 测量阶段
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
// 2. 布局阶段
performLayout(lp, mWidth, mHeight);
// 3. 绘制阶段
performDraw();
}
嗯,这里要注意:performTraversals()并不是每帧都执行。它只在需要的时候被触发,比如调用了requestLayout()、invalidate(),或者系统收到了Vsync信号。
2. Choreographer:帧率的节拍器
Choreographer,翻译过来就是“编舞者”。它负责协调动画、输入、绘制等任务,确保它们在一个Vsync周期内完成。你可以把它理解成Android的“帧率节拍器”。
为什么会需要这个东西?因为屏幕刷新是固定频率的(比如60Hz,每16.6ms一帧)。如果我们在两次刷新之间做了太多工作,下一帧就赶不上了,于是掉帧。
我的经验:我曾经在一个列表滑动场景中,发现Choreographer的doFrame()回调里耗时过长。用Systrace一抓,发现是某个自定义View的onDraw()里做了大量Bitmap操作。后来我把Bitmap缓存起来,帧率就从20fps回到了60fps。
Choreographer的工作流程大致如下:
- 应用注册一个回调(比如动画回调、绘制回调)。
- Choreographer等待下一个Vsync信号。
- Vsync到来时,Choreographer按优先级依次执行回调:输入处理、动画、遍历绘制。
- 如果所有回调在16.6ms内完成,下一帧就能准时显示。
// Choreographer.java (核心逻辑)
void doFrame(long frameTimeNanos, int frame) {
// 1. 处理输入事件
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
// 2. 处理动画
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
// 3. 处理遍历绘制
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
}
避坑指南:我曾经在项目里遇到一个诡异问题:界面偶尔卡顿,但CPU和GPU负载都不高。后来发现是某个第三方SDK在Choreographer的CALLBACK_INPUT回调里做了耗时操作,导致后续的绘制回调被延迟。记住,不要在输入回调里做任何耗时操作!
3. Measure:到底有多大?
Measure阶段的目标是确定每个View的宽高。它从ViewRootImpl的performMeasure()开始,传入两个MeasureSpec参数:一个代表宽度约束,一个代表高度约束。
MeasureSpec由两部分组成:模式(Mode)和大小(Size)。模式有三种:
| 模式 | 含义 | 典型场景 |
|---|---|---|
| EXACTLY | 精确大小,父View已经确定了子View的尺寸 | match_parent 或固定dp值 |
| AT_MOST | 最大大小,子View不能超过这个值 | wrap_content |
| UNSPECIFIED | 无限制,子View想要多大就多大 | ScrollView、ListView内部 |
每个View的onMeasure()方法会接收父View传递的MeasureSpec,然后计算出自己的宽高,最后调用setMeasuredDimension()保存结果。
// 自定义View的onMeasure示例
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width, height;
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
} else {
width = 200; // 默认宽度
if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(width, widthSize);
}
}
// 高度同理...
setMeasuredDimension(width, height);
}
注意:Measure可能会执行多次!比如当父View是LinearLayout时,它会先测量一次子View,然后根据权重重新分配尺寸,再测量一次。这就是为什么onMeasure()里不要做耗时操作的原因。
4. Layout:放在哪里?
Layout阶段的目标是确定每个View在父容器中的位置。它从ViewRootImpl的performLayout()开始,传入四个参数:left、top、right、bottom。
每个ViewGroup的onLayout()方法负责遍历子View,调用子View的layout()方法,设置子View的坐标。
// 自定义ViewGroup的onLayout示例
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
int currentTop = t;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
int childHeight = child.getMeasuredHeight();
// 垂直排列子View
child.layout(l, currentTop, r, currentTop + childHeight);
currentTop += childHeight;
}
}
嗯,这里有个坑:layout()方法内部会调用onLayout(),但也会触发一些回调,比如onSizeChanged()。如果你在onSizeChanged()里做了requestLayout(),就可能陷入无限循环。我曾经就踩过这个坑,导致界面卡死。
避坑指南:不要在onLayout()里修改子View的MeasureSpec或请求重新测量。如果你需要动态调整子View尺寸,应该在onMeasure()阶段就处理好,而不是在布局阶段临时改变。
5. Draw:画出来!
Draw阶段的目标是把View的内容绘制到Canvas上。它从ViewRootImpl的performDraw()开始,最终调用View的draw()方法。
draw()方法的执行顺序是固定的:
- 绘制背景(drawBackground)
- 绘制自身内容(onDraw)
- 绘制子View(dispatchDraw)
- 绘制前景(onDrawForeground)
- 绘制装饰(比如滚动条)
// View.java (简化版)
public void draw(Canvas canvas) {
drawBackground(canvas);
onDraw(canvas);
dispatchDraw(canvas);
onDrawForeground(canvas);
// 绘制装饰...
}
我个人习惯在onDraw()里尽量少创建对象,尤其是Bitmap和Path。因为onDraw()每帧都可能被调用,频繁创建对象会导致GC频繁,引起掉帧。
我的经验:有一次我优化一个自定义进度条,发现onDraw()里每次都会new一个Paint对象。改成成员变量后,帧率从45fps提升到了60fps。你想想看,一个Paint对象才多大?但架不住每帧都new啊。
6. 三大流程的协作:一张图看懂
下面我用一张SVG图来展示Measure、Layout、Draw三大流程与ViewRootImpl、Choreographer的关系。这张图我画了很多遍,希望能帮你建立整体认知。
7. 总结:记住这三句话
好了,今天的内容就到这里。我帮你总结三句口诀:
- Measure:父View给子View下约束,子View自己算大小。
- Layout:父View给子View定位置,子View自己存坐标。
- Draw:View自己画内容,Canvas是画板。
而ViewRootImpl和Choreographer,就是保证这三步能按时按序执行的“总导演”和“节拍器”。
最后说一句:理解了这个流程,你就能明白为什么嵌套多层RelativeLayout会导致性能问题——因为Measure会从根节点递归遍历整棵树,层级越深,耗时越长。下次写布局时,想想今天讲的MeasureSpec和递归遍历,也许你就能做出更好的选择。