android 是怎么标记哪些view是需要重新绘制的 笔记

0 阅读4分钟

这是一个很深入的问题。Android系统标记需要重绘的View,其核心机制可以概括为:通过一个由下至上、再由上至下的标志位传递过程,精准地锁定并更新“脏区域”

整个过程主要围绕两个核心方法展开:invalidate()requestLayout()。前者负责重绘(draw),后者负责重新测量和布局(measure + layout)。

简单来说,它的工作流程可以这样理解:

  1. 触发标记:当View的内容或位置发生变化时,开发者或系统会调用 invalidate()requestLayout() 来发起更新请求。
  2. 向上传递:这个请求会沿着View树一路向上,最终到达 ViewRootImpl。这个过程会标记沿途的View状态,并计算出一个需要重绘的“脏区域”
  3. 注册VSyncViewRootImpl 收到请求后,会通过 Choreographer 向系统底层注册监听下一个屏幕刷新信号(VSync)
  4. 向下重绘:VSync信号到来后,ViewRootImpl 开始执行遍历(performTraversals()),根据之前标记的状态,只对“脏区域”内的View进行重新绘制

下面,我们通过源码视角来拆解这两个核心路径。

🏷️ 路径一:仅重绘内容 (invalidate) —— 标记 "Dirty" 区域

当View的内容发生变化,但大小和位置没变时(例如一个ProgressBar的进度更新),会调用 invalidate()。它的核心任务是计算出需要重绘的最小矩形区域,并把这个区域信息向上传递

源码视角的调用链:

  1. 起点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); 
        }
    }
    
  2. 中间层(ViewGroup)ViewGroup.invalidateChild() 负责合并父容器自身的滚动偏移,将子View的“脏区域”坐标转换成父容器中的坐标。然后通过循环,一直向上传递,直到 ViewRootImpl
    // ViewGroup.java
    public final void invalidateChild(View child, Rect dirty) {
        // ... 坐标转换和区域合并逻辑 ...
        parent.invalidateChildInParent(location, dirty);
    }
    
  3. 终点(ViewRootImpl)ViewRootImpl 将收到的“脏区域”保存下来,并调用 scheduleTraversals() 发起一次绘制调度。
    // ViewRootImpl.java
    void scheduleTraversals() {
        // 向 Choreographer 注册,等待下一个 VSync 信号
        mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, 
                                     mTraversalRunnable, null);
    }
    
  4. 执行:当VSync信号到来,performTraversals() 开始执行。在 draw() 阶段,系统会只对之前保存的“脏区域”内的View调用 onDraw() 进行重绘,其他区域保持不变。

这个过程可以用下面的流程图来清晰地展示:

image.png

📐 路径二:重新布局并重绘 (requestLayout) —— 标记 "FORCE_LAYOUT"

当View的尺寸或位置发生变化时(例如动态修改TextView的文字大小,或添加/移除View),需要调用 requestLayout()。它的核心任务是通过一个专门的标志位,通知整个View树,所有子View都需要重新测量和布局

源码视角的调用链:

  1. 起点View.requestLayout() 会给自己设置一个标志位。
    // View.java
    public void requestLayout() {
        // 1. 设置强制布局的标志位
        mPrivateFlags |= PFLAG_FORCE_LAYOUT; 
        
        // 2. 向上传递请求
        if (mParent != null && !mParent.isLayoutRequested()) {
            mParent.requestLayout();
        }
    }
    
  2. 向上传递:这个过程和 invalidate() 类似,ViewGroup 会覆盖 requestLayout(),同样只是简单地将请求向上转发给父视图,直到 ViewRootImpl
  3. 执行ViewRootImpl 同样会调用 scheduleTraversals()。但在随后的 performTraversals() 中,情况就不同了:
    • 测量(Measure):当遍历到带有 PFLAG_FORCE_LAYOUT 标志的View时,系统会强制执行 onMeasure(),无论其父View传进来的尺寸规格是否改变。
    • 布局(Layout):测量完成后,会执行 onLayout() 来确定每个子View的新位置。
    • 绘制(Draw):因为位置变了,整个View通常也需要重绘,所以往往也会触发 invalidate() 的逻辑。

⚡ 硬件加速下的现代绘制模型

在默认开启硬件加速的现代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的指挥下,精准、高效地完成界面刷新