理解这套机制,是构建高性能、流畅UI应用的基础。我们将从宏观体系到微观细节,层层深入。
第一部分:UI体系 - 建筑的蓝图
在盖房子之前,你需要蓝图、施工队和材料。Android的UI体系也是如此。
1. 核心组件:View 与 ViewGroup
这是整个UI体系的基石。
- View :所有UI组件的基类,如
Button
、TextView
。它是一个负责绘制和事件处理的矩形区域。你可以把它看作一块砖。 - ViewGroup :
View
的子类,但它可以包含其他View
和ViewGroup
。它负责布局其子View 。比如LinearLayout
、RelativeLayout
、ConstraintLayout
。你可以把它看作房间的隔断或楼层规划。
关系 :整个UI界面就是一棵由View
和ViewGroup
构成的树形结构 ,我们称之为 View Tree。
2. 承载者:Window、Activity 与 DecorView
- Window :一个抽象概念,代表一个"窗口"。每个
Activity
都会关联一个Window
(具体实现是PhoneWindow
)。它管理着窗口的样式、标题栏、背景等。 - Activity :作为与用户交互的界面,它并不直接绘制UI,而是通过
Window
来承载UI内容。 - DecorView :是
Window
中的顶级ViewGroup
,它包含了系统定义好的标准窗口框架(如ActionBar、状态栏)和一个名为android.R.id.content
的FrameLayout
。setContentView(R.layout.xxx)
这个方法,实际上就是把我们写的布局文件,加载并添加到这个content
父容器中。
体系流程总结 : Activity
-> PhoneWindow
-> DecorView
-> ContentView (你的布局根视图)
-> 你的View Tree。
第二部分:绘制机制 - 施工与粉刷
当UI体系的结构(蓝图)建立好后,系统需要将其绘制到屏幕上。这个过程是Android渲染引擎的核心。
核心绘制流程:三部曲
整个View树的绘制流程,始于ViewRootImpl
的performTraversals()
方法。它会依次触发三个关键过程:测量(Measure) -> 布局(Layout) -> 绘制(Draw)。这个过程会从View树的根节点开始,递归地执行每一个View/ViewGroup。
1. Measure (测量阶段) - 确定大小
- 目标 :计算每个
View
和ViewGroup
的尺寸(宽度和高度)。 - 过程 :
ViewRootImpl
会从根View开始,调用measure()
方法。measure()
方法内部会调用onMeasure()
,这是自定义View时经常需要重写的方法。ViewGroup
(如LinearLayout
)的onMeasure()
会遍历它的所有子View,并调用每个子View的measure()
方法,将父容器的约束(MeasureSpec) 传递下去。MeasureSpec
是一个32位int值,高2位代表模式(Mode) ,低30位代表大小(Size) 。模式有三种:EXACTLY
:精确值,如100dp
或match_parent
。AT_MOST
:最大值,如wrap_content
,子View的大小不能超过这个值。UNSPECIFIED
:不限制,常见于ScrollView
中,子View想多大就多大。
- 子View根据自己的特性和父容器的约束,计算出自己期望的尺寸,并通过
setMeasuredDimension()
保存结果。
2. Layout (布局阶段) - 确定位置
- 目标 :确定每个
View
和ViewGroup
在其父容器中的位置(四个顶点的坐标)。 - 过程 :
- 测量完成后,
ViewRootImpl
会调用根View的layout()
方法。 layout()
方法会调用onLayout()
方法,并传入l, t, r, b
四个参数,代表该View相对于其父容的位置。ViewGroup
必须重写onLayout()
方法 ,因为它的职责是根据测量阶段得到的结果,遍历所有子View,并调用每个子View的layout(l, t, r, b)
方法,告诉它们应该放在哪里。- 普通的
View
(叶子节点)的onLayout()
通常是空实现,因为它没有子View需要安排。
- 测量完成后,
3. Draw (绘制阶段) - 实际渲染
- 目标:将View的内容实际绘制到屏幕上。
- 过程 :
- 布局完成后,
ViewRootImpl
会调用根View的draw()
方法。 draw()
方法会按顺序执行以下操作:- 绘制背景 (
drawBackground
) - 绘制View自身内容 (
onDraw
方法) - 这是最需要关注的地方!- 对于
TextView
,onDraw()
会绘制文字。 - 对于
ImageView
,onDraw()
会绘制位图。 - 自定义View时,我们主要就是重写这个
onDraw(Canvas)
方法。
- 对于
- 绘制子View (
dispatchDraw
) - 如果当前View是ViewGroup,它会遍历子View,调用子View的draw()
方法,如此递归下去。 - 绘制装饰(如滚动条、前景) (
onDrawForeground
)
- 绘制背景 (
- 布局完成后,
重要提示 :为了性能,避免在onDraw
中分配对象 (如new Paint()
),因为onDraw
会被频繁调用,这会导致大量垃圾回收,引起卡顿。
第三部分:性能优化与高级话题
理解了绘制机制,我们就可以针对性地进行优化。
1. 性能瓶颈:过度绘制 (Overdraw)
- 定义:屏幕上的一个像素在单帧内被绘制了多次。
- 原因:不透明的背景重叠、复杂的层级。
- 检测 :在开发者选项中开启 "调试GPU过度绘制" ,颜色越深(特别是红色)表示过度绘制越严重。
- 优化 :
- 移除不必要的背景。
- 使用
canvas.clipRect()
来避免绘制View的不可见部分。 - 使用
android:outlineSpotShadowColor
和android:outlineAmbientShadowColor
(API 28+) 替代直接绘制阴影,以减少图层。
2. 布局优化
- 保持布局层级扁平化 :层级越深,测量和布局所需的时间就越长。
- 首选
ConstraintLayout
:它可以创建扁平化的复杂布局,基本避免了嵌套。 - 善用
<merge>
标签:当自定义View作为根布局是另一个相同的ViewGroup时,使用<merge>
可以去除冗余层级。 - 善用
ViewStub
:用于延迟加载那些初始并不需要显示的布局,只在需要时才实例化,减少初始化时的测量和布局时间。
- 首选
3. 理解硬件加速
从Android 3.0 (API 11) 开始,引入了硬件加速。
- 软件绘制:CPU主导,将绘制指令记录到一个位图(Bitmap)中,最后由CPU提交到屏幕。灵活性高,但效率较低。
- 硬件加速 :将绘制指令(由
Canvas
发出)记录到一个显示列表(Display List) 中,然后交由GPU进行渲染。GPU擅长并行处理大量几何图形计算,因此效率更高、更流畅。 - 兼容性 :大多数标准的View和
Canvas
操作都支持硬件加速。但一些不常用的或自定义的绘制操作可能不支持,此时系统会自动回退到软件绘制,可能会导致性能问题和视觉错误。你可以通过setLayerType
来手动控制。
4. 刷新信号的源头:VSync & Choreographer
为了保证流畅性(如60fps),Android使用了一个稳定的节奏来触发绘制:
- VSync (垂直同步):一个由显示硬件发出的周期性信号(每16.6ms一次),标志着上一帧已显示完毕,可以开始准备下一帧了。
- Choreographer :Android系统中的"编舞者"。它接收VSync信号,然后协调应用的输入、动画和绘制三大操作。
- 当我们需要更新UI(如调用
invalidate()
或执行动画)时,最终会向Choreographer
提交一个绘制任务。 Choreographer
在下一个VSync信号到来时,唤醒ViewRootImpl
开始执行performTraversals()
(即测量、布局、绘制)。- 这样保证了所有的UI变化都与屏幕的刷新率同步,避免了画面撕裂。
- 当我们需要更新UI(如调用
一个完整的16.6ms帧周期 : 应用在VSync n信号后开始计算(CPU:Measure, Layout, Record Display List -> GPU:Rasterize),必须在VSync n+1信号到来之前完成,否则就会丢帧 ,用户就会感觉到卡顿。
总结
作为Android开发者,深入理解UI体系与绘制机制至关重要:
- 宏观上 ,要明白你的View是如何被
Activity
、Window
、DecorView
组织和管理,形成一棵View Tree的。 - 微观上 ,要深刻理解绘制的Measure、Layout、Draw三部曲,知道每个阶段的作用和递归流程。这是自定义View和性能优化的理论基础。
- 系统层面上 ,要了解
Choreographer
和VSync
如何协同工作,以16.6ms为周期驱动着整个UI的更新,并理解硬件加速如何利用GPU来提升绘制效率。
掌握了这些,你就能系统地分析和解决绝大多数UI相关的性能问题(卡顿、掉帧),并写出更优雅、高效的UI代码。