当前位置: 首页 > news >正文

Android View的绘制原理详解

Android 中 View 的绘制原理是一个核心机制,它决定了 UI 如何从代码定义最终变成屏幕上可见的像素。整个过程是一个自顶向下递归遍历、测量、布局和绘制的过程,主要涉及三个核心阶段:测量(Measure)布局(Layout)绘制(Draw)。以下是详细描述:

核心参与者

  1. ViewRootImpl: 连接 WindowManagerDecorView 的纽带。它是整个 View 树绘制的发起者和总协调者。每个窗口(Activity/Dialog)对应一个 ViewRootImpl
  2. DecorView: 是窗口的顶级 View。它通常包含系统 UI(如状态栏背景、ActionBar/Toolbar)和开发者设置的 setContentView(R.layout.xxx) 的布局(作为其子 View,通常是 FrameLayoutLinearLayout)。
  3. View: 所有 UI 组件的基类。负责自身的测量、布局和绘制。
  4. ViewGroup: View 的子类,是容器,可以包含其他 ViewViewGroup(子 View)。它负责管理子 View,包括调用子 View 的测量、布局方法,并决定子 View 的位置和尺寸。

核心流程:三部曲

绘制过程由 ViewRootImplperformTraversals() 方法发起。这个方法会根据需要(如 View 请求刷新、窗口首次显示、窗口大小改变等)依次调用三个核心方法:

  1. 测量(Measure)

    • 目的:确定每个 ViewViewGroup尺寸(宽和高)
    • 发起者ViewRootImpl 调用 DecorViewmeasure() 方法开始整个 View 树的测量。
    • 关键方法
      • View.measure(int widthMeasureSpec, int heightMeasureSpec): 最终会调用 onMeasure(...)。View 的尺寸由其父 ViewGroup 传递下来的约束(MeasureSpec)和自身的需求共同决定。
      • View.onMeasure(int widthMeasureSpec, int heightMeasureSpec): 必须被每个 ViewViewGroup 子类重写。在此方法中,View 根据父 ViewGroup 提供的约束(MeasureSpec)计算自己期望的尺寸。
      • setMeasuredDimension(int measuredWidth, int measuredHeight): 在 onMeasure() 内部必须调用此方法来保存计算出的最终尺寸。
    • MeasureSpec: 一个 32 位 int 值,高 2 位是 Mode,低 30 位是 Size。它封装了父 ViewGroup 对子 View 尺寸的约束要求。
      • EXACTLY (精确模式): 父 ViewGroup 已经为子 View 确定了精确的尺寸(size 就是具体值)。子 View 必须使用这个尺寸,通常对应 layout_width/height="具体dp值"match_parent
      • AT_MOST (最大模式): 子 View 的尺寸不能超过父 ViewGroup 指定的 size。子 View 应计算出它在这个限制下希望的大小,通常对应 layout_width/height="wrap_content"
      • UNSPECIFIED (未指定模式): 父 ViewGroup 对子 View 没有限制。子 View 可以取它想要的任意大小。这种情况相对少见,例如在 ScrollView 内部测量时可能用到。
    • ViewGroup 的职责
      • 遍历所有子 View。
      • 根据自身的布局逻辑(如 LinearLayout 的垂直/水平排列、RelativeLayout 的相对规则)和自身的约束,为每个子 View 计算出合适的 MeasureSpec
      • 调用每个子 View 的 measure(childWidthMeasureSpec, childHeightMeasureSpec) 方法,将计算好的约束传递下去。
      • 收集所有子 View 测量后的尺寸。
      • 根据子 View 的尺寸和自身布局逻辑,计算并设置自身 (ViewGroup) 的尺寸(调用 setMeasuredDimension())。
    • 递归性:测量过程从 DecorView 开始,递归地向下传递到每一个 View 和 ViewGroup,直到最底层的 View。每个 View 的尺寸确定都依赖于父 ViewGroup 的约束和自身的 onMeasure() 实现。
  2. 布局(Layout)

    • 目的:确定每个 ViewViewGroup 在其父容器中的位置(左上右下坐标)
    • 发起者:测量完成后,ViewRootImpl 调用 DecorViewlayout(int l, int t, int r, int b) 方法开始整个 View 树的布局。参数 l,t,r,bDecorView 相对于其父窗口的位置(通常是全屏)。
    • 关键方法
      • View.layout(int l, int t, int r, int b): 设置 View 在其父容器中的位置(left, top, right, bottom)。这个方法通常会调用 onLayout(...)
      • View.onLayout(boolean changed, int l, int t, int r, int b): View 的默认实现是空的。ViewGroup 必须重写此方法。 在此方法中,ViewGroup 根据其布局逻辑,计算并设置其所有子 View 的具体位置(调用每个子 View 的 layout(childL, childT, childR, childB) 方法)。
    • ViewGroup 的职责
      • 遍历所有子 View。
      • 根据在测量阶段得到的子 View 尺寸、自身的布局规则(如 LinearLayout 的顺序排列、FrameLayout 的叠加、RelativeLayout 的依赖关系)以及自身的尺寸和位置(由父 ViewGroup 在 layout() 中设置),计算每个子 View 应该放置的具体坐标 (left, top, right, bottom)
      • 调用每个子 View 的 layout(l, t, r, b) 方法,将计算好的位置传递下去。
    • 递归性:布局过程也是递归的。DecorView 的位置由 ViewRootImpl 设置后,它负责布局其直接子 View,这些子 View(如果是 ViewGroup)再负责布局它们的子 View,如此递归下去,直到所有叶节点 View 的位置都被确定。
  3. 绘制(Draw)

    • 目的:将 View 的内容实际渲染到屏幕上。
    • 发起者:布局完成后,ViewRootImpl 调用 DecorViewdraw(Canvas canvas) 方法开始整个 View 树的绘制。
    • 关键方法
      • View.draw(Canvas canvas): 这是实际绘制的总调度方法。它按顺序执行以下步骤(通常不需要重写):
        1. 绘制背景:调用 drawBackground(Canvas)
        2. 保存图层(如果需要,用于透明/特效)。
        3. 绘制自身内容:调用 onDraw(Canvas canvas)
        4. 绘制子 View:如果是 ViewGroup,调用 dispatchDraw(Canvas canvas) 来绘制其子 View。
        5. 绘制装饰(如滚动条、前景):调用 onDrawForeground(Canvas)
        6. 恢复图层(如果之前保存了)。
      • View.onDraw(Canvas canvas): View 的核心绘制方法,必须重写。 开发者在这里使用 CanvasPaint 等 API 绘制 View 的自定义内容(文本、形状、图片等)。ViewGroup 通常不需要重写此方法(除非有特殊背景),因为它本身通常没有可见内容,主要作用是容纳子 View。
      • ViewGroup.dispatchDraw(Canvas canvas): ViewGroup 重写了此方法。 它负责遍历所有子 View 并调用它们的 draw(Canvas) 方法(进而触发子 View 的 onDraw 和它们子 View 的绘制)。递归绘制子 View 的核心逻辑就在这里。
    • Canvas (画布): 由 SurfaceFlinger(通过 SurfaceHardwareRenderer / Skia)提供,代表一块可以绘制的区域。所有的绘制操作(drawLine, drawRect, drawText, drawBitmap)最终都作用在这个 Canvas 上。
    • 递归性:绘制过程同样是递归的。DecorView.draw() -> onDraw() (可能绘制背景等) -> dispatchDraw() (绘制子 View) -> 子 View 的 draw() -> 子 View 的 onDraw() -> 如果子 View 是 ViewGroup,它的 dispatchDraw() -> … 直到所有叶节点 View 完成 onDraw()

硬件加速 vs. 软件绘制

  • 软件绘制 (Software Rendering):
    • 整个绘制过程(onDraw() 中的操作)在 CPU 上完成。
    • 结果绘制到一个 Bitmap (即 Surface 的软件层)。
    • 最终由 SurfaceFlinger 将这个 Bitmap 合成到屏幕上。
    • 效率相对较低,尤其是在复杂 UI 或动画时。
  • 硬件加速 (Hardware Acceleration - 默认开启):
    • 从 Android 3.0 (API 11) 开始引入并逐步成为默认和推荐方式。
    • 将 View 的绘制命令(Canvas 操作)记录 (Record)显示列表 (Display List) 中(一个在 GPU 内存中的绘制指令序列)。
    • 在合适的时机(通常是 ViewRootImpl 调度或 Choreographer 触发 VSync 信号后),由 RenderThread 使用 OpenGL ESVulkan 将这些显示列表渲染 (Render)Surface 对应的 Texture 上。
    • 最终由 SurfaceFlinger 负责将各个窗口的 Surface (包含纹理) 合成 (Compose) 并最终显示 (Display) 到屏幕上。
    • 利用 GPU 并行处理能力,显著提升绘制性能,特别是动画和复杂视图。

触发绘制的时机

  • 首次显示:当 Activity 从不可见变为可见时,ViewRootImpl 会执行完整的 performTraversals()(包含测量、布局、绘制)。
  • View 的状态变化
    • invalidate(): 请求重绘 (Redraw)。调用此方法的 View 或其父 View 的 onDraw() 方法会在下一个绘制周期被调用。不会触发测量和布局。 适用于仅内容改变但尺寸位置不变的情况(如改变背景色、文本内容)。
    • requestLayout(): 请求重新布局 (Relayout)。调用此方法的 View 会向上回溯到 ViewRootImpl,触发一次新的 performTraversals(),通常包含测量和布局(可能也会触发绘制)。适用于尺寸或位置可能发生改变的情况(如改变文本大小导致 TextView 尺寸变化、动态添加/移除子 View)。
  • 窗口大小改变:如屏幕旋转、分屏模式切换,会触发完整的 performTraversals()
  • 动画:属性动画或 View 动画会持续调用 invalidate() 来刷新视图。

优化点

  • 减少层级深度:过度嵌套的 ViewGroup 会增加测量、布局、绘制的递归深度和耗时。使用 ConstraintLayout 或优化布局结构。
  • 避免过度绘制 (Overdraw):使用开发者选项中的 “显示过度绘制区域” 检查并优化(如移除不必要的背景、使用 clipRect)。
  • 高效 onDraw():避免在 onDraw() 中创建对象(如 new Paint())、进行耗时计算或复杂操作。对象应预先初始化并复用。
  • 使用 ViewStubMerge: 延迟加载不立即显示的视图,减少初始布局复杂度。
  • 理解 MeasureSpec: 正确实现 onMeasure()onLayout(),特别是自定义 ViewGroup 时,确保尺寸计算高效准确,避免不必要的多次测量(measure() 调用可能导致 onMeasure() 被多次调用)。
  • 利用硬件加速:理解其工作原理,避免在硬件加速下不支持的 Canvas 操作(通常会有日志警告)。

总结流程图

+-------------------------+
|       ViewRootImpl       | <---- (1) 触发:首次显示、invalidate()、requestLayout()、窗口大小改变
|                         |       (2) 发起:performTraversals()
+------------+------------+|v (measure, layout, draw)
+-------------------------+
|        DecorView        | (顶级ViewGroup)
| (包含系统UI & 用户布局)  |
+------------+------------+|| 递归遍历v
+-------------------------+
|       ViewGroup         |
| (容器: e.g., LinearLayout) |
|                         |
| onMeasure()  -> 测量自身 | <---- 使用 MeasureSpec
|    |                    |       计算并设置自身尺寸 (setMeasuredDimension)
|    | 遍历子View          |       为每个子View计算 MeasureSpec
|    v                    |       调用 child.measure(childSpec)
|  测量子View              |
|                         |
| onLayout()   -> 布局自身 | <---- 根据自身位置和子View尺寸
|    |                    |       计算每个子View的位置 (l, t, r, b)
|    | 遍历子View          |       调用 child.layout(l, t, r, b)
|    v                    |
|  布局子View              |
|                         |
| dispatchDraw() -> 绘制   | <---- 遍历子View
|    |                    |       调用 child.draw(canvas)
|    v                    |
|  绘制子View              |
+------------+------------+|| 递归遍历v
+-------------------------+
|          View           | (叶子节点,如TextView, Button)
|                         |
| onMeasure()  -> 测量自身 | <---- 根据父ViewGroup给的 MeasureSpec
|                         |       计算并设置自身尺寸 (setMeasuredDimension)
|                         |
| onLayout()   -> (空实现) | <---- 通常不需要,位置由父ViewGroup设置
|                         |
| onDraw()     -> 绘制内容 | <---- 使用Canvas, Paint绘制文本/图形/图片等
+-------------------------+

理解 Android View 的绘制原理(Measure -> Layout -> Draw)对于构建高性能、流畅的 UI 至关重要。它解释了 UI 如何从 XML 或代码定义转换为屏幕上的像素,并揭示了常见的性能瓶颈来源和优化方向。硬件加速的引入极大地提升了绘制效率,但开发者仍需遵循最佳实践以避免不必要的开销。

http://www.lryc.cn/news/579914.html

相关文章:

  • 怎么限制某些IP访问服务器?
  • 基于AR和SLAM技术的商场智能导视系统技术原理详解
  • 基于dropbear实现嵌入式系统ssh服务端与客户端完整交互
  • 适用于 vue2、vue3 的自定义指定:v-int(正整数)
  • HDMI延长器 vs 分配器 vs KVM切换器 vs 矩阵:技术区别与应用场景
  • Django+DRF 实战:从异常捕获到自定义错误信息
  • VS中将cuda项目编译为DLL并调用
  • Excel 如何处理更复杂的嵌套逻辑判断?
  • Java并发性能优化|读写锁与互斥锁解析
  • openEuler 24.03 全流程实战:用 Ansible 5 分钟部署分布式 MinIO 高可用集群
  • 分布式集合通信--学习笔记
  • Data的时区格式BUG
  • 4 位量化 + FP8 混合精度:ERNIE-4.5-0.3B-Paddle本地部署,重新定义端侧推理效率
  • 【三维重建】【3DGS系列】【深度学习】3DGS的理论基础知识之高斯椭球的颜色表达
  • 替代MT6701,3D 霍尔磁性角度传感器芯片
  • Python 机器学习核心入门与实战进阶 Day 2 - KNN(K-近邻算法)分类实战与调参
  • PyTorch实战(14)——条件生成对抗网络(conditional GAN,cGAN)
  • vue-39(为复杂 Vue 组件编写单元测试)
  • MySQL分布式ID冲突详解:场景、原因与解决方案
  • FFmpeg、WebAssembly 和 WebGL 在 Web 端的结合应用
  • GO 语言学习 之 结构体
  • 【深度学习新浪潮】如何使用大模型等技术基于序列预测蛋白质的结构,功能和靶点?
  • 韩顺平之第九章综合练习-----------房屋出租管理系统
  • hive中2种常用的join方式
  • 基于 PyTorch 的猫狗图像分类实战
  • 【HarmonyOS Next之旅】DevEco Studio使用指南(四十) -> 灵活定制编译选项
  • 判断文件是否有硬链接
  • 类图+案例+代码详解:软件设计模式----单例模式
  • 【基础算法】贪心 (二) :推公式
  • PHP:从入门到进阶的全面指南