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 操作(通常会有日志警告)。

总结流程图

scss 复制代码
+-------------------------+
|       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 或代码定义转换为屏幕上的像素,并揭示了常见的性能瓶颈来源和优化方向。硬件加速的引入极大地提升了绘制效率,但开发者仍需遵循最佳实践以避免不必要的开销。

相关推荐
还鮟1 小时前
CTF Web的数组巧用
android
小蜜蜂嗡嗡3 小时前
Android Studio flutter项目运行、打包时间太长
android·flutter·android studio
aqi003 小时前
FFmpeg开发笔记(七十一)使用国产的QPlayer2实现双播放器观看视频
android·ffmpeg·音视频·流媒体
zhangphil4 小时前
Android理解onTrimMemory中ComponentCallbacks2的内存警戒水位线值
android
移动开发者1号7 小时前
使用 Android App Bundle 极致压缩应用体积
android·kotlin
移动开发者1号7 小时前
构建高可用线上性能监控体系:从原理到实战
android·kotlin
ii_best12 小时前
按键精灵支持安卓14、15系统,兼容64位环境开发辅助工具
android
美狐美颜sdk13 小时前
跨平台直播美颜SDK集成实录:Android/iOS如何适配贴纸功能
android·人工智能·ios·架构·音视频·美颜sdk·第三方美颜sdk
恋猫de小郭17 小时前
Meta 宣布加入 Kotlin 基金会,将为 Kotlin 和 Android 生态提供全新支持
android·开发语言·ios·kotlin