Android | 视图渲染:从invalidate()到屏幕刷新的链路解析

流程图

先上图,下图描述了调用invalidate()后各个阶段与核心组件协作:

flowchart TD subgraph Phase1[阶段一: 应用层 - 标记脏区域与调度] A[View.invalidate] --> B[递归向上标记脏区域DirtyFlag] B --> C[最终调用到ViewRootImpl] C --> D[ViewRootImpl.scheduleTraversals] D --> E[Choreographer.postCallback
提交TRAVERSAL任务] end subgraph Phase2[阶段二: 系统层 - 等待与响应VSync信号] E --> F[Choreographer订阅下一次VSync] F --> G{等待VSync信号到达} G --> H[FrameDisplayEventReceiver.onVsync] H --> I[发送异步消息到MessageQueue] I --> J[因同步屏障, 消息被优先处理] J --> K[执行Choreographer.doFrame] end subgraph Phase3[阶段三: 执行绘制回调] K --> L[按顺序执行Callbacks] L --> M[INPUT回调] M --> N[ANIMATION回调] N --> O[TRAVERSAL回调] O --> P[执行ViewRootImpl.PerformTraversals] end subgraph Phase4[阶段四: 遍历视图树并绘制] P --> Q[PerformMeasure: 测量] Q --> R[PerformLayout: 布局] R --> S[PerformDraw: 绘制] S --> T[View.draw: 发起整个视图树的绘制] T --> U[递归调用子View的draw] U --> V[最终调用你的View.onDraw方法] end subgraph Phase5[阶段五: 渲染与显示] V --> W[UI线程: 录制绘制命令到DisplayList] W --> X[将命令同步至RenderThread渲染线程] X --> Y[RenderThread: GPU栅格化
将矢量命令转换为像素数据] Y --> Z[操作Back Buffer] Z --> A1{栅格化在16.6ms内完成?} A1 -- 是 --> B1[交换Swap Buffer] A1 -- 否 --> C1[丢帧 Jank] B1 --> D1[SurfaceFlinger合成所有Layer] D1 --> E1[屏幕从Frame Buffer读取数据显示] end Phase1 --> Phase2 --> Phase3 --> Phase4 --> Phase5

源码

主要源码如下(基于API32):

java 复制代码
//View.java
public void invalidate() {
        invalidate(true);
}

public void invalidate(boolean invalidateCache) {
    invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}

void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache, boolean fullInvalidate) {
         //...忽略其他代码...
        // Propagate the damage rectangle to the parent view.
        final AttachInfo ai = mAttachInfo;
        final ViewParent p = mParent;
        if (p != null && ai != null && l < r && t < b) {
            final Rect damage = ai.mTmpInvalRect;
            damage.set(l, t, r, b);
            //继续向上找,找到父类ViewGroup
            p.invalidateChild(this, damage);
        }
}

在invalidateInternal()中,会向上找父ViewGroup#invalidateChild(),并将dirty区域向上传。

java 复制代码
//ViewGroup.java
public final void invalidateChild(View child, final Rect dirty) {
    final AttachInfo attachInfo = mAttachInfo;
    ViewParent parent = this;
        do {
               //忽略其他代码
               parent = parent.invalidateChildInParent(location, dirty);
            }
        } while (parent != null);
    }
}

public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
    if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID)) != 0) {
            //合并脏区域,并且将脏区域的坐标从子View转换成父View的相对坐标
            dirty.offset(location[CHILD_LEFT_INDEX] - mScrollX,
                    location[CHILD_TOP_INDEX] - mScrollY);
            if ((mGroupFlags & FLAG_CLIP_CHILDREN) == 0) {
                dirty.union(0, 0, mRight - mLeft, mBottom - mTop);
            }
        //返回父View
        return mParent;
    }
    return null;
}

invalidateChild()会在循环中先调用自身的invalidateChildInParent()合并dirty脏区域并返回父mParent,只要mParent不为空会一直循环遍历,直到找到根节点ViewRootImpl并调用其invalidateChildInParent()方法:

typescript 复制代码
//ViewRootImpl.java
public void invalidateChild(View child, Rect dirty) {
    invalidateChildInParent(null, dirty);
}

接着会调用invalidateChildInParent() -> invalidateRectOnScreen() -> scheduleTraversals()

java 复制代码
final Choreographer mChoreographer;

void scheduleTraversals() {
    //设置为异步消息,优先执行
    mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
    //注册Vsync回调
    mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}

//Vsync后执行的回调
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}

scheduleTraversals()中通过Choreographer注册了CALLBACK_TRAVERSAL回调,mTraversalRunnable就是要执行的Runnable,其run()方法中会执行doTraversal()

scss 复制代码
void doTraversal() {
    //忽略其他代码
    performTraversals();
}

终于到了我们熟悉的ViewRootImpl#performTraversals()方法了,后续的measure、layout、draw都在这里执行了,等绘制完毕,GPU会将数据栅格化并存入Buffer。然后就等待Vsync信号返回后屏幕展示我们绘制的东西了。

阶段详解

阶段一:应用层 - 标记脏区域与调度

  1. View.invalidate() 被调用 :表示这个 View 的内容已经失效("脏"了),需要重新绘制,invalidate() 只是计划一个重绘事件,并不会立即触发绘制。
  2. 递归向上标记invalidate() 方法内部并不会直接进行绘制。它会沿着View的父级一路递归向上 ,直到 ViewRootImpl(整个视图树的根)。这个过程中,各级父View会记录下需要重绘的区域(Dirty区域),以便优化,只绘制必要的部分。
  3. 调度遍历 :当调用链到达 ViewRootImpl 时,会调用它的 scheduleTraversals() 方法。这个方法的核心作用是:向 Choreographer 提交一个任务 ,请求在下一帧执行完整的"遍历"(Traversal),即测量、布局、绘制。它通过 Choreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null) 实现。

阶段二:系统层 - 等待与响应VSync信号

  1. 订阅VSyncChoreographer 收到你的 TRAVERSAL 任务后,会将其放入一个专门的队列(CallbackQueue)中等待执行。然后,它会通过 FrameDisplayEventReceiver 向系统底层订阅下一次VSync信号
  2. VSync信号到达 : 大约16.6ms后(在60Hz设备上),硬件发出VSync信号。ChoreographerFrameDisplayEventReceiver 会接收到这个信号,并触发它的 onVsync() 方法。
  3. 派发到UI线程onVsync() 方法会向主线程的 MessageQueue 发送一个异步消息(这是一个高优先级的消息,即使此时队列中有其他同步消息,它也会被优先处理,主要是因为设置了"同步屏障",即通过Handler发送的是异步消息。

阶段三:执行绘制回调 - doFrame()

  1. 执行 Choreographer.doFrame() :主线程处理这条异步消息,执行 Choreographer.doFrame(long frameTimeNanos) 方法。这是真正开始构建一帧的起点doFrame() 会按严格且固定的顺序 处理三种类型的回调: * CALLBACK_INPUT :处理输入事件。 * CALLBACK_ANIMATION :执行动画回调,更新所有动画的属性值。 * CALLBACK_TRAVERSAL :这就是我们提交的任务所在队列。在这里,会从队列中取出之前放入的 mTraversalRunnable 并执行。
  2. 执行 performTraversals()mTraversalRunnablerun() 方法很简单,就是调用 ViewRootImplperformTraversals()。这是整个View系统中最核心、最复杂的方法,它负责协调: * performMeasure() :测量View树中各个View的尺寸。 * performLayout() :布局,确定每个View在屏幕上的位置。 * performDraw():绘制

阶段四:遍历视图树 - 触发onDraw()

  1. 发起绘制performDraw() 方法会最终调用 View.draw() 方法(注意不是 onDraw()),开始整个视图树的绘制流程。
  2. 递归调用与触发 onDraw()
    • View.draw() 方法会完成自己的背景绘制等内容,然后调用 onDraw(Canvas canvas) 方法
    • onDraw() 方法中的 Canvas 对象在这里进行自定义绘制(画线、文字、位图等)。
    • 之后,View.draw() 方法会继续分发,递归地调用所有子View的 draw() 方法,从而让整个视图树都得到绘制。
  3. 录制绘制命令 :需要注意的是,在硬件加速开启 的情况下(默认),在 onDraw() 中调用 Canvas 的绘制方法(如 drawRect, drawPath)并不会立即执行 ,而是被录制为一系列绘制命令(Display List),稍后交由另一个线程处理。

阶段五:渲染与显示 - 像素上屏

  1. 同步到渲染线程 : UI线程的绘制命令录制完成后,会将这些命令同步 给一个独立的 RenderThread(渲染线程)
  2. GPU栅格化RenderThread 将这些矢量绘制命令提交给 GPU 。GPU在 Back Buffer(后台缓冲区) 上执行栅格化 操作,将矢量命令(如"在x,y位置画一个红色的圆")转换成最终的像素数据
  3. 交换缓冲区 :当一帧的栅格化完成后,GPU会执行一个 swap 操作,将已经准备好的 Back Buffer 和当前正在显示的 Frame Buffer(前台缓冲区) 进行交换。
  4. SurfaceFlinger 合成与显示 :系统服务 SurfaceFlinger 会收集所有应用程序窗口交换后的缓冲区。它根据窗口的Z-order、透明度等信息,将它们合成 为最终屏幕应该显示的图像。当下一个 VSync 信号到来时,屏幕就会从最终合成好的 Frame Buffer 中读取像素数据,你将看到最新的画面

总结

调用 invalidate() 不是一个即时命令,而是一个预约请求。它触发了一个漫长的流水线:

invalidate() -> 标记脏区域 -> 调度 Traversal -> 订阅VSync -> 等待信号 -> doFrame() -> performTraversals() -> draw() -> onDraw() -> 录制命令 -> 提交渲染线程 -> GPU栅格化 -> 交换缓冲区 -> SurfaceFlinger合成 -> 屏幕显示

这个过程任何一步(尤其是UI线程的 doFrame 或GPU的栅格化)超过16.6ms,就会导致丢帧。

相关推荐
独自破碎E1 分钟前
【BISHI9】田忌赛马
android·java·开发语言
代码s贝多芬的音符1 小时前
android 两个人脸对比 mlkit
android
darkb1rd3 小时前
五、PHP类型转换与类型安全
android·安全·php
gjxDaniel3 小时前
Kotlin编程语言入门与常见问题
android·开发语言·kotlin
csj503 小时前
安卓基础之《(22)—高级控件(4)碎片Fragment》
android
峥嵘life4 小时前
Android16 【CTS】CtsMediaCodecTestCases等一些列Media测试存在Failed项
android·linux·学习
stevenzqzq5 小时前
Compose 中的状态可变性体系
android·compose
似霰5 小时前
Linux timerfd 的基本使用
android·linux·c++
darling3317 小时前
mysql 自动备份以及远程传输脚本,异地备份
android·数据库·mysql·adb
你刷碗7 小时前
基于S32K144 CESc生成随机数
android·java·数据库