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的工作流程大致如下:

  1. 应用注册一个回调(比如动画回调、绘制回调)。
  2. Choreographer等待下一个Vsync信号。
  3. Vsync到来时,Choreographer按优先级依次执行回调:输入处理、动画、遍历绘制。
  4. 如果所有回调在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()方法的执行顺序是固定的:

  1. 绘制背景(drawBackground)
  2. 绘制自身内容(onDraw)
  3. 绘制子View(dispatchDraw)
  4. 绘制前景(onDrawForeground)
  5. 绘制装饰(比如滚动条)
// 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的关系。这张图我画了很多遍,希望能帮你建立整体认知。

View绘制三大流程与核心组件协作图 Choreographer Vsync信号 ViewRootImpl performTraversals() performMeasure() performLayout() performDraw() onMeasure() onLayout() onDraw() dispatchDraw() 由Choreographer驱动 → ViewRootImpl调度 → 依次执行Measure/Layout/Draw 每个阶段都对应View/ViewGroup的回调方法

7. 总结:记住这三句话

好了,今天的内容就到这里。我帮你总结三句口诀:

  • Measure:父View给子View下约束,子View自己算大小。
  • Layout:父View给子View定位置,子View自己存坐标。
  • Draw:View自己画内容,Canvas是画板。

而ViewRootImpl和Choreographer,就是保证这三步能按时按序执行的“总导演”和“节拍器”。

最后说一句:理解了这个流程,你就能明白为什么嵌套多层RelativeLayout会导致性能问题——因为Measure会从根节点递归遍历整棵树,层级越深,耗时越长。下次写布局时,想想今天讲的MeasureSpec和递归遍历,也许你就能做出更好的选择。

公众号:蓝海资料掘金营,微信deep3321