Android 中 View 的绘制原理是一个核心机制,它决定了 UI 如何从代码定义最终变成屏幕上可见的像素。整个过程是一个自顶向下递归遍历、测量、布局和绘制 的过程,主要涉及三个核心阶段:测量(Measure) 、布局(Layout) 和 绘制(Draw)。以下是详细描述:
核心参与者
ViewRootImpl
: 连接WindowManager
和DecorView
的纽带。它是整个 View 树绘制的发起者和总协调者。每个窗口(Activity/Dialog)对应一个ViewRootImpl
。DecorView
: 是窗口的顶级 View。它通常包含系统 UI(如状态栏背景、ActionBar/Toolbar)和开发者设置的setContentView(R.layout.xxx)
的布局(作为其子 View,通常是FrameLayout
或LinearLayout
)。View
: 所有 UI 组件的基类。负责自身的测量、布局和绘制。ViewGroup
:View
的子类,是容器,可以包含其他View
或ViewGroup
(子 View)。它负责管理子 View,包括调用子 View 的测量、布局方法,并决定子 View 的位置和尺寸。
核心流程:三部曲
绘制过程由 ViewRootImpl
的 performTraversals()
方法发起。这个方法会根据需要(如 View 请求刷新、窗口首次显示、窗口大小改变等)依次调用三个核心方法:
-
测量(Measure):
- 目的 :确定每个
View
和ViewGroup
的尺寸(宽和高)。 - 发起者 :
ViewRootImpl
调用DecorView
的measure()
方法开始整个 View 树的测量。 - 关键方法 :
View.measure(int widthMeasureSpec, int heightMeasureSpec)
: 最终会调用onMeasure(...)
。View 的尺寸由其父 ViewGroup 传递下来的约束(MeasureSpec
)和自身的需求共同决定。View.onMeasure(int widthMeasureSpec, int heightMeasureSpec)
: 必须 被每个View
或ViewGroup
子类重写。在此方法中,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()
实现。
- 目的 :确定每个
-
布局(Layout):
- 目的 :确定每个
View
和ViewGroup
在其父容器中的位置(左上右下坐标)。 - 发起者 :测量完成后,
ViewRootImpl
调用DecorView
的layout(int l, int t, int r, int b)
方法开始整个 View 树的布局。参数l,t,r,b
是DecorView
相对于其父窗口的位置(通常是全屏)。 - 关键方法 :
View.layout(int l, int t, int r, int b)
: 设置 View 在其父容器中的位置(l
eft,t
op,r
ight,b
ottom)。这个方法通常会调用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 的位置都被确定。
- 目的 :确定每个
-
绘制(Draw):
- 目的 :将
View
的内容实际渲染到屏幕上。 - 发起者 :布局完成后,
ViewRootImpl
调用DecorView
的draw(Canvas canvas)
方法开始整个 View 树的绘制。 - 关键方法 :
View.draw(Canvas canvas)
: 这是实际绘制的总调度方法。它按顺序执行以下步骤(通常不需要重写):- 绘制背景 :调用
drawBackground(Canvas)
。 - 保存图层(如果需要,用于透明/特效)。
- 绘制自身内容 :调用
onDraw(Canvas canvas)
。 - 绘制子 View :如果是
ViewGroup
,调用dispatchDraw(Canvas canvas)
来绘制其子 View。 - 绘制装饰 (如滚动条、前景):调用
onDrawForeground(Canvas)
。 - 恢复图层(如果之前保存了)。
- 绘制背景 :调用
View.onDraw(Canvas canvas)
:View
的核心绘制方法,必须重写。 开发者在这里使用Canvas
和Paint
等 API 绘制 View 的自定义内容(文本、形状、图片等)。ViewGroup
通常不需要重写此方法(除非有特殊背景),因为它本身通常没有可见内容,主要作用是容纳子 View。ViewGroup.dispatchDraw(Canvas canvas)
:ViewGroup
重写了此方法。 它负责遍历所有子 View 并调用它们的draw(Canvas)
方法(进而触发子 View 的onDraw
和它们子 View 的绘制)。递归绘制子 View 的核心逻辑就在这里。
Canvas
(画布) : 由SurfaceFlinger
(通过Surface
和HardwareRenderer
/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 ES 或 Vulkan 将这些显示列表渲染 (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()
)、进行耗时计算或复杂操作。对象应预先初始化并复用。 - 使用
ViewStub
和Merge
: 延迟加载不立即显示的视图,减少初始布局复杂度。 - 理解
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 或代码定义转换为屏幕上的像素,并揭示了常见的性能瓶颈来源和优化方向。硬件加速的引入极大地提升了绘制效率,但开发者仍需遵循最佳实践以避免不必要的开销。