EN

Android Systrace 基础知识 - Triple Buffer 解读

字数统计: 4.8k阅读时长: 17 min
2019/12/15
loading

本文是 Systrace 系列文章的第十一篇,主要是对 Systrace 中的 Triple Buffer 进行简单介绍,简单介绍了如何在 Systrace 中判断卡顿情况的发生,进行初步的定位和分析,以及介绍 Triple Buffer 的引入对性能的影响

本系列的目的是通过 Systrace 这个工具,从另外一个角度来看待 Android 系统整体的运行,同时也从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你可以理解的更深入一些。

版本说明(2026-10-13 核查):本文保留 2019 年的 Systrace 截图和 60 Hz 时序示意。缓冲如何缓解流水线等待仍有参考价值,但旧图不能直接代表新系统的 BufferQueue 拓扑和调度。历史开关按 AOSP android-9.0.0_r1 / android-10.0.0_r1 核对,新路径对照 android-16.0.0_r1。历史属性仅用于解释对应版本的实现,不作为通用运行时开关。

本文目录

系列文章目录

  1. Systrace 简介
  2. Systrace 基础知识 - Systrace 预备知识
  3. Systrace 基础知识 - Why 60 fps ?
  4. Systrace 基础知识 - SystemServer 解读
  5. Systrace 基础知识 - SurfaceFlinger 解读
  6. Systrace 基础知识 - Input 解读
  7. Systrace 基础知识 - Vsync 解读
  8. Systrace 基础知识 - Vsync-App :基于 Choreographer 的渲染机制详解
  9. Systrace 基础知识 - MainThread 和 RenderThread 解读
  10. Systrace 基础知识 - Binder 和锁竞争解读
  11. Systrace 基础知识 - Triple Buffer 解读
  12. Systrace 基础知识 - CPU Info 解读
  13. Systrace 流畅性实战 1 :了解卡顿原理
  14. Systrace 流畅性实战 2 :案例分析: MIUI 桌面滑动卡顿分析
  15. Systrace 流畅性实战 3 :卡顿分析过程中的一些疑问
  16. Systrace 响应速度实战 1 :了解响应速度原理
  17. Systrace 响应速度实战 2 :响应速度实战分析-以启动速度为例
  18. Systrace 响应速度实战 3 :响应速度延伸知识
  19. Systrace 线程 CPU 运行状态分析技巧 - Runnable 篇
  20. Systrace 线程 CPU 运行状态分析技巧 - Running 篇
  21. Systrace 线程 CPU 运行状态分析技巧 - Sleep 和 Uninterruptible Sleep 篇

怎么定义掉帧?

这篇文章可与 Perfetto 8:Vsync 机制与性能分析 对照阅读。分析时要区分 App 错过渲染期限、某个 Buffer 没有呈现、画面更新不均匀,以及输入到显示的延迟增加;它们并不总是同一件事。

60 Hz 的显示周期约为 16.67 ms,90 Hz 约 11.11 ms,120 Hz 约 8.33 ms,但一个主线程 Slice 的允许时长不能只套这个数字。还要看目标帧率、调度给出的 deadline、CPU/GPU 流水线和目标呈现时间。FrameTimeline 从 Android 12 起提供相关证据;采集支持该数据时,先看 Expected/Actual Timeline、jank 类型和 present 状态,再沿 App、RenderThread、SurfaceFlinger、fence 和调度轨道找原因。旧 Trace 没有这些数据时,结论要相应保留边界。

App 端判断掉帧

初看这张图,很容易想:主线程都超过 16.6 ms 了,这不就是掉帧吗?先别急着下结论。下面框选区间是 30.891 ms,其中主线程 Choreographer#doFrame 很长。它提示 App 生产延迟,不能单凭它断定屏幕在某次刷新中一定重复了旧画面。此前已生产、尚未呈现的新 Buffer 可能暂时维持输出节奏,但也可能没有。

因此,旧 App 端方法耗时要与下游呈现证据对应起来;这不等于“App 端任何帧指标都不能判断 jank”。

SurfaceFlinger 端判断掉帧

这张旧图显示,目标 Layer 的队列在相关时段没有新条目,SurfaceFlinger 也没有对应的新一轮工作。若当时预期画面应每个周期更新,这与旧画面多保留一个周期的情况一致。没有新 Buffer 不等于 App 的 Layer 消失或被跳过合成:系统可以保留已锁存的内容;若其他 Layer 更新,也可以继续使用它。静止画面或低于显示刷新率的内容正常复用旧帧,本身不是卡顿。

第一张图对应的 SurfaceFlinger 轨道如下,队列中还有之前生产的条目,可为后续锁存提供候选:

SurfaceFlinger

图上 Layer 计数表示排队条目数,不是总共分配了几个 Buffer,也不保证 GPU 已经完成。正值与 SurfaceFlinger 有工作只能支持进一步关联,不能独立证明每帧按时显示、输入延迟没有增加。要继续看 frame number/token、acquire fence 和实际呈现。图中的 0/1/2 也只是这个样本的取值范围。

逻辑掉帧

还有一种问题是画面按时输出,但内容更新节奏不对。这里沿用“逻辑掉帧”的叫法:比如列表惯性滑动时,位移步长本应按动画曲线变化,却无故出现 20、10、30 像素的跳变。即便渲染和呈现都赶上了期限,用户仍会感觉不连贯。

仅看通用帧轨道未必能发现原因;可以补充动画进度、位置、输入时间戳等埋点,并结合录像核查。不要把所有曲线不均匀都归为 bug:加速、减速和弹簧运动本就可能是有意设计。

Android 10 的 AnimationUtils 有下面三个相关方法(这里只列签名):

1
2
3
public static void lockAnimationClock(long vsyncMillis)
public static void unlockAnimationClock()
public static long currentAnimationTimeMillis()

Android 10 源码中,前两个是隐藏的测试/Framework 接口;Choreographer 在处理一帧期间固定线程本地动画时钟,保证同一帧的动画取时一致。currentAnimationTimeMillis() 使用的是 uptime 时间域,不是日历墙钟;Android 16还提供携带 expected presentation time 的内部重载。

应用通常使用动画框架,或按 Choreographer 提供的帧时间推进动画,不需要自行调用这些隐藏方法。回调实际执行时刻可能因调度而抖动;稳定的帧时间基准有助于避免同一帧取时不一致,但不能补回已经错过的显示更新,也不能保证系统动画永远没有逻辑问题。

卡顿原因也可参考下面三篇文章:

  1. Android 中的卡顿丢帧原因概述 - 方法论
  2. Android 中的卡顿丢帧原因概述 - 系统篇
  3. Android 中的卡顿丢帧原因概述 - 应用篇

BufferQueue 和 Triple Buffer

BufferQueue

BufferQueue 连接生产者与消费者,传递图形 Buffer 的句柄和同步信息,队列本身不复制整幅像素。两端可以跨进程,也可以在同一进程。AOSP BufferQueue 文档介绍了基本模型:

  1. 生产者通过 dequeueBuffer() 请求空闲 Buffer 槽位,参数包括尺寸、像素格式和 usage;拿到槽位后还要遵守返回的 release fence,才能安全覆盖上次内容。这不是“请求释放一个 Buffer”。
  2. 生产者提交 CPU/GPU 绘制工作后,通过 queueBuffer() 提交 Buffer 及其 fence。GPU 可以仍在执行,入队不等于渲染已完成。
  3. 消费者通过 acquireBuffer() 取得条目,并在使用像素前满足 acquire fence 依赖;获取句柄与实际读取内容可以是不同时间。
  4. 消费者通过 releaseBuffer() 归还槽位并传递 release fence,生产者再次使用时仍需等待相关访问完成。

普通非 shared 模式的槽位通常经过 FREE → DEQUEUED → QUEUED → ACQUIRED → FREE。这是所有权状态,不能代替 fence 的完成状态;取消、丢弃等路径也会改变状态。见 Android 16 BufferSlot.h。

Vsync 帮助安排渲染和呈现,BufferQueue 的所有权与 fence 负责访问同步;四个操作并不是每次都由 Vsync 直接触发。相关机制见:

  1. Systrace 基础知识 - Vsync 解读
  2. Android 基于 Choreographer 的渲染机制详解

上面的生产者、消费者还是有些抽象,我们换成 App 和 SurfaceFlinger 来看。在本文旧设备的 Layer BufferQueue 路径里,App 是生产者,SurfaceFlinger 一侧是消费者:App dequeue、绘制并 queue;SurfaceFlinger 在合成流程中 acquire,在下游不再需要旧内容时 release。release 不等于 SurfaceFlinger CPU 合成函数一返回就能立刻覆写,HWC/GPU 仍可能持有依赖。

新系统不能照搬这个进程边界。Android 16 的 BLASTBufferQueue在客户端创建队列和消费者,再通过 SurfaceControl Transaction 向 SurfaceFlinger 提交 Buffer。缓冲上限受生产者、消费者、异步模式等约束,还会涉及刷新率和工作时长;“三缓冲”不代表所有窗口始终分配或占用恰好三个 Buffer。

那么,这个队列到底需要几个 Buffer 才能运转?下面分别用一、二、三个图像 Buffer 来看流水线如何重叠。图中的 Front/Back 是角色,不是固定槽号;A/B/C 是复用的存储,不是永不变化的帧编号。“CPU 渲染”在硬件加速场景主要包含记录和提交 GPU 命令,不能理解为 CPU、GPU、SurfaceFlinger 永远各自占一块像素 Buffer。图中的屏幕行表示显示出的内容,也不保证直接扫描的就是 App 那块内存,实际还可能经过合成目标或 HWC。

Single Buffer

只有一块图像存储时,生产者写入与消费者读取需要协调,不能按一般多缓冲队列的方式随意重叠。

Single Buffer

下图是串行安排 CPU、GPU、合成和显示的简化理想模型。Vsync offset 可以调整计划时机,但不能保证实际工作总能在预算内结束,也不能替代同步:

Single Buffer

若直接扫描的同一块存储还在被写入,屏幕可能读到新旧混合内容,形成撕裂。单纯错过某个渲染期限并不必然导致撕裂;正确同步的多缓冲系统通常会延迟呈现或继续显示旧内容。

Single Buffer

“单缓冲已经不再使用”也过于绝对。Android 10/API 29 起可用的 低延迟 front-buffer 绘制适用于手写等小范围更新,同时需要处理撕裂风险;它不是普通全屏 View 渲染的通用替代。

Double Buffer

双缓冲允许消费者使用一块时,生产者在另一块可复用的存储上工作:

Double Buffer

下面是 A/B 交替的理想流程。原图底部第 4 条写成生产 A,按图中蓝色区间应为生产 B、消费 A。

DoubleBufferPipline_NoJank

接下来这个示意中,生产和释放时机让 App 无法及时准备下一帧,第二、第四个显示周期重复了旧内容。它展示的是特定时序,不代表所有连续两帧耗时较长都会产生完全相同的结果。图底部第 5 条末尾应为取得已释放的 A 进行生产。

Double Buffer

Triple Buffer

再增加一块可用存储,可以让部分 CPU 提交工作与前一帧 GPU/消费工作重叠,减少等待旧 Buffer 归还造成的空转。若一块正在消费、另一块还在 GPU 绘制,第三块可能可用;这不等于始终有两个空闲 Buffer,也不能让尚未完成的 GPU 结果提前可用。

Triple Buffer

下图中,第二个周期可以先准备 C,第四个周期因此有 C 可显示;第二个周期仍重复 B。原图底部第 4 条沿用了双缓冲说明,应以这里的 显示 C 为准。第五周期 SurfaceFlinger 行同时画出 B 和 A,只能视为简化示意,不能据此认定两帧都在同一次显示刷新中呈现。

TripleBufferPipline_NoJank

把两图放在一起,这个模型的重复旧内容次数从两次减少到一次。它解释了可用 Buffer 增多为什么有时有帮助,不是“三缓冲能消除所有卡顿”的保证。

TripleBuffer_VS_DoubleBuffer

Triple Buffer 的作用

缓解掉帧

三缓冲提供额外周转空间,可以吸收短暂的生产或消费波动。效果取决于哪一段在等待、何时释放,以及此前是否有尚未呈现的有效内容;不能把所有被掩盖的 App 延迟都归为 GPU 问题。

缓解掉帧

上图仍是前面的旧样本。它说明应结合队列看生产与消费,不能只凭计数与合成 Slice 宣布按时呈现。持续超出处理能力时,队列里的余量终会用完。

减少主线程和渲染线程等待时间

双缓冲时,生产者可能要等上一块 Buffer 释放才能继续。图里的 B 要在下游不再使用、依赖满足后才能复用。App 与 SurfaceFlinger 的 Vsync 相位可能不同,不能把“两者总同时收到 Vsync”当作这个现象的前提。

减少主线程和渲染线程等待时间

在旧 HWUI 路径上,RenderThread 的 dequeueBuffer 变长,还可能让主线程后续同步等待。下面的图展示了这种关联,具体阻塞点仍需结合线程状态和 fence/Binder 信息确认:

减少主线程和渲染线程等待时间

第三块 Buffer 有时能缩短这类等待,但三块都在使用、消费速度不足或 fence 未满足时,仍可能出现背压与 dequeue 等待。主线程何时被 RenderThread 解除阻塞也依赖 HWUI 路径,不能把整段 DrawFrame 都算作主线程同步等待。

降低 GPU 和 SurfaceFlinger 瓶颈

这里“降低”的是缓冲不足造成的额外等待,不是提高 GPU 或 SurfaceFlinger 的计算能力。GPU 绘制由提交的命令和依赖推进,不是等 SurfaceFlinger 想使用某块 Buffer 时才临时开始。额外缓冲可以改善重叠,但无法消除持续的 GPU 过载、长合成或调度问题。

下面两图保留了原案例中的 SurfaceFlinger Binder 线程等待和 App RenderThread 长 dequeueBuffer。它们是排查消费端背压的线索;仅凭这组图与附件文件名,不能证明一次受控的“双缓冲改三缓冲后问题基本消失”实验。

内存与延迟代价

额外 Buffer 要占用图形内存,实际大小受 stride、格式、压缩和分配方式影响。生产过快还会积压尚未呈现的内容,增加输入到显示的延迟;这类情况可表现为 FrameTimeline 的 Buffer Stuffing。即使输出节奏平稳,交互也可能更迟钝。是否保留、替换或丢弃排队内容,取决于队列模式及呈现策略,不能假定所有 Buffer 必定逐个显示。

因此,评估缓冲策略应同时比较帧间隔、目标呈现时间、输入延迟、等待位置和内存,而不是只看 FPS 或已分配 Buffer 数。

Debug Triple Buffer

Dumpsys SurfaceFlinger

可以先只读查看设备的版本、属性和服务状态。以下是检查命令,本次未在设备执行;字段和权限以具体系统为准:

1
2
3
4
adb shell getprop ro.build.version.release
adb shell getprop ro.build.fingerprint
adb shell getprop ro.sf.disable_triple_buffer
adb shell dumpsys SurfaceFlinger

getprop 返回空值只说明该属性未设置或不可见,不能直接当作有效配置为 0。还需要核对源码默认值和实际使用它的分支。

图里的 Buffering stats 是旧统计格式。Android 9 SurfaceFlinger按 OccupancyTracker 接受的活动分段累计时间:

  • Active time:纳入统计的活动时间,单位秒,不是应用完整运行时间。
  • Two buffer:没有记录到队列占用超过 1 的分段时间占比。
  • Double buffered / Triple buffered:平均队列占用分别小于 1、介于 1(含)和 2(不含)的分段时间占比。

这几列不是实时分配数量,也不是三项互斥的 Buffer 使用百分比。Two buffer 与后两列可以重叠;Triple buffered 为 0,不能证明从未分配过第三块图形内存。OccupancyTracker 源码还会筛选统计分段,因此没有一行记录也不能填成零使用。

关闭 Triple Buffer

下面保留历史实现说明,不是通用设备开关。行为要以读取属性后的使用位置为准,不能仅根据 disable 名称或日志判断。

Android 版本 <= Android P

本节仅核实 Android 9(android-9.0.0_r1),不外推所有更早版本或厂商 ROM。SurfaceFlinger 读取默认值为 1:

1
2
3
property_get("ro.sf.disable_triple_buffer", value, "1");
mLayerTripleBufferingDisabled = atoi(value);
ALOGI_IF(mLayerTripleBufferingDisabled, "Disabling Triple Buffering");

但 BufferLayer::onFirstRef实际是正条件:

1
2
3
if (mFlinger->isLayerTripleBufferingDisabled()) {
mProducer->setMaxDequeuedBufferCount(2);
}

也就是说,在该路径中值为 1 反而把生产者最大 dequeue 数从默认 1 提高到 2;值为 0 不执行这个扩展。这里确实存在属性名称、日志与使用逻辑反向的问题。它调整的是该队列的参数,不能据此认定整台设备所有队列都恰好是双缓冲或三缓冲。

Android 版本 > Android P

在 Android 10(android-10.0.0_r1),读取默认值变为 0:

1
2
3
property_get("ro.sf.disable_triple_buffer", value, "0");
mLayerTripleBufferingDisabled = atoi(value);
ALOGI_IF(mLayerTripleBufferingDisabled, "Disabling Triple Buffering");

BufferQueueLayer::onFirstRef也配套改成了取反条件:

1
2
3
if (!mFlinger->isLayerTripleBufferingDisabled()) {
mProducer->setMaxDequeuedBufferCount(2);
}

所以此标签值为 0 时允许扩展,值为 1 时不扩展;不是只改了默认值。这个 Android 10 结论不能概括“所有 Android P 之后的版本”。Android 16 的 BLAST 路径会从 SurfaceFlinger 获取最大 acquired 数,并结合其他队列限制;SurfaceFlinger 对该数量的计算还涉及刷新率及 App/SF 工作时长。

原来的 adb root → setprop ro.sf.disable_triple_buffer → stop/start 操作不再作为通用方法提供。ro.* 是首次写入后只读的属性,已存在时,即使有 root 也不能通过普通 setprop 覆盖;未设置时能否首次设置还受权限约束。见 property_service。若进行系统级对比实验,应在可控测试构建中确认配置入口、生效路径及重启范围,记录相同设备、版本和负载下的结果。

参考

  1. AOSP BufferQueue 与 Gralloc
  2. Perfetto FrameTimeline 与 Buffer Stuffing

附件

附件 Systrace_Demo.zip 包含 5 个旧 HTML Trace。本次已核对归档和内嵌 Trace 文本,未执行 HTML 脚本;当前 Chrome 是否兼容旧查看器未验证。文件名里的 Double/Triple 不是运行配置的独立证据,做对比时还需要设备版本、实际队列参数和相同负载。
点此链接下载文章所涉及到的 Systrace 附件

关于我 && 博客

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 博主个人介绍 :里面有个人的微信和微信群链接。
  2. 本博客内容导航 :个人博客内容的一个导航。
  3. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  4. Android性能优化知识星球:付费社群已于 2026 年停止运营,不再招收新成员;此处保留历史介绍。

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

CATALOG
  1. 1. 本文目录
  • 系列文章目录
  • 怎么定义掉帧?
    1. 1. App 端判断掉帧
    2. 2. SurfaceFlinger 端判断掉帧
    3. 3. 逻辑掉帧
  • BufferQueue 和 Triple Buffer
    1. 1. BufferQueue
    2. 2. Single Buffer
    3. 3. Double Buffer
    4. 4. Triple Buffer
  • Triple Buffer 的作用
    1. 1. 缓解掉帧
    2. 2. 减少主线程和渲染线程等待时间
    3. 3. 降低 GPU 和 SurfaceFlinger 瓶颈
    4. 4. 内存与延迟代价
  • Debug Triple Buffer
    1. 1. Dumpsys SurfaceFlinger
    2. 2. 关闭 Triple Buffer
      1. 2.1. Android 版本 <= Android P
      2. 2.2. Android 版本 > Android P
  • 参考
  • 附件
  • 关于我 && 博客