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异步执行

相关推荐
鹏多多.40 分钟前
flutter-使用AnimatedDefaultTextStyle实现文本动画
android·前端·css·flutter·ios·html5·web
似霰2 小时前
安卓系统属性之androidboot.xxx转换成ro.boot.xxx
android·gitee
0wioiw02 小时前
Android-Kotlin基础(Jetpack①-ViewModel)
android
用户2018792831673 小时前
限定参数范围的注解之 "咖啡店定价" 的故事
android·java
泓博3 小时前
Android底部导航栏图标变黑色
android
包达叔3 小时前
使用 Tauri 开发 Android 应用:环境搭建与入门指南
android
初学者-Study3 小时前
Android UI(一)登录注册
android·ui
视觉CG3 小时前
【JS】扁平树数据转为树结构
android·java·javascript
深盾安全4 小时前
Android 安全编程:Kotlin 如何从语言层保障安全性
android