Android 渲染流水线全解析:从 Choreographer 到 SurfaceFlinger

一、为什么写这篇文章?

Android 渲染是一个跨层协作的复杂系统:应用层负责「画什么」,系统层负责「怎么摆」,硬件层负责「亮起来」。很多开发者能背出「16ms 一帧」,但追问「这 16ms 里系统到底在干什么」,往往就卡壳了。

这篇文章的目标是把整条链路串成一根线,从应用层的 Choreographer 一路穿透到系统层的 SurfaceFlinger,再到硬件层的 HWC/DRM。读完之后,你会理解:

  • 为什么 invalidate() 不是立即重绘?
  • 为什么 onDraw() 里不能创建对象?
  • 为什么 Triple Buffering 能减少卡顿?
  • 为什么 SurfaceView 比 TextureView 更省电?

二、整体架构:一张图看清全局

核心结论

  • 应用层负责「画什么」(Canvas 绘制、View 遍历)
  • 系统层负责「怎么摆」(窗口层级、Surface 合成)
  • 硬件层负责「亮起来」(物理像素刷新)

三者通过 BufferQueue 解耦,通过 VSync 信号同步。


三、应用层:Choreographer 的节拍器

3.1 为什么需要 Choreographer?

Android 的显示刷新率是固定的(60Hz、90Hz、120Hz)。如果应用「想画就画」,很可能画到一半屏幕就开始刷新,导致画面撕裂(Tearing)。

Choreographer 的作用就是让应用的绘制节奏和屏幕的刷新节奏对齐

3.2 Choreographer 的源码核心

java 复制代码
// frameworks/base/core/java/android/view/Choreographer.java

public final class Choreographer {
    // 单例,每个线程一个(但主线程的才是渲染相关的)
    private static final ThreadLocal<Choreographer> sThreadInstance = 
        new ThreadLocal<Choreographer>() {
            @Override
            protected Choreographer initialValue() {
                // 获取当前线程的 Looper,创建 Choreographer
                Looper looper = Looper.myLooper();
                if (looper == null) {
                    throw new IllegalStateException(...);
                }
                return new Choreographer(looper, VSYNC_SOURCE_APP);
            }
        };

    // 核心:申请下一个 VSync 信号
    public void postFrameCallback(FrameCallback callback) {
        postCallbackInternal(CALLBACK_ANIMATION, callback, null);
    }

    // 收到 VSync 后的处理
    void doFrame(long frameTimeNanos, int frame) {
        // ... 时间戳校验

        // 阶段 1:处理输入事件(INPUT)
        doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);

        // 阶段 2:执行动画(ANIMATION)
        doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);

        // 阶段 3:遍历 View 树(TRAVERSAL)
        // 这里会触发 measure -> layout -> draw
        doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);

        // 阶段 4:绘制(DRAW)
        doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
    }
}

3.3 四个 Callback 类型的含义

Callback 类型 执行内容 典型耗时 卡顿根因
CALLBACK_INPUT 分发触摸/按键事件 1-3ms 事件处理耗时(如点击后同步加载数据)
CALLBACK_ANIMATION 执行 ValueAnimator、ObjectAnimator 1-5ms 动画回调中做重计算
CALLBACK_TRAVERSAL measure -> layout -> draw 5-15ms 布局嵌套过深、onMeasure 耗时
CALLBACK_COMMIT 提交绘制结果到 Surface 1-3ms 大量绘制命令(如复杂 Path)

3.4 VSync 信号的传递路径

rust 复制代码
HWC (Hardware Composer)
    │
    │ 产生 VSync 信号(硬件中断)
    ▼
SurfaceFlinger::onVSyncReceived()
    │
    │ 通过 Binder IPC 发送 VSync 事件
    ▼
Choreographer::FrameDisplayEventReceiver::onVSync()
    │
    │ 发送 Message 到主线程 Looper
    ▼
Choreographer::doFrame()
    │
    │ 依次执行 INPUT -> ANIMATION -> TRAVERSAL -> DRAW
    ▼
ViewRootImpl::performTraversals()

关键点 :VSync 信号从硬件到应用,经历了 HWC -> SurfaceFlinger -> Binder IPC -> 主线程 Handler ,每一步都有延迟。所以应用实际可用的绘制时间 不到 16ms ,通常只有 12-14ms


四、View 绘制:从 invalidate 到 Canvas

4.1 invalidate() 的调用链

java 复制代码
// 开发者调用
view.invalidate();

// -> View.java
public void invalidate() {
    invalidate(true);
}

void invalidate(boolean invalidateCache) {
    // 把自己标记为「需要重绘」
    // 递归向上通知父 View
    final ViewParent parent = mParent;
    if (parent != null) {
        parent.invalidateChild(this, null);
    }
}

// -> ViewGroup.java
public final void invalidateChild(View child, final Rect dirty) {
    // 合并脏区域,继续向上传递
    // 最终到达 ViewRootImpl
}

// -> ViewRootImpl.java
void invalidate() {
    mDirty.set(0, 0, mWidth, mHeight);
    if (!mWillDrawSoon) {
        scheduleTraversals();  // 关键:申请下一次 VSync
    }
}

核心逻辑invalidate() 不是立即重绘,而是标记脏区域并申请 VSync 。真正的绘制发生在下一个 doFrame()TRAVERSAL 阶段。

4.2 performTraversals() 的三阶段

java 复制代码
// ViewRootImpl.java
private void performTraversals() {
    // 阶段 1:Measure(测量)
    // 从根 View 递归调用 child.measure()
    // 确定每个 View 的宽高
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

    // 阶段 2:Layout(布局)
    // 从根 View 递归调用 child.layout()
    // 确定每个 View 的位置
    performLayout(lp, desiredWindowWidth, desiredWindowHeight);

    // 阶段 3:Draw(绘制)
    // 创建 Canvas,从根 View 递归调用 child.draw()
    performDraw();
}

4.3 draw() 方法的 6 步

java 复制代码
// View.java
public void draw(Canvas canvas) {
    // Step 1: 绘制背景
    drawBackground(canvas);

    // Step 2: 保存 Canvas 状态
    final int saveCount = canvas.save();

    // Step 3: 绘制内容(onDraw)
    onDraw(canvas);

    // Step 4: 绘制子 View(dispatchDraw)
    dispatchDraw(canvas);

    // Step 5: 绘制装饰(如滚动条)
    onDrawForeground(canvas);

    // Step 6: 恢复 Canvas 状态
    canvas.restoreToCount(saveCount);
}

为什么 onDraw() 里不能创建对象?

因为 onDraw() 在每一帧都会被调用(如果 View 在动画或滚动中)。创建对象会触发 GC,而 GC 的 Stop-The-World 会阻塞主线程,导致掉帧。正确做法是用成员变量缓存 Paint、Path、Rect 等对象


五、Surface 与 BufferQueue:跨进程的数据通道

5.1 为什么需要 Surface?

View 的绘制最终要落到屏幕上,但应用进程不能直接访问屏幕。Android 使用 Surface 作为抽象层,应用向 Surface 绘制,系统从 Surface 读取并合成。

5.2 BufferQueue 的三缓冲机制

三缓冲的意义

  • 双缓冲:应用绘制一个 Buffer,SF 合成另一个。如果应用绘制超时,SF 只能重复显示旧帧(卡顿)。
  • 三缓冲:多一个 Buffer 作为「备胎」。应用绘制超时时,SF 可以用第三个 Buffer 继续合成,减少卡顿感知。

Android 4.1 引入 Project Butter 后,默认就是三缓冲。

5.3 从 unlockCanvasAndPost 到 SurfaceFlinger

java 复制代码
// 应用层:绘制完成后提交
SurfaceHolder.unlockCanvasAndPost(canvas);

// -> Surface.java (JNI 调用)
public void unlockCanvasAndPost(Canvas canvas) {
    // ... 检查 Canvas 状态
    unlockSwCanvasAndPost(canvas);
}

// -> android_view_Surface.cpp (Native 层)
static void nativeUnlockCanvasAndPost(JNIEnv* env, jclass clazz,
        jlong nativeObject, jobject canvasObj) {
    // 获取 ANativeWindow
    sp<Surface> surface(reinterpret_cast<Surface *>(nativeObject));

    // 解锁 Buffer 并提交到 BufferQueue
    status_t err = surface->unlockAndPost();

    // 通知 SurfaceFlinger 有新数据
    // 通过 Binder 发送 INVALIDATE 消息
}

六、SurfaceFlinger:系统的「合成器」

6.1 SurfaceFlinger 的职责

SurfaceFlinger 是 Android 系统的核心系统服务,负责:

  1. 接收来自各个应用的图形 Buffer
  2. 合成(Compose)所有 Layer 为一个最终图像
  3. 送显(Present)到物理屏幕

6.2 Layer 的概念

每个 Window 对应一个 Layer ,每个 Layer 对应一个 BufferQueue

6.3 合成方式:GPU 合成 vs HWC 合成

HWC (Hardware Composer) 是显示芯片的硬件模块,可以直接合成多个 Layer,不需要 GPU 参与。它的优势:

  • 省电:不需要唤醒 GPU
  • 低延迟:硬件直通,没有 GPU 渲染的排队

但 HWC 有限制:支持的 Layer 数量有限(通常 4-8 个),不支持复杂混合模式(如 Porter-Duff)。

6.4 SurfaceFlinger 的主循环

cpp 复制代码
// frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp

void SurfaceFlinger::run() {
    while (true) {
        // 1. 等待 VSync 信号或事件
        waitForEvent();

        // 2. 处理事务(如应用提交新 Buffer、窗口属性变化)
        processTransactionRequests();

        // 3. 收集所有需要合成的 Layer
        refreshLayers();

        // 4. 执行合成
        //    - 如果有 HWC 支持,走 HWC 合成
        //    - 否则,用 GPU 合成到 Framebuffer
        compose();

        // 5. 送显
        //    - 调用 HWC::presentDisplay() 或 DRM ioctl
        present();
    }
}

七、硬件层:从 Framebuffer 到像素

7.1 DRM/KMS 架构

现代 Android 设备使用 DRM (Direct Rendering Manager)KMS (Kernel Mode Setting) 管理显示硬件。

7.2 送显时序:从 VBlank 到像素发光

VBlank (Vertical Blank) 是屏幕两行扫描之间的空隙。在这个时间段内切换 Framebuffer,用户不会看到「一半旧一半新」的画面。


八、卡顿的根因定位:从现象到代码

8.1 卡顿的三种类型

类型 表现 根因 检测方法
渲染卡顿 滑动掉帧、动画不流畅 主线程绘制耗时 > 16ms Systrace / Perfetto 看 doFrame
合成卡顿 所有 App 都卡 SurfaceFlinger 合成耗时 dumpsys SurfaceFlinger
硬件卡顿 特定场景卡(如玩游戏) GPU 满载、温度降频 GPU Profiling / 温度监控

8.2 Systrace 解读:找到卡顿帧

8.3 常见卡顿根因与修复

根因 Systrace 特征 修复方案
布局嵌套过深 performMeasure 耗时 8ms+ ConstraintLayout 替代嵌套 LinearLayout
过度绘制 draw() 耗时 5ms+,多次 onDraw 开启「调试 GPU 过度绘制」优化
主线程 IO doFrame 中有 FileInputStream.read 异步线程 + 回调
Bitmap 大图 drawBitmap 耗时 10ms+ 采样压缩、inBitmap 复用
RecyclerView 复用问题 onBindViewHolder 耗时 5ms+ ViewHolder 模式、异步加载图片
动画阻塞 Choreographer#Animation 耗时 8ms+ 动画回调中避免重计算

九、深度问答

Q1: 为什么 Android 要引入 Triple Buffering?

Triple Buffering 解决的是**「CPU 等待 GPU」**的问题。

在 Double Buffering 下:

  • Buffer A:显示中
  • Buffer B:应用绘制中

如果应用绘制耗时(CPU)> 16ms,比如用了 20ms:

  • 16ms 时 VSync 到了,Buffer B 还没画完
  • SurfaceFlinger 只能继续显示 Buffer A(重复帧,用户感知卡顿)
  • 20ms 时 Buffer B 画完,但下一个 VSync 在 33ms
  • 所以 CPU 从 20ms 到 33ms 一直在空等

在 Triple Buffering 下:

  • Buffer A:显示中
  • Buffer B:应用绘制中(20ms 完成)
  • Buffer C:空闲

20ms 时 Buffer B 画完,SurfaceFlinger 可以立即合成到 Buffer C,CPU 不需要等待 VSync,可以立即开始画下一帧。这样提高了 CPU 利用率,减少了空闲等待。

但代价是内存增加(多一个 Buffer,可能 8MB+),所以低端设备可能还是 Double Buffering。

Q2: SurfaceView 和 TextureView 有什么区别?

特性 SurfaceView TextureView
渲染层 独立的 Surface,在 Window 之外 和普通 View 一样,在 View 树中
合成方式 直接通过 HWC overlay,不经过 GPU 需要 GPU 合成到 Layer
性能 更高(零拷贝、HWC 直通) 较低(需要 GPU 纹理采样)
灵活性 低(不能透明、不能旋转、不能动画) 高(支持 Alpha、Matrix 变换、动画)
适用场景 视频播放、相机预览 需要灵活变换的 Surface 内容

SurfaceView 的 Surface 是独立 Layer,SurfaceFlinger 可以直接用 HWC 合成,不需要 GPU 参与。而 TextureView 的内容需要先渲染到 GPU 纹理,再作为普通 View 的一部分合成,多了一次 GPU 纹理采样。

Q3: onDraw() 里为什么不能做动画?

onDraw() 的调用频率不是固定的,它取决于:

  1. 是否调用了 invalidate()
  2. Choreographer 的 VSync 调度
  3. 系统负载

如果做动画(如逐帧改变位置),需要主动触发重绘 ,但 onDraw() 本身是被动调用的。正确做法是:

  • ValueAnimator + Choreographer.postFrameCallback,在回调中更新状态并 invalidate()
  • 或者用 SurfaceView + 独立线程主动绘制

Q4: Android 12 的 SplashScreen API 是怎么实现的?

SplashScreen 本质上是一个特殊 Window ,由 WindowManagerService 在 App 进程启动时创建。

流程:

  1. ActivityThread.handleLaunchActivity() 调用 performLaunchActivity()
  2. Activity.onCreate() 之前,PhoneWindow 创建 SplashScreenView
  3. SplashScreenView 是一个独立 Layer,Z-Order 在 App Window 之上
  4. 当 App 第一帧绘制完成(reportDrawn()),WMS 发送消息移除 SplashScreen Layer
  5. 移除动画是 WMS 控制的 SurfaceAnimator,通过 Transaction 改变 Layer Alpha

关键点:SplashScreen 不是 App 自己画的,是系统进程画的,所以即使 App 主线程卡住,SplashScreen 也能正常显示。


十、总结:一张图记住整条链路

核心记忆点

  1. VSync 是节拍器:Choreographer 靠它对齐绘制节奏
  2. Surface 是画布:应用画完提交给系统
  3. SurfaceFlinger 是合成器:把多个应用的 Layer 拼成一张图
  4. HWC 是加速器:能用硬件就不用 GPU,省电省资源
  5. 卡顿在应用层:系统层通常不是瓶颈,主线程耗时才是

相关推荐
Java编程爱好者3 小时前
OpenEvent:事件驱动、日志先行的Agent框架
面试
destinying3 小时前
前端秒变AI全栈,我的核心资产是一套Node.js“中间件”
前端·后端·面试
AI人工智能+电脑小能手3 小时前
【大白话说Java面试题 第91题】【Mysql篇】第21题:分布式锁的使用场景和原理?
java·数据库·分布式·mysql·面试
JAVA社区4 小时前
Java高级全套教程(十三)—— 分布式锁超详细实战详解(原理+三种方案企业级落地)
java·开发语言·分布式·spring cloud·面试·java-zookeeper
Mahir084 小时前
MyBatis 延迟加载深度解密:从使用方式到底层动态代理原理全解
java·后端·面试·mybatis
贺国亚5 小时前
Multi-Agent 与 Multi-Task 编排架构
面试
神奇小汤圆5 小时前
滴滴面试官摇头:"你 SKILL.md 全塞进 context 了?我刚翻完 Anthropic 文档,人家是按需加载的。" 我后背一凉
面试
仙俊红7 小时前
线程池面试
python·面试·职场和发展
小江的记录本8 小时前
【JVM虚拟机】类加载机制:类加载器、双亲委派模型、好处、破坏双亲委派的场景(附《思维导图》+《面试高频考点清单》)
java·jvm·spring boot·后端·python·spring·面试