编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • Android提升进阶

    • 库的解读

    • 专栏博客

      • 系统启动Zygote
      • Binder通信原理
      • Handler消息机制
      • Activity启动原理
      • 四大组件原理分析
      • AMS与组件管理
      • View绑制与渲染
        • 一、引言:从setContentView到像素显示
        • 二、View树与ViewRootImpl
          • 2.1 View层级结构
          • 2.2 ViewRootImpl的角色
          • 2.3 requestLayout触发绘制
        • 三、Measure过程深度剖析
          • 3.1 performMeasure入口
          • 3.2 View的onMeasure默认实现
          • 3.3 ViewGroup的测量——以LinearLayout为例
        • 四、Layout过程深度剖析
          • 4.1 performLayout入口
          • 4.2 View.layout()
          • 4.3 ViewGroup的布局
        • 五、Draw过程深度剖析
          • 5.1 performDraw入口
          • 5.2 View.draw()的六步绘制
          • 5.3 软件绘制路径
        • 六、MeasureSpec的设计原理
          • 6.1 MeasureSpec的位运算设计
          • 6.2 MeasureSpec的生成规则
        • 七、自定义View的绘制流程
          • 7.1 自定义View必须处理wrap_content
          • 7.2 View.post()获取宽高的原理
        • 八、硬件加速渲染原理
          • 8.1 软件渲染 vs 硬件加速
          • 8.2 RenderNode与DisplayList
        • 九、VSync与Choreographer
          • 9.1 VSync信号的作用
          • 9.2 Choreographer的工作机制
          • 9.3 掉帧的原因分析
        • 十、Surface与SurfaceFlinger
          • 10.1 Surface是什么
          • 10.2 BufferQueue:三重缓冲
          • 10.3 SurfaceFlinger的合成过程
        • 十一、RenderThread与GPU渲染
          • 11.1 RenderThread的工作流程
          • 11.2 GPU渲染管线
        • 十二、invalidate与requestLayout的区别
          • 12.1 invalidate的流程
          • 12.2 requestLayout的流程
          • 12.3 使用场景
        • 十三、布局优化与渲染性能
          • 13.1 过度绘制(Overdraw)
          • 13.2 布局层级优化
          • 13.3 GPU呈现模式分析
        • 十四、面试高频问题与深度分析
          • 14.1 View的绘制流程是从哪里开始的?
          • 14.2 为什么子线程不能更新UI?
          • 14.3 getWidth和getMeasuredWidth的区别?
        • 十五、MeasureSpec设计详解
          • 15.1 MeasureSpec意图设计
          • 15.2 测量的"递"和"归"思想
          • 15.3 单个控件测量流程
          • 15.4 完整View树测量流程
        • 十六、View刷新与显示
          • 16.1 View显示在屏幕的完整流程
          • 16.2 View生成图片原理
        • 十七、总结
      • 事件分发机制
      • Surface渲染原理
      • 自定义View设计
      • WMS窗口管理
      • PMS与APK安装
      • 虚拟机与类加载
      • 内存管理与GC
      • 线程与并发编程
      • 性能优化与监控
      • 序列化与数据存储
      • 组件化与路由设计
      • 插件化与热修复
      • NDK开发实践
      • WebView核心设计
      • ADB常见使用操作
    • 智能硬件

  • iOS开发和进阶

  • Web开发和进阶

  • Linux应用开发

  • Apps
  • Android提升进阶
  • 专栏博客
杨充
2026-04-14
目录

View绑制与渲染

# 07.View绑制与渲染

# 目录介绍

  • 一、引言:从setContentView到像素显示
  • 二、View树与ViewRootImpl
    • 2.1 View层级结构
    • 2.2 ViewRootImpl的角色
    • 2.3 requestLayout触发绘制
  • 三、Measure过程深度剖析
    • 3.1 performMeasure入口
    • 3.2 View的onMeasure默认实现
    • 3.3 ViewGroup的测量——以LinearLayout为例
  • 四、Layout过程深度剖析
    • 4.1 performLayout入口
    • 4.2 View.layout()
    • 4.3 ViewGroup的布局
  • 五、Draw过程深度剖析
    • 5.1 performDraw入口
    • 5.2 View.draw()的六步绘制
    • 5.3 软件绘制路径
  • 六、MeasureSpec的设计原理
    • 6.1 MeasureSpec的位运算设计
    • 6.2 MeasureSpec的生成规则
  • 七、自定义View的绘制流程
    • 7.1 自定义View必须处理wrap_content
    • 7.2 View.post()获取宽高的原理
  • 八、硬件加速渲染原理
    • 8.1 软件渲染 vs 硬件加速
    • 8.2 RenderNode与DisplayList
  • 九、VSync与Choreographer
    • 9.1 VSync信号的作用
    • 9.2 Choreographer的工作机制
    • 9.3 掉帧的原因分析
  • 十、Surface与SurfaceFlinger
    • 10.1 Surface是什么
    • 10.2 BufferQueue:三重缓冲
    • 10.3 SurfaceFlinger的合成过程
  • 十一、RenderThread与GPU渲染
    • 11.1 RenderThread的工作流程
    • 11.2 GPU渲染管线
  • 十二、invalidate与requestLayout的区别
    • 12.1 invalidate的流程
    • 12.2 requestLayout的流程
    • 12.3 使用场景
  • 十三、布局优化与渲染性能
    • 13.1 过度绘制(Overdraw)
    • 13.2 布局层级优化
    • 13.3 GPU呈现模式分析
  • 十四、面试高频问题与深度分析
    • 14.1 View的绘制流程是从哪里开始的?
    • 14.2 为什么子线程不能更新UI?
    • 14.3 getWidth和getMeasuredWidth的区别?
  • 十五、MeasureSpec设计详解
    • 15.1 MeasureSpec意图设计
    • 15.2 测量的"递"和"归"思想
    • 15.3 单个控件测量流程
    • 15.4 完整View树测量流程
  • 十六、View刷新与显示
    • 16.1 View显示在屏幕的完整流程
    • 16.2 View生成图片原理
  • 十七、总结

# 一、引言:从setContentView到像素显示

当我们在Activity的onCreate中调用setContentView(R.layout.activity_main)时,从布局XML被解析到屏幕上出现像素,中间经历了复杂而精密的过程。

疑惑:为什么在onCreate中调用view.getWidth()返回0?为什么自定义View要经过measure-layout-draw三个阶段?

本文将从ViewRootImpl触发绘制开始,逐层深入到Surface、SurfaceFlinger、GPU等底层渲染机制。


# 二、View树与ViewRootImpl

# 2.1 View层级结构

Activity的View层级:

ViewRootImpl(不是View,是View树的管理者)
  └── DecorView(FrameLayout子类)
        ├── StatusBarBackground
        ├── NavigationBarBackground
        └── LinearLayout
              ├── ActionBarContainer
              └── FrameLayout (android.R.id.content)
                    └── 用户布局根View
                          ├── TextView
                          ├── ImageView
                          └── LinearLayout
                                ├── Button
                                └── EditText
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 2.2 ViewRootImpl的角色

// ViewRootImpl是View树与WindowManagerService之间的桥梁
// 职责:
// 1. 触发View树的measure/layout/draw
// 2. 管理Surface(绘制目标)
// 3. 处理输入事件分发
// 4. 管理与WMS的通信

// 创建时机:WindowManagerGlobal.addView()
public void addView(View view, ViewGroup.LayoutParams params, ...) {
    ViewRootImpl root = new ViewRootImpl(view.getContext(), display);
    root.setView(view, wparams, panelParentView, userId);
}

// ViewRootImpl.setView() → requestLayout()
public void setView(View view, WindowManager.LayoutParams attrs, ...) {
    mView = view;  // DecorView
    requestLayout();  // 触发首次绘制
    // addToDisplayAsUser → 通知WMS添加窗口
    res = mWindowSession.addToDisplayAsUser(mWindow, ...);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 2.3 requestLayout触发绘制

// ViewRootImpl.java
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();  // 检查是否在创建ViewRootImpl的线程
        mLayoutRequested = true;
        scheduleTraversals();  // 安排遍历
    }
}

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        
        // 1. 设置同步屏障(保证绘制消息优先处理)
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        
        // 2. 注册VSync回调
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL,
                mTraversalRunnable,  // VSync到来时执行
                null);
    }
}

// VSync信号到来后执行
final class TraversalRunnable implements Runnable {
    public void run() {
        doTraversal();
    }
}

void doTraversal() {
    mTraversalScheduled = false;
    // 移除同步屏障
    mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
    // 执行measure/layout/draw三大流程
    performTraversals();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

# 三、Measure过程深度剖析

# 3.1 performMeasure入口

// ViewRootImpl.performTraversals() 中
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

// View.measure() — final方法,不可重写
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    // 如果MeasureSpec没变且已经测量过,跳过
    if (forceLayout || needsLayout) {
        int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
        
        if (cacheIndex < 0 || sIgnoreMeasureCache) {
            onMeasure(widthMeasureSpec, heightMeasureSpec);  // 实际测量
        } else {
            long value = mMeasureCache.valueAt(cacheIndex);  // 使用缓存
            setMeasuredDimensionRaw((int)(value >> 32), (int)value);
        }
    }
    
    mOldWidthMeasureSpec = widthMeasureSpec;
    mOldHeightMeasureSpec = heightMeasureSpec;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 3.2 View的onMeasure默认实现

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;   // 使用父容器给的大小
        break;
    }
    return result;
}
// 注意:AT_MOST和EXACTLY返回相同值
// 这就是为什么自定义View必须重写onMeasure处理wrap_content
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 3.3 ViewGroup的测量——以LinearLayout为例

// LinearLayout.onMeasure() 简化逻辑
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
    int totalLength = 0;  // 总高度
    int totalWeight = 0;  // 总权重
    
    // 第一次测量:测量非weight子View
    for (int i = 0; i < count; i++) {
        final View child = getVirtualChildAt(i);
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        
        if (lp.weight > 0) {
            totalWeight += lp.weight;
            if (lp.height == 0) {
                continue;  // weight子View第一次不测量
            }
        }
        
        measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                heightMeasureSpec, totalLength);
        totalLength += child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
    }
    
    // 第二次测量:分配剩余空间给weight子View
    if (totalWeight > 0) {
        int remainingSpace = heightSize - totalLength;
        for (int i = 0; i < count; i++) {
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            if (lp.weight > 0) {
                int share = (int)(lp.weight * remainingSpace / totalWeight);
                int childHeight = Math.max(0, share);
                child.measure(
                        childWidthMeasureSpec,
                        MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

# 四、Layout过程深度剖析

# 4.1 performLayout入口

// ViewRootImpl.java
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
        int desiredWindowHeight) {
    final View host = mView;  // DecorView
    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
}
1
2
3
4
5
6

# 4.2 View.layout()

public void layout(int l, int t, int r, int b) {
    // 记录旧位置
    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;
    
    // 设置新位置
    boolean changed = setFrame(l, t, r, b);
    // setFrame内部:mLeft=l, mTop=t, mRight=r, mBottom=b
    // 同时计算:mWidth = r - l, mHeight = b - t
    
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) != 0) {
        onLayout(changed, l, t, r, b);  // 子类实现
    }
}

// View.getWidth() = mRight - mLeft
// 这就是为什么在onCreate中getWidth()=0:layout还未执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 4.3 ViewGroup的布局

// FrameLayout.onLayout() 简化
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        if (child.getVisibility() != GONE) {
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            
            // 根据gravity计算子View位置
            int childLeft, childTop;
            switch (gravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                case Gravity.CENTER_HORIZONTAL:
                    childLeft = (right - left - child.getMeasuredWidth()) / 2;
                    break;
                default:
                    childLeft = lp.leftMargin;
            }
            
            child.layout(childLeft, childTop,
                    childLeft + child.getMeasuredWidth(),
                    childTop + child.getMeasuredHeight());
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 五、Draw过程深度剖析

# 5.1 performDraw入口

// ViewRootImpl.java
private boolean performDraw() {
    // 选择绘制方式
    if (mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled()) {
        // 硬件加速绘制路径
        mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
    } else {
        // 软件绘制路径
        drawSoftware(surface, mAttachInfo, ...);
    }
}
1
2
3
4
5
6
7
8
9
10
11

# 5.2 View.draw()的六步绘制

// View.draw() — 绘制的核心流程
public void draw(Canvas canvas) {
    // Step 1: 绘制背景
    drawBackground(canvas);
    
    // Step 2: 保存画布层(如果需要渐变边缘效果)
    // 通常跳过
    
    // Step 3: 绘制自身内容
    onDraw(canvas);  // 子类重写这个方法绘制自己的内容
    
    // Step 4: 绘制子View
    dispatchDraw(canvas);  // ViewGroup重写此方法遍历绘制子View
    
    // Step 5: 绘制渐变边缘效果和滚动条
    // 通常跳过
    
    // Step 6: 绘制前景和滚动指示器
    onDrawForeground(canvas);
    drawDefaultFocusHighlight(canvas);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 5.3 软件绘制路径

// ViewRootImpl.drawSoftware()
private boolean drawSoftware(Surface surface, ...) {
    // 1. 锁定Canvas(从Surface获取绘制缓冲区)
    Canvas canvas = mSurface.lockCanvas(dirty);
    // 底层:通过GraphicBuffer获取一块内存区域用于绘制
    
    try {
        // 2. 清除dirty区域
        canvas.drawColor(0, PorterDuff.Mode.CLEAR);
        
        // 3. 从DecorView开始递归绘制
        mView.draw(canvas);
    } finally {
        // 4. 解锁并提交
        surface.unlockCanvasAndPost(canvas);
        // 底层:将绘制好的GraphicBuffer提交给SurfaceFlinger合成
    }
    
    return true;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 六、MeasureSpec的设计原理

# 6.1 MeasureSpec的位运算设计

// MeasureSpec用一个int值同时编码mode和size
// 高2位 = mode,低30位 = size

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;  // 00 + 30位size
    public static final int EXACTLY     = 1 << MODE_SHIFT;  // 01 + 30位size
    public static final int AT_MOST     = 2 << MODE_SHIFT;  // 10 + 30位size
    
    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);
    }
}

// 设计思想:用一个int传递两个信息,减少方法参数和对象创建
// 30位size最大值 = 2^30 - 1 = 1073741823 ≈ 10亿像素
// 远超任何屏幕尺寸,所以30位足够
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 6.2 MeasureSpec的生成规则

父View的MeasureSpec + 子View的LayoutParams → 子View的MeasureSpec

┌─────────────────┬───────────────┬──────────────┬──────────────┐
│                  │ 子View:       │ 子View:      │ 子View:      │
│ 父View Mode     │ match_parent  │ wrap_content │ 固定尺寸dp   │
├─────────────────┼───────────────┼──────────────┼──────────────┤
│ EXACTLY (精确值) │ EXACTLY       │ AT_MOST      │ EXACTLY      │
│ 如200dp          │ size=父size   │ size=父size  │ size=子size  │
├─────────────────┼───────────────┼──────────────┼──────────────┤
│ AT_MOST (最大值) │ AT_MOST       │ AT_MOST      │ EXACTLY      │
│ 如wrap_content   │ size=父size   │ size=父size  │ size=子size  │
├─────────────────┼───────────────┼──────────────┼──────────────┤
│ UNSPECIFIED      │ UNSPECIFIED   │ UNSPECIFIED  │ EXACTLY      │
│ 如ScrollView内   │ size=0        │ size=0       │ size=子size  │
└─────────────────┴───────────────┴──────────────┴──────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 七、自定义View的绘制流程

# 7.1 自定义View必须处理wrap_content

// 错误:不处理wrap_content
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    // 问题:wrap_content和match_parent效果相同
}

// 正确:区分AT_MOST
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 = calculateContentWidth();  // 计算内容实际宽度
        if (widthMode == MeasureSpec.AT_MOST) {
            width = Math.min(width, widthSize);  // 不超过父容器限制
        }
    }
    // height同理
    
    setMeasuredDimension(width, height);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 7.2 View.post()获取宽高的原理

// 在onCreate中获取View宽高的正确方式
view.post(new Runnable() {
    @Override
    public void run() {
        int width = view.getWidth();   // 此时已有值
        int height = view.getHeight();
    }
});

// 原理:
// 1. View.post()在View未attach时,将Runnable保存到HandlerActionQueue
// 2. View.dispatchAttachedToWindow()时,将队列中的Runnable post到Handler
// 3. 此时measure/layout已执行完毕
// 4. Runnable在主线程消息队列中执行,此时getWidth()已有值

// 源码路径:
// View.post(runnable)
//   → if (attachInfo != null) { attachInfo.mHandler.post(runnable); }
//   → else { getRunQueue().post(runnable); } // 暂存
// dispatchAttachedToWindow() → getRunQueue().executeActions(handler);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 八、硬件加速渲染原理

# 8.1 软件渲染 vs 硬件加速

软件渲染(Software Rendering):
CPU负责所有绘制操作
View.draw() → Canvas → Skia → 写入GraphicBuffer(CPU内存)
└── 单线程,主线程执行

硬件加速(Hardware Accelerated Rendering):
CPU记录绘制命令,GPU执行实际渲染
View.draw() → DisplayListCanvas → 记录DrawOp
  └── RenderThread → OpenGL/Vulkan → GPU → 写入GraphicBuffer
      └── 独立线程,不阻塞主线程

硬件加速的优势:
1. GPU并行计算能力强(数千个核心同时工作)
2. RenderThread独立于主线程(减少主线程负担)
3. DisplayList可以缓存和复用(避免重复构建绘制命令)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 8.2 RenderNode与DisplayList

// 每个View都有一个RenderNode
// RenderNode内部维护一个DisplayList(绘制指令列表)

// View.updateDisplayListIfDirty()
public RenderNode updateDisplayListIfDirty() {
    final RenderNode renderNode = mRenderNode;
    
    if (needsUpdate) {
        // 开始录制绘制指令
        RecordingCanvas canvas = renderNode.beginRecording(width, height);
        try {
            // 调用draw(),但此时Canvas是RecordingCanvas
            // 所有绘制操作被记录而非直接执行
            draw(canvas);
            // canvas.drawRect() → 记录DrawRectOp
            // canvas.drawText() → 记录DrawTextOp
            // canvas.drawBitmap() → 记录DrawBitmapOp
        } finally {
            renderNode.endRecording();
        }
    }
    
    return renderNode;
}

// DisplayList结构示意:
// RenderNode(DecorView) {
//   DrawRectOp(0,0,1080,1920, paint=white)  // 背景
//   DrawRenderNodeOp → RenderNode(LinearLayout) {
//     DrawRenderNodeOp → RenderNode(TextView) {
//       DrawTextOp("Hello", 100, 50, paint)
//     }
//     DrawRenderNodeOp → RenderNode(ImageView) {
//       DrawBitmapOp(bitmap, 0, 100)
//     }
//   }
// }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

# 九、VSync与Choreographer

# 9.1 VSync信号的作用

没有VSync时的问题 — 画面撕裂(Tearing):

显示器刷新  ─────────────────────────────────→
            │← 帧1 →│← 帧2 →│
GPU渲染    ────────────────────────────────→
              │← 帧A ──→│← 帧B ─→│
                         ↑ GPU正在写入帧B时,显示器读取了半帧A+半帧B
                           → 画面撕裂!

有VSync后:
VSync信号  ──┤────────┤────────┤────────┤──→
              ↑ 通知开始绘制下一帧
显示器刷新  ──│← 帧1 →│← 帧2 →│← 帧3 →│──→
GPU渲染    ──│← 帧A →│← 帧B →│← 帧C →│──→
              同步!无撕裂
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 9.2 Choreographer的工作机制

// Choreographer是Android UI渲染的"指挥家"
// 它协调输入、动画、绘制三者的执行时机

public final class Choreographer {
    // 回调类型(按优先级排序)
    public static final int CALLBACK_INPUT = 0;       // 输入事件
    public static final int CALLBACK_ANIMATION = 1;   // 动画
    public static final int CALLBACK_INSETS_ANIMATION = 2;
    public static final int CALLBACK_TRAVERSAL = 3;   // View遍历(measure/layout/draw)
    public static final int CALLBACK_COMMIT = 4;      // 帧提交
    
    // VSync回调
    private final class FrameDisplayEventReceiver extends DisplayEventReceiver {
        @Override
        public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
            // VSync信号到来
            mTimestampNanos = timestampNanos;
            Message msg = Message.obtain(mHandler, this);
            msg.setAsynchronous(true);  // 异步消息,不受同步屏障影响
            mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
        }
        
        @Override
        public void run() {
            doFrame(mTimestampNanos, mFrame);
        }
    }
    
    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);
        doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
    }
}

// 一帧的完整流程(16.6ms @60fps):
// VSync → Input处理 → Animation计算 → Traversal(measure/layout/draw)
//       → RenderThread → GPU渲染 → SurfaceFlinger合成 → 显示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

# 9.3 掉帧的原因分析

正常帧(16.6ms内完成):
VSync0 ──── Input → Animation → Traversal → Render ──── VSync1
         ← 16.6ms →

掉帧(超过16.6ms):
VSync0 ──── Input → Animation → [耗时操作] → Traversal → Render ──── VSync2
         ← 33.2ms(丢了1帧)→
         VSync1时没有新帧,重复显示旧帧

常见掉帧原因:
1. 主线程耗时操作(IO、计算)→ Traversal被延迟
2. 布局太复杂 → measure/layout耗时长
3. 过度绘制 → draw耗时长
4. GC停顿 → 所有线程暂停
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 十、Surface与SurfaceFlinger

# 10.1 Surface是什么

Surface的本质:一块图形缓冲区(GraphicBuffer)的管理者

┌──────────────────────────────────────────────┐
│  App进程                                      │
│  ┌──────────┐                                │
│  │ Surface   │ → 管理BufferQueue的生产者端    │
│  │           │ → dequeueBuffer() 获取空缓冲区 │
│  │           │ → 绘制内容到缓冲区              │
│  │           │ → queueBuffer() 提交已绘制缓冲区│
│  └──────────┘                                │
└──────────────────────────────────────────────┘
              │ BufferQueue
              ↓
┌──────────────────────────────────────────────┐
│  SurfaceFlinger进程                           │
│  ┌──────────┐                                │
│  │ Layer     │ → 管理BufferQueue的消费者端    │
│  │           │ → acquireBuffer() 获取已绘制缓冲区│
│  │           │ → 合成所有Layer                 │
│  │           │ → 输出到显示设备                 │
│  └──────────┘                                │
└──────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 10.2 BufferQueue:三重缓冲

BufferQueue的三重缓冲机制:

┌─────────┐   ┌─────────┐   ┌─────────┐
│ Buffer0  │   │ Buffer1  │   │ Buffer2  │
│ (Front)  │   │ (Back)   │   │ (Free)   │
│ 正在显示  │   │ 正在绘制  │   │ 空闲     │
└─────────┘   └─────────┘   └─────────┘

一帧的Buffer流转:
1. App: dequeueBuffer() → 获取Free Buffer(Buffer2)
2. App: 在Buffer2上绘制
3. App: queueBuffer(Buffer2) → Buffer2变为Queued
4. SF:  acquireBuffer() → 获取Buffer2用于合成
5. SF:  合成完成 → 释放Buffer0(之前的Front)→ Buffer0变为Free
6. Buffer2变为新的Front Buffer

// 三重缓冲减少了"掉帧"概率:
// 双缓冲:如果渲染和合成都需要Buffer,只有2个可能都在用
// 三重缓冲:总有一个空闲Buffer可用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 10.3 SurfaceFlinger的合成过程

SurfaceFlinger合成多个Layer的过程:

┌─────────────┐  ┌─────────────┐  ┌─────────────┐
│ 状态栏Layer   │  │ 应用Layer    │  │ 导航栏Layer  │
│ (半透明)      │  │ (不透明)     │  │ (半透明)     │
└──────┬──────┘  └──────┬──────┘  └──────┬──────┘
       │                │                │
       └────────┬───────┘────────────────┘
                ↓
       ┌──────────────┐
       │ SurfaceFlinger │
       │ 合成策略:      │
       │ 1. GPU合成     │ → OpenGL ES混合各Layer
       │ 2. HWC合成     │ → 硬件合成器(更省电)
       └──────┬───────┘
              ↓
       ┌──────────────┐
       │  Display HAL  │ → 输出到物理屏幕
       └──────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 十一、RenderThread与GPU渲染

# 11.1 RenderThread的工作流程

主线程与RenderThread的协作:

主线程:                         RenderThread:
┌─────────────────────┐        ┌─────────────────────┐
│ 1. 构建/更新DisplayList │        │                     │
│    (View.draw录制)     │        │                     │
│ 2. syncFrameState()   │───────→│ 3. 接收DisplayList   │
│    同步渲染数据         │ 同步   │ 4. 转换为GPU指令     │
│                       │ 点     │    (DrawOp → GL Cmd) │
│ 3. 主线程继续处理      │        │ 5. 提交到GPU执行     │
│    下一帧的事务         │        │ 6. swapBuffers()     │
│                       │        │    提交帧到SF         │
└─────────────────────┘        └─────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13

# 11.2 GPU渲染管线

绘制指令到像素的GPU处理流程:

DrawOp (如drawRect)
  ↓
顶点着色器(Vertex Shader)
  → 处理矩形的4个顶点坐标
  → 应用变换矩阵(平移、旋转、缩放)
  ↓
光栅化(Rasterization)
  → 将矩形覆盖的区域转换为像素片段
  ↓
片段着色器(Fragment Shader)
  → 计算每个像素的最终颜色
  → 应用纹理采样、混合模式等
  ↓
帧缓冲(Framebuffer)
  → 像素写入GraphicBuffer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 十二、invalidate与requestLayout的区别

# 12.1 invalidate的流程

// invalidate:只触发draw,不触发measure和layout
public void invalidate() {
    invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, true, true);
}

void invalidateInternal(int l, int t, int r, int b, ...) {
    // 标记PFLAG_DIRTY
    mPrivateFlags |= PFLAG_DIRTY;
    
    // 向上传递dirty区域
    final ViewParent p = mParent;
    if (p != null) {
        p.invalidateChild(this, damage);
        // 层层向上 → 到ViewRootImpl
        // ViewRootImpl.invalidateChildInParent()
        //   → scheduleTraversals()
        //   → performTraversals()中只执行performDraw()
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 12.2 requestLayout的流程

// requestLayout:触发measure + layout + draw
public void requestLayout() {
    // 标记PFLAG_FORCE_LAYOUT
    mPrivateFlags |= PFLAG_FORCE_LAYOUT | PFLAG_INVALIDATED;
    
    if (mParent != null) {
        mParent.requestLayout();
        // 层层向上 → 到ViewRootImpl
        // ViewRootImpl.requestLayout()
        //   → scheduleTraversals()
        //   → performTraversals()中执行全部三个阶段
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 12.3 使用场景

invalidate():
→ View的内容变了(颜色、文字等),但大小位置不变
→ 例如:改变背景色、更新文字内容
→ 只需重新draw

requestLayout():
→ View的大小或位置可能变了
→ 例如:setText改变文字长度、setVisibility
→ 需要重新measure + layout + draw

postInvalidate():
→ 在子线程中调用invalidate
→ 内部通过Handler.post切到主线程
1
2
3
4
5
6
7
8
9
10
11
12
13

# 十三、布局优化与渲染性能

# 13.1 过度绘制(Overdraw)

过度绘制:同一个像素被绘制多次

示例:
Activity背景(白色)     → 第1次绘制
  LinearLayout背景(灰色) → 第2次绘制
    CardView背景(白色)    → 第3次绘制
      TextView文字          → 第4次绘制

这个像素被绘制了4次,其中前3次是浪费的

优化方案:
1. 移除不必要的背景
   → Activity主题设置:<item name="android:windowBackground">@null</item>
   → 移除ViewGroup的默认背景
   
2. clipRect裁剪
   → 自定义View中只绘制可见区域
   canvas.clipRect(visibleRect);
   canvas.drawBitmap(bitmap, 0, 0, paint);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 13.2 布局层级优化

减少布局层级的策略:

1. ConstraintLayout替代嵌套LinearLayout
   嵌套3层 → ConstraintLayout 1层
   measure次数从 O(2^n) 降为 O(n)

2. merge标签
   减少include引入的额外层级

3. ViewStub延迟加载
   不常显示的布局(错误页、空状态)使用ViewStub
   只在需要时才inflate,不占用measure/layout时间

4. 避免RelativeLayout嵌套
   RelativeLayout会触发两次measure
   嵌套两层 = 4次measure(指数增长)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 13.3 GPU呈现模式分析

开发者选项 → GPU呈现模式 → 柱状图:

每个竖条代表一帧的渲染时间,颜色代表各阶段:

绿色横线 = 16.6ms(60fps目标线)
超过绿线 = 掉帧

竖条颜色(从底到顶):
├── 蓝色:Draw时间(创建/更新DisplayList)
│   → 问题:onDraw()太复杂,Canvas操作太多
├── 紫色:同步上传(纹理上传到GPU)
│   → 问题:Bitmap太大
├── 红色:处理执行(GPU执行绘制命令)
│   → 问题:View树太深,过度绘制严重
├── 橙色:交换缓冲区
│   → 问题:合成负担太重
└── 黄色:其他(Input、Animation等)
    → 问题:主线程有耗时操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 十四、面试高频问题与深度分析

# 14.1 View的绘制流程是从哪里开始的?

完整链路:
ViewRootImpl.requestLayout()
  → scheduleTraversals()
    → mChoreographer.postCallback(CALLBACK_TRAVERSAL, mTraversalRunnable)
      → VSync信号到来
        → doTraversal()
          → performTraversals()
            → performMeasure() → View.measure() → onMeasure()
            → performLayout()  → View.layout()  → onLayout()
            → performDraw()    → View.draw()    → onDraw()
1
2
3
4
5
6
7
8
9
10

# 14.2 为什么子线程不能更新UI?

根本原因:ViewRootImpl.checkThread()

void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException("...");
    }
}

调用时机:requestLayout()、invalidate()等

但也有例外:
1. ViewRootImpl创建前(onResume之前)→ 无检查
2. SurfaceView → 专门支持子线程绘制
3. TextureView → 通过lockCanvas支持子线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 14.3 getWidth和getMeasuredWidth的区别?

getMeasuredWidth():
→ 在onMeasure()中由setMeasuredDimension()设置
→ 表示View测量后的宽度
→ 在measure后可用

getWidth():
→ 等于mRight - mLeft,在layout()中由setFrame()设置
→ 表示View最终在屏幕上的宽度
→ 在layout后可用

通常两者相等,但也可以不等:
→ 在layout()中故意设置不同的值
→ 例如:child.layout(0, 0, child.getMeasuredWidth() + 100, ...)
→ 此时 getWidth() = getMeasuredWidth() + 100
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 十五、MeasureSpec设计详解

# 15.1 MeasureSpec意图设计

测量流程中对布局的设计是通过MeasureSpec类来描述的。MeasureSpec包含两个属性:测量模式和测量大小。

MeasureSpec用一个32位int值来表示布局要求。前2位代表测量模式,后30位表示测量大小,通过位运算获取mode和size。

三种测量模式:

  • UNSPECIFIED:父元素不对子元素施加任何束缚,子元素可以得到任意想要的大小
  • EXACTLY:父元素决定子元素的确切大小(对应match_parent或指定dp/px)
  • AT_MOST:子元素至多达到指定大小的值(对应wrap_content)

# 15.2 测量的"递"和"归"思想

View测量流程使用了经典的递归思想:

递流程(自顶向下):

  1. 顶层父控件将布局要求(MeasureSpec)传递给子控件
  2. 子控件根据测量策略计算出自身的布局要求,再传递给下一级子控件
  3. 如此往复,直至最后一级子控件

归流程(自底向上):

  1. 最后一级子控件测量完毕后调用setMeasuredDimension()
  2. 父控件根据所有child的宽高数据进行聚合,计算出自身宽高
  3. 如此往复,直至完成顶层View的测量

关键理解:子控件的测量结果是由父控件和其本身共同决定的,父控件通过MeasureSpec将布局约束传递给子控件。

# 15.3 单个控件测量流程

单个View测量流程:

父控件调用 child.measure(widthSpec, heightSpec)
    → measure() [final方法,公共逻辑]
        → onMeasure() [开发者自定义测量策略]
            → getDefaultSize() [根据MeasureSpec计算默认大小]
            → setMeasuredDimension() [保存测量结果,标志测量完成]
1
2
3
4
5
6
7
  • measure():被配置了final修饰符,保证公共逻辑代码安全
  • onMeasure():暴露给开发者重写,自定义测量策略
  • setMeasuredDimension():将测量结果存储到mMeasuredWidth和mMeasuredHeight

# 15.4 完整View树测量流程

以竖直方向LinearLayout为例,测量策略为"遍历获取所有子控件,将高度累加":

// 简化版LinearLayout测量
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 1.遍历测量每个child(递流程)
    for (int i = 0; i < getChildCount(); i++) {
        View child = getChildAt(i);
        // 计算子控件的布局要求(考虑padding)
        measureChild(child, widthMeasureSpec, heightMeasureSpec);
    }
    // 2.累加高度(归流程)
    int height = 0;
    for (int i = 0; i < getChildCount(); i++) {
        height += child.getMeasuredHeight();
    }
    // 3.完成自身测量
    setMeasuredDimension(width, height);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

measureChild()方法的核心作用是:根据父布局的MeasureSpec和padding值,通过getChildMeasureSpec()计算出子控件的MeasureSpec,再让子控件根据新的布局要求进行测量。

# 十六、View刷新与显示

# 16.1 View显示在屏幕的完整流程

View显示在屏幕的完整流程:

Vsync调度 → 消息调度(doFrame) → input处理 → 动画处理
    → View三大流程(measure/layout/draw)
    → DisplayList更新 → OpenGL指令转换
    → 指令buffer交换 → GPU处理
    → Layer合成 → 光栅化 → Display → buffer切换

各阶段详细说明:

1. VSync调度
   Display硬件产生VSync信号(每16.6ms一次)
   → SurfaceFlinger分发给Choreographer
   → Choreographer触发doFrame回调

2. 消息调度
   Choreographer.doFrame()
   → 按优先级依次处理:Input → Animation → Traversal
   → Traversal回调中执行performTraversals()

3. View三大流程
   performTraversals()
   → performMeasure() → 自顶向下递归测量
   → performLayout() → 自顶向下递归布局
   → performDraw() → 构建/更新DisplayList

4. GPU渲染
   RenderThread接收DisplayList
   → 遍历RenderNode树
   → 转换为OpenGL/Vulkan绘制命令
   → GPU执行光栅化
   → 结果写入GraphicBuffer

5. 合成显示
   SurfaceFlinger接收所有App的GraphicBuffer
   → HWC(硬件合成器)合成所有Layer
   → 写入FrameBuffer
   → Display控制器读取FrameBuffer → 屏幕显示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

# 16.2 View生成图片原理

从普通View获取图像的核心API是view.getDrawingCache()(API 28已废弃),原理是:

  1. 根据View的宽高属性创建一个新的Bitmap
  2. 将新Bitmap设置给一个Canvas
  3. 调用源View的draw(canvas)方法,将图像绘制到新Bitmap上
  4. 保存新Bitmap即得到View的图像
View截图的多种方案对比:

1. getDrawingCache() [已废弃]
   view.setDrawingCacheEnabled(true);
   Bitmap bmp = view.getDrawingCache();
   → 缺点:Bitmap大小受drawingCacheSize限制
   → API 28标记为@Deprecated

2. Canvas直接绘制 [推荐]
   Bitmap bmp = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Config.ARGB_8888);
   Canvas canvas = new Canvas(bmp);
   view.draw(canvas);
   → 优点:无大小限制,兼容性好
   → 缺点:软件渲染,不包含硬件加速的绘制内容

3. PixelCopy [Android 7.1+, 推荐]
   PixelCopy.request(window, rect, bitmap, listener, handler);
   → 优点:可以截取硬件加速渲染的内容
   → 从SurfaceFlinger的合成结果中拷贝像素
   → 支持截取指定区域
   → 异步回调,不阻塞主线程

4. Surface截图
   Surface.lockHardwareCanvas()
   → 直接从GPU的Buffer中获取像素数据
   → 性能最好,但需要Surface引用

注意事项:
  SurfaceView/TextureView有独立Surface
  → Canvas方式无法截取其内容
  → 必须使用PixelCopy或Surface方案
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# 十七、总结

View绘制与渲染是Android UI框架的核心:

View渲染知识图谱:

绘制流程
├── ViewRootImpl → scheduleTraversals → performTraversals
├── Measure → MeasureSpec → onMeasure → setMeasuredDimension
├── Layout → onLayout → setFrame(l,t,r,b)
└── Draw → onDraw → Canvas绑定操作

渲染管线
├── Choreographer → VSync信号协调
├── 硬件加速 → RenderNode → DisplayList → RenderThread
├── Surface → BufferQueue → 三重缓冲
└── SurfaceFlinger → Layer合成 → 显示

性能优化
├── 减少布局层级 → ConstraintLayout
├── 减少过度绘制 → 移除不必要背景
├── invalidate vs requestLayout → 选择最小代价刷新
└── 硬件加速 → 充分利用GPU能力
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

理解View绘制原理,是做UI性能优化、自定义View开发、动画实现的基础。

上次更新: 2026/06/10, 11:13:41
AMS与组件管理
事件分发机制

← AMS与组件管理 事件分发机制→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式