Android中视图测量、布局、绘制过程

一、测量阶段(Measure)

1. MeasureSpec 机制详解
java 复制代码
// MeasureSpec结构(32位int)
int spec = (mode << 30) | size;
  • 三种模式

    模式 触发场景 特点
    UNSPECIFIED 0 ScrollView/RecyclerView子View 父容器不限制子View尺寸
    EXACTLY 1 match_parent/固定值(dp) 子View必须使用指定尺寸
    AT_MOST 2 wrap_content 子View尺寸不超过指定值
  • 生成规则 (核心方法 ViewGroup.getChildMeasureSpec()):

    java 复制代码
    public static int getChildMeasureSpec(int parentSpec, int padding, int childDimension) {
        int parentMode = MeasureSpec.getMode(parentSpec);
        int parentSize = MeasureSpec.getSize(parentSpec);
        int size = Math.max(0, parentSize - padding);
    
        int resultSize = 0;
        int resultMode = 0;
    
        switch (parentMode) {
            case MeasureSpec.EXACTLY:  // 父容器有确定尺寸
                if (childDimension >= 0) {       // 子View固定尺寸
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.MATCH_PARENT) {
                    resultSize = size;            // 子View填满父容器
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    resultSize = size;            // 子View尺寸不超过父容器
                    resultMode = MeasureSpec.AT_MOST;
                }
                break;
            case MeasureSpec.AT_MOST:  // 父容器尺寸不确定
                if (childDimension >= 0) {
                    resultSize = childDimension;  // 子View固定尺寸优先
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.MATCH_PARENT) {
                    resultSize = size;            // 子View尺寸不超过父容器
                    resultMode = MeasureSpec.AT_MOST;
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    resultSize = size;             // 同上
                    resultMode = MeasureSpec.AT_MOST;
                }
                break;
            case MeasureSpec.UNSPECIFIED:  // 父容器无限制
                if (childDimension >= 0) {
                    resultSize = childDimension;  // 子View固定尺寸
                    resultMode = MeasureSpec.EXACTLY;
                } else {
                    resultSize = 0;               // 子View可任意尺寸
                    resultMode = MeasureSpec.UNSPECIFIED;
                }
                break;
        }
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
2. 测量流程源码级分析

关键步骤:

  1. ViewRootImpl 触发 performTraversals()

  2. DecorView 开始测量:

    • 调用 measure()onMeasure()

    • 遍历子View(ViewGroup)并调用其 measure()

  3. ViewGroup 测量逻辑:

    • 通过 measureChildWithMargins() 为子View生成MeasureSpec

    • 调用子View的 measure() 方法

  4. 子View 响应测量:

    • onMeasure() 中计算自身尺寸

    • 必须调用 setMeasuredDimension() 保存结果

3. 高频问题:为何 onMeasure() 被多次调用?
调用场景 触发原因 调用次数 源码定位
常规Activity Surface创建流程 2次 ViewRootImpl#measureHierarchy() → performMeasure() relayoutWindow()后再次performMeasure()
Dialog主题 宽度自适应尝试 最多6次 measureHierarchy()中三次测量尝试: 1. 预设宽度(baseSize) 2. 折中宽度((baseSize+desiredWidth)/2) 3. 全屏宽度
权重布局 LinearLayout权重计算 +1次 LinearLayout#measureVertical()中二次测量带权重子View
滑动容器 RecyclerView预加载 动态增加 RecyclerView#onMeasure()中预测量屏幕外item

常见问题:

Q1:描述MeasureSpec的作用和组成?
A:

MeasureSpec是32位int值,包含:

  1. 模式(高2位)

    • UNSPECIFIED:父容器不限制子View尺寸(如ScrollView子View)

    • EXACTLY:子View必须使用精确尺寸(match_parent/固定值)

    • AT_MOST:子View尺寸不超过设定值(wrap_content)

  2. 尺寸(低30位):父容器提供的可用空间

  3. 核心作用:父容器通过MeasureSpec向子View传递布局约束条件


Q2:自定义View时onMeasure()要注意什么?
A: 必须处理三点:

  1. UNSPECIFIED模式

    java 复制代码
    protected void onMeasure(int widthSpec, int heightSpec) {
        int width = resolveSize(minWidth, widthSpec); // 处理无限制情况
        int height = resolveSize(minHeight, heightSpec);
        setMeasuredDimension(width, height);
    }
  2. wrap_content支持

    java 复制代码
    if (widthMode == MeasureSpec.AT_MOST) {
        width = Math.min(desiredWidth, widthSize); // 限制在AT_MOST范围内
    }
  3. 调用setMeasuredDimension()

源码中使用的是**setMeasuredDimension(getDefaultSizexxx),**而对于getDefaultSize,对于AT_MOST以及EXACTLY的情况,返回大小事一样的,如果自定义View,需要重写onMeasure方法,对Wrap_content情况进行处理


Q3:ScrollView子View的测量为何特殊?
A: 源码强制修改高度模式为UNSPECIFIED:

java 复制代码
// frameworks/base/core/java/android/widget/ScrollView.java
protected void measureChild(View child, int parentWidthSpec, int parentHeightSpec) {
    int childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
    child.measure(childWidthMeasureSpec, childHeightSpec);
}

结果 :无论子View设置wrap_contentmatch_parent,实际高度均为内容高度(可超过屏幕),通过滚动查看完整内容。


Q4:ViewGroup如何测量子View?
A: 关键三步:

  1. 计算可用空间

    java 复制代码
    int availableWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
  2. 生成子View MeasureSpec

    java 复制代码
    int childWidthSpec = getChildMeasureSpec(parentWidthSpec, 
                                          paddingLeft + paddingRight, 
                                          lp.width);
  3. 触发子View测量

    java 复制代码
    child.measure(childWidthSpec, childHeightSpec);
    // 测量后通过child.getMeasuredWidth()获取结果

Q5:解释ViewRootImpl的测量触发链
A: 核心链路:

  • performTraversals() 是三大流程的总入口

  • measureHierarchy() 处理Dialog等自适应布局的多次测量

  • relayoutWindow() 在测量后申请Surface,触发二次测量


Q6:根视图MeasureSpec如何得到

A:

观察performTraversals()方法可以发现如下代码:

java 复制代码
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);

可以看到,这里调用了getRootMeasureSpec()方法去获取widthMeasureSpec和heightMeasureSpec的值,注意方法中传入的参数,其中lp.width和lp.height在创建ViewGroup实例的时候就被赋值了,它们都等于MATCH_PARENT。然后看下getRootMeasureSpec()方法中的代码,如下所示:

java 复制代码
private int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {
    case ViewGroup.LayoutParams.MATCH_PARENT:
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
        break;
    case ViewGroup.LayoutParams.WRAP_CONTENT:
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
        break;
    default:
        measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
        break;
    }
    return measureSpec;
}

可以看到,这里使用了MeasureSpec.makeMeasureSpec()方法来组装一个MeasureSpec,当rootDimension参数等于MATCH_PARENT的时候,MeasureSpec的specMode就等于EXACTLY,当rootDimension等于WRAP_CONTENT的时候,MeasureSpec的specMode就等于AT_MOST。并且MATCH_PARENT和WRAP_CONTENT时的specSize都是等于windowSize的,也就意味着根视图总是会充满全屏的。


二、布局阶段(Layout)

核心流程
关键知识点
  1. LayoutParams的核心作用

    • 存储View的布局参数(宽/高、margin等)

    • 自定义ViewGroup需重写generateLayoutParams()支持margin

  2. ViewGroup布局流程

    java 复制代码
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            // 1. 计算子View位置(根据业务逻辑)
            int left = calculateChildLeft();
            int top = calculateChildTop();
            // 2. 调用子View.layout
            child.layout(left, top, left + width, top + height);
        }
    }
  3. 位置计算要点

    • 基于getMeasuredWidth/Height(测量阶段结果)

    • 需处理padding(父容器)和margin(子View)


三、绘制阶段(Draw)

核心流程
关键知识点
  1. 绘制顺序(软件绘制)

    java 复制代码
    public void draw(Canvas canvas) {
        drawBackground(canvas);    // 1. 绘制背景
        onDraw(canvas);            // 2. 绘制自身内容
        dispatchDraw(canvas);      // 3. 绘制子View(ViewGroup实现)
        onDrawForeground(canvas);  // 4. 绘制前景/滚动条
    }
  2. 绘制阶段:一是绘制流程执行的顺序,测量和布局阶段都是先执行子 View 再执行 ViewGroup 自身,而绘制是先执行 ViewGroup 绘制流程,再执行子 View 的绘制流程.View 的绘制流程和 ViewGroup 的绘制流程几乎一模一样,唯一的区别是 View 中的 dispatchDraw() 是空实现,因为它没有子视图.

  3. 硬件加速本质

    • DisplayListCanvas:在UI线程记录绘制指令

    • RenderThread:在渲染线程执行GPU绘图

    • 优势:避免重复录制指令(如View未失效时复用DisplayList)

  4. 优化点

    • 避免在onDraw中创建对象(引发GC)

    • 使用canvas.clipRect()减少过度绘制


总结

Q:简述自定义View的三大流程及核心方法?
A:

  1. Measure(测量)

    • 目标:确定View的宽高

    • 关键方法:onMeasure(int widthMeasureSpec, int heightMeasureSpec)

    • 核心机制:父容器通过MeasureSpec传递约束条件,子View调用setMeasuredDimension()保存结果

  2. Layout(布局)

    • 目标:确定View的位置(四个顶点坐标)

    • 关键方法:onLayout(boolean changed, int l, int t, int r, int b)

    • 核心机制:父容器遍历子View并调用其layout(),子View通过setFrame()保存坐标

  3. Draw(绘制)

    • 目标:将View绘制到屏幕

    • 关键方法:onDraw(Canvas canvas)

    • 执行顺序:背景 → 自身内容 → 子View → 前景

    • 硬件加速:通过DisplayListCanvas录制指令,由RenderThread异步执行

相关推荐
Kapaseker4 小时前
Compose 进阶—巧用 GraphicsLayer
android·kotlin
黄林晴5 小时前
Android17 为什么重写 MessageQueue
android
阿巴斯甜1 天前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker1 天前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95271 天前
Andorid Google 登录接入文档
android
黄林晴1 天前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab2 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿2 天前
Android MediaPlayer 笔记
android
Jony_2 天前
Android 启动优化方案
android
阿巴斯甜2 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android