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,就会导致丢帧。

相关推荐
alexhilton6 分钟前
运行时着色器实战:实现元球(Metaballs)动效
android·kotlin·android jetpack
從南走到北39 分钟前
JAVA国际版东郊到家同城按摩服务美容美发私教到店服务系统源码支持Android+IOS+H5
android·java·开发语言·ios·微信·微信小程序·小程序
观熵3 小时前
Android 相机系统全景架构图解
android·数码相机·架构·camera·影像
Huntto3 小时前
在Android中使用libpng
android
雨白5 小时前
Android 自定义 View:彻底搞懂 Xfermode 与官方文档陷阱
android
_小马快跑_6 小时前
从VSync心跳到SurfaceFlinger合成:拆解 Choreographer与Display刷新流程
android
Monkey-旭8 小时前
Android 定位技术全解析:从基础实现到精准优化
android·java·kotlin·地图·定位
树獭非懒10 小时前
Android 媒体篇|吃透 MediaSession 与 MediaController
android·架构
一起搞IT吧11 小时前
高通Camx hal进程CSLAcquireDeviceHW crash问题分析一:CAM-ICP FW response timeout导致
android·图像处理·数码相机