这是一个很深入的问题。Android系统标记需要重绘的View,其核心机制可以概括为:通过一个由下至上、再由上至下的标志位传递过程,精准地锁定并更新“脏区域”。
整个过程主要围绕两个核心方法展开:invalidate() 和 requestLayout()。前者负责重绘(draw),后者负责重新测量和布局(measure + layout)。
简单来说,它的工作流程可以这样理解:
- 触发标记:当View的内容或位置发生变化时,开发者或系统会调用
invalidate()或requestLayout()来发起更新请求。 - 向上传递:这个请求会沿着View树一路向上,最终到达
ViewRootImpl。这个过程会标记沿途的View状态,并计算出一个需要重绘的“脏区域”。 - 注册VSync:
ViewRootImpl收到请求后,会通过Choreographer向系统底层注册监听下一个屏幕刷新信号(VSync)。 - 向下重绘:VSync信号到来后,
ViewRootImpl开始执行遍历(performTraversals()),根据之前标记的状态,只对“脏区域”内的View进行重新绘制。
下面,我们通过源码视角来拆解这两个核心路径。
🏷️ 路径一:仅重绘内容 (invalidate) —— 标记 "Dirty" 区域
当View的内容发生变化,但大小和位置没变时(例如一个ProgressBar的进度更新),会调用 invalidate()。它的核心任务是计算出需要重绘的最小矩形区域,并把这个区域信息向上传递。
源码视角的调用链:
- 起点:
View.invalidate()最终调用到invalidateInternal()。// View.java void invalidateInternal(int l, int t, int r, int b, ...) { // ... 区域计算 ... final ViewParent p = mParent; if (p != null) { // 向上传递脏区域 p.invalidateChild(this, damage); } } - 中间层(ViewGroup):
ViewGroup.invalidateChild()负责合并父容器自身的滚动偏移,将子View的“脏区域”坐标转换成父容器中的坐标。然后通过循环,一直向上传递,直到ViewRootImpl。// ViewGroup.java public final void invalidateChild(View child, Rect dirty) { // ... 坐标转换和区域合并逻辑 ... parent.invalidateChildInParent(location, dirty); } - 终点(ViewRootImpl):
ViewRootImpl将收到的“脏区域”保存下来,并调用scheduleTraversals()发起一次绘制调度。// ViewRootImpl.java void scheduleTraversals() { // 向 Choreographer 注册,等待下一个 VSync 信号 mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); } - 执行:当VSync信号到来,
performTraversals()开始执行。在draw()阶段,系统会只对之前保存的“脏区域”内的View调用onDraw()进行重绘,其他区域保持不变。
这个过程可以用下面的流程图来清晰地展示:
📐 路径二:重新布局并重绘 (requestLayout) —— 标记 "FORCE_LAYOUT"
当View的尺寸或位置发生变化时(例如动态修改TextView的文字大小,或添加/移除View),需要调用 requestLayout()。它的核心任务是通过一个专门的标志位,通知整个View树,所有子View都需要重新测量和布局。
源码视角的调用链:
- 起点:
View.requestLayout()会给自己设置一个标志位。// View.java public void requestLayout() { // 1. 设置强制布局的标志位 mPrivateFlags |= PFLAG_FORCE_LAYOUT; // 2. 向上传递请求 if (mParent != null && !mParent.isLayoutRequested()) { mParent.requestLayout(); } } - 向上传递:这个过程和
invalidate()类似,ViewGroup会覆盖requestLayout(),同样只是简单地将请求向上转发给父视图,直到ViewRootImpl。 - 执行:
ViewRootImpl同样会调用scheduleTraversals()。但在随后的performTraversals()中,情况就不同了:- 测量(Measure):当遍历到带有
PFLAG_FORCE_LAYOUT标志的View时,系统会强制执行onMeasure(),无论其父View传进来的尺寸规格是否改变。 - 布局(Layout):测量完成后,会执行
onLayout()来确定每个子View的新位置。 - 绘制(Draw):因为位置变了,整个View通常也需要重绘,所以往往也会触发
invalidate()的逻辑。
- 测量(Measure):当遍历到带有
⚡ 硬件加速下的现代绘制模型
在默认开启硬件加速的现代Android系统中(API 14+),实际的绘制逻辑有所优化,但标记机制的核心思想不变。
- 软件绘制:如前所述,系统会直接重绘脏区域内的所有View,并将像素数据更新到Bitmap上。
- 硬件加速:
onDraw()中的绘制命令不再立即执行,而是被记录到一个叫做显示列表(Display List) 的结构中。当通过invalidate()标记某个View为“脏”时,系统会仅更新这个View对应的显示列表。在合成帧时,GPU会重放所有显示列表(未变化的View重用旧的显示列表)来完成绘制。这使得绘制效率更高,因为不需要重新执行Java层的onDraw()代码。
💎 总结
| 触发方式 | 核心标记 | 影响范围 | 触发流程 | 适用场景 |
|---|---|---|---|---|
invalidate() | 脏区域 (Dirty Region) | 仅重绘(draw) | 计算脏区域 -> 向上传递 -> 注册VSync -> 重绘脏区域 | 视图内容变化,如进度条、文本变色 |
requestLayout() | PFLAG_FORCE_LAYOUT | 测量与布局(measure + layout) | 设置强制布局标记 -> 向上传递 -> 注册VSync -> 重新测量布局 -> 重绘 | 视图尺寸/位置变化,如增删子View、修改宽高 |
这个机制的精妙之处在于它的分层与协作:上层View通过标志位来表达更新意图,下层 ViewRootImpl 统一调度,最终在VSync的指挥下,精准、高效地完成界面刷新。