当前位置: 首页 > news >正文

双端 FPS 全景解析:Android 与 iOS 的渲染机制、监控与优化

双端 FPS 全景解析:Android 与 iOS 的渲染机制、监控与优化

本文结合 Android 与 iOS 渲染机制,深入解析 FPS(Frames Per Second)原理、系统实现差异、监控技术演进与优化实践,帮助开发者建立跨平台的性能视角。


1. FPS 与刷新率的概念与关系

屏幕刷新率(Refresh Rate)

  • 定义:硬件属性,指屏幕每秒能够刷新的次数,单位 Hz(赫兹)。
  • 例子:常见的 60Hz 意味着屏幕每秒最多刷新 60 次,即每 16.6ms 会刷新一次画面。
  • 固定性:由屏幕硬件决定,应用无法修改。

FPS(Frames Per Second)

  • 定义:软件渲染输出,指 GPU 每秒绘制完成的帧数。
  • 本质:反映 CPU/GPU 的处理速度,是否能及时准备好一帧画面供屏幕显示。
  • 常见值:60fps、90fps、120fps 等。

两者的关系

  • FPS > 刷新率:实际显示受限于刷新率。例如在 60Hz 屏幕上,哪怕 GPU 渲染能力达到 120fps,用户仍只能看到 60fps,并且可能出现画面撕裂。
  • FPS = 刷新率:理想状态,渲染与刷新匹配,画面最流畅。
  • FPS < 刷新率:渲染端供给不足,屏幕会重复显示旧帧,导致掉帧和卡顿。

关于 120Hz 高刷新率

  • 定义:现在高端手机普遍支持 90Hz 或 120Hz 屏幕,意味着屏幕每秒可以刷新 90/120 次。
  • 意义:相比 60Hz,高刷新率让画面滚动更丝滑,响应更跟手,尤其在游戏和滑动场景中明显。
  • 限制
    • 如果应用渲染能力不足(例如 FPS 只有 60fps),即使屏幕支持 120Hz,用户体验也不会提升。
    • 反之,如果渲染能稳定维持 120fps,高刷屏才能真正发挥优势。

📌 总结:

  • 刷新率是天花板,由硬件决定;
  • FPS 是地板,由软件渲染决定;
  • 用户实际体验取决于两者的匹配程度。

2. 渲染管线全景对比

在理解 FPS 之前,需要先弄清楚 一帧画面是如何从应用层产生,最终显示到屏幕上的
虽然 Android 和 iOS 在实现细节和命名上差别很大,但整体的渲染管线高度一致:从 应用层提交绘制请求 → 系统调度 → 合成 → GPU 渲染 → 显示

下表给出了两端的核心组件对照:

阶段AndroidiOS共性
应用层View / RenderThreadUIKit / CALayer接收输入事件、生成绘制指令
渲染调度ChoreographerCADisplayLink基于 VSync 驱动帧循环,决定何时渲染
合成器SurfaceFlingerRender Server将多层内容合成为最终画面
GPUOpenGL/Skia/VulkanMetal/OpenGL ES负责图形光栅化和绘制
显示FrameBufferFrameBuffer最终送入屏幕显示

为什么要对照这张表?

  1. 打通认知:Android 和 iOS 的术语不同,但职责几乎一一对应。比如 Choreographer ≈ CADisplayLink,它们都负责在 VSync 信号到来时调度渲染。
  2. 抓住共性:不论平台,掉帧的根本原因都是“某个阶段没能在 16.6ms 内完成”。所以优化思路具有跨平台通用性。
  3. 承上启下:理解了这条流水线,后面再讲 VSync、掉帧诊断、FPS 监控时,就能迅速定位到问题是出在调度、合成还是 GPU 渲染。

📌 可以把这张表理解为 渲染管线的双语对照表,它让我们在跨平台性能分析时不至于“各说各话”。


3. 缓冲机制演进

屏幕渲染并不是实时显示 GPU 的绘制结果,而是通过 缓冲区(Buffer) 来解耦“绘制”和“显示”两个过程。
缓冲机制的演进,正是为了在 效率、撕裂、延迟 之间寻找平衡。

3.1 单缓存(Single Buffer)

最原始的方式:

  • 只有一个 Buffer。
  • GPU 不断往 Buffer 里写入数据,屏幕同时从 Buffer 里读取。

📌 问题:

  • 如果 GPU 正在写入,而屏幕同时读取,就可能出现 部分显示新帧、部分显示旧帧 的情况,造成 撕裂(Tearing)
写入
读取
CPU/GPU 绘制
Single Buffer
屏幕显示

3.2 双缓存(Double Buffering)

为避免撕裂,引入两个 Buffer:

  • Front Buffer:屏幕正在显示。
  • Back Buffer:GPU 正在绘制。
  • VSync 信号 到来时,交换 Front/Back Buffer 的指针。
📌 优点
  • 避免撕裂,屏幕总是显示完整帧。
📌 问题
  • 如果 GPU 没能在 16.6ms 内画完一帧,就会错过 VSync 信号,导致 掉帧(Jank)
  • 在双缓冲机制下,GPU 渲染完一帧画面后,如果屏幕还没刷新到下一次,它就只能停下来等。这段时间 GPU 什么都干不了,相当于“坐在那儿发呆”。这就是所谓的“闲置”,会让 GPU 的工作效率变低。换句话说,并不是 GPU 算力不够,而是它画得太快,结果被屏幕刷新节奏卡住了。
显示
VSync 时交换
GPU 绘制
Back Buffer
Front Buffer
屏幕

3.3 三缓存(Triple Buffering)

为缓解 GPU “等待”的问题,再引入一个 Buffer:

  • GPU 绘制时,如果 Back Buffer 已满,可以先写入第三个 Buffer。
  • 这样 GPU 几乎不会阻塞,提高吞吐量。
📌 优点
  • 避免 GPU 空转,提高整体流畅度。
三缓冲的两个问题
1. 延迟更高
  • 因为多了一个缓冲区,用户的操作要经过更多“排队”,画面响应会比双缓冲慢大约 1 帧(8–16ms)
  • 这会让操作灵敏度下降,尤其是在需要快速反应的游戏里更容易被感知。
2. 显存占用更大
  • 每多一个缓冲区就要额外存一整帧画面,意味着更多的显存开销。
  • 在高分辨率或显存有限的设备上,这会增加内存压力。
显示
VSync 时交换
VSync 时交换
GPU 绘制
Back Buffer1
Back Buffer2
Front Buffer
屏幕

3.4 iOS 的演进类比

  • 早期 iOS(Quartz/CoreGraphics 时代)

    • 类似双缓冲,Core Animation 将 Layer 渲染后提交给显示服务器(WindowServer)。
  • 现代 iOS(Core Animation + Metal)

    • 默认使用 Triple Buffering,保证滚动和动画的平滑。
    • Render Server 类似 Android 的 SurfaceFlinger,负责合成多层内容。
    • Apple 在 WWDC 文档中明确提出,高刷新率(120Hz ProMotion)下 Triple Buffering 是保持丝滑体验的核心。
📌 总结
  • Android 和 iOS 都从单缓存 → 双缓存 → 三缓存 演进。
  • 区别在于:iOS 更早在系统级别强制采用 Triple Buffering,而 Android 允许设备厂商/驱动层灵活选择。

3.5 对比总结

机制原理优点缺点Android 实现iOS 实现
单缓存一个 Buffer 同时读写简单、延迟最低容易撕裂早期图形栈早期 Quartz
双缓存前台显示 + 后台绘制避免撕裂容易掉帧,GPU 等待Android 早期默认iOS 早期
三缓存再加一个后台缓冲提高吞吐、平滑增加一帧延迟Android 可选iOS Metal 默认

3.6 高刷新率下的意义

  • 120Hz 屏幕 下,每帧的预算时间缩短到 8.3ms(相比 60Hz 的 16.6ms)。
  • 如果只有 双缓冲,GPU 容易因错过 VSync 而产生大面积掉帧。
  • 三缓冲 能在一定程度上缓冲 GPU 压力,保证帧率稳定。
  • 因此,高刷新率设备几乎都会依赖 Triple Buffering 来支撑丝滑体验。

4. VSync 驱动下的帧调度机制

在 Android 和 iOS 上,屏幕刷新都以 VSync 信号 作为“节拍器”,驱动一帧的渲染流水线。
60Hz 下每 16.6ms 就会发出一次 VSync 信号,120Hz 下则是 8.3ms。
如果某一帧的流水线执行超过了这个时间预算,就会触发掉帧。

Android 的调度链路

VSync 信号到来后,由 Choreographer 统一调度,依次执行四类回调队列:

  1. Input:分发输入事件,例如触摸、点击、手势等。
  2. Animation:执行系统和开发者定义的动画。
  3. Traversal:进行 UI 树的 measure、layout、draw。
  4. Commit:开发者注册的自定义回调(postCallback)。

随后由 RenderThread 负责发起 OpenGL/Skia 指令,交给 GPU 渲染。

iOS 的调度链路

iOS 通过 CADisplayLink 将 VSync 信号传递到 RunLoop,在一次循环中完成:

  1. 执行输入事件分发。
  2. 执行动画回调(Core Animation)。
  3. 提交图层树到 Render Server
  4. Render Server 汇总后交由 GPU 渲染。

📌 共性问题

  • 整条流水线上任一环节(输入 → 动画 → 布局 → 渲染 → GPU)超过刷新间隔时间,就会导致掉帧。
  • 例如:
    • 输入事件分发过慢 → 手势响应卡顿;
    • 动画逻辑复杂 → 播放不流畅;
    • 布局层级过深 → 测量和绘制时间超标;
    • GPU 过载 → 渲染无法赶在下一个 VSync 完成。

5. 系统内核实现

在理解 FPS 的过程中,必须要认识两个核心组件:

  • Android 的 Choreographer
  • iOS 的 CADisplayLink

它们的作用类似,都是基于 VSync 信号 驱动渲染的“帧调度器”,决定了应用何时处理输入、动画和绘制。

5.1 Android - Choreographer

Choreographer 出现在 Android 4.1 (Project Butter) 中,解决了早期掉帧严重的问题。
它的核心职责是:

  • 统一接收 VSync 信号(由 DisplayEventReceiver 下发)。
  • 将一帧内的任务分阶段调度
    1. Input:处理输入事件。
    2. Animation:执行动画回调。
    3. Traversal:视图树遍历,触发 measure/layout/draw
  • 最终把数据交给 RenderThread + GPU 渲染。

关键点

  • 所有操作都在 VSync 的 16.6ms(或 8.3ms@120Hz)预算内执行。
  • 任意阶段阻塞,都会导致掉帧。
  • 我们常用 Choreographer.postFrameCallback() 注册帧回调,来统计 FPS 或监控掉帧。
Choreographer.getInstance().postFrameCallback { frameTimeNanos ->// 在 VSync 驱动下被调用// 可用于 FPS 监控
}

5.2 iOS - CADisplayLink

在 iOS 中,CADisplayLink 提供了类似的能力:

  • 本质是一个 基于 VSync 的计时器
  • 每次屏幕准备刷新时,CADisplayLink 会回调一次。
  • 开发者可以在回调中执行动画步进或 FPS 统计。
典型用法
let displayLink = CADisplayLink(target: self, selector: #selector(step))
displayLink.add(to: .main, forMode: .common)@objc func step(link: CADisplayLink) {// 每次 VSync 调用一次,可用来更新动画或统计 FPS
}
关键点
  • CADisplayLink 不直接渲染,而是通知开发者“下一帧要来了”。
  • 最终绘制由 Core Animation 提交 Layer 树,再交给 Render Server 合成并交给 GPU。
  • Instruments 中的 Core Animation FPS 面板 就是基于 CADisplayLink 机制采样的。
📌 Swift 语法解释
  • to: .main

    • 表示将 CADisplayLink 添加到 主运行循环(RunLoop.main)
    • 因为 UI 更新必须在主线程完成,所以这里选择 .main
  • forMode: .common

    • 指定运行循环模式。
    • .common 是一个集合模式,包含了常见的 RunLoop 模式(如默认模式、UI 跟踪模式)。
    • 使用 .common 可以确保 CADisplayLink 在 滚动、动画、交互 等多种情况下都能继续触发,而不会因为 RunLoop 切换模式而被暂停。

📌 如果使用 .default

  • 当用户滑动 ScrollView 时,RunLoop 模式会切换到 UITrackingRunLoopMode,此时定时器/DisplayLink 会被“卡住”。
  • 使用 .common 就是为了解决这个问题,保证动画和 FPS 统计不中断。

6. FPS 监控方案

Android FPS - 旧方案

  • 旧方案:单纯统计 frameCallback 次数 → 易误判。
  • 新方案:统计每帧耗时,换算掉帧数 → 精确反映 FPS。
核心代码示例解读(旧方案)

旧方案的 FPS 统计完全依赖 Choreographer 的帧回调:

  • 每次 VSync 信号触发时回调 doFrame()
  • 在回调中把计数器 counter++
  • 在统计结束时,用 (总帧数 - 1) / 时间间隔 算出平均 FPS。
核心方案
// FpsTracer.java  —— 旧方案(基于 Choreographer 计数的窗口平均 FPS)
package your.pkg;import android.annotation.TargetApi;
import android.os.Build;
import android.util.Log;
import android.view.Choreographer;public class FpsTracer {public interface IFPSCallBack {void fpsCallBack(long fps);}/** 可选:逐帧时间回调(毫秒),便于打点或联动其他监控 */public interface IFrameCallBack {void onFrame(long frameTimeMs);}private final String mTag;// 运行态private volatile boolean mFPSState = false;private Choreographer.FrameCallback mFrameCallback;// 统计变量(纳秒)private long mStartTimeNanos = -1L;private long mLastFrameNanos = -1L;private int mCounter = 0;// 回调private IFPSCallBack mIFPSCallBack;private IFrameCallBack mIFrameCallBack;public FpsTracer(String tag) {this.mTag = tag;}public void setIFPSCallBack(IFPSCallBack cb) {this.mIFPSCallBack = cb;}public void setIFrameCallBack(IFrameCallBack cb) {this.mIFrameCallBack = cb;}/** 对外:开始采样(需在主线程调用) */public synchronized void start() {if (mFPSState) return;mFPSState = true;if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {startJellyBean();} else {// Pre-16 设备极少,旧方案不做适配(也可用 Handler 16ms 模拟)Log.w(mTag, "FpsTracer unsupported on API < 16");mFPSState = false;}}/** 对外:停止采样并计算/回调(需在主线程调用) */public synchronized void stop() {if (!mFPSState) return;if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {endHighJellyBean();} else {mFPSState = false;}}// ---------------- 内部:API 16+ 核心实现 ----------------@TargetApi(Build.VERSION_CODES.JELLY_BEAN)private void startJellyBean() {// 复位统计mStartTimeNanos = -1L;mLastFrameNanos = -1L;mCounter = 0;mFrameCallback = new Choreographer.FrameCallback() {@Overridepublic void doFrame(long frameTimeNanos) {if (mStartTimeNanos == -1L) {mStartTimeNanos = frameTimeNanos;}// 可选:逐帧时间回调(转毫秒)if (mIFrameCallBack != null) {mIFrameCallBack.onFrame(frameTimeNanos / 1_000_000L);}// 累计帧数++mCounter;// 循环注册下一帧if (mFPSState) {Choreographer.getInstance().postFrameCallback(this);}mLastFrameNanos = frameTimeNanos;}};// 注册首次回调try {Choreographer.getInstance().postFrameCallback(mFrameCallback);} catch (Throwable t) {Log.w(mTag, "postFrameCallback error: " + t.getMessage());mFPSState = false;}}@TargetApi(Build.VERSION_CODES.JELLY_BEAN)private void endHighJellyBean() {// 计算/上报calculateFps();// 移除回调,结束态if (mFrameCallback != null) {try {Choreographer.getInstance().removeFrameCallback(mFrameCallback);} catch (Throwable ignore) {}mFrameCallback = null;}mFPSState = false;}/** 窗口平均 FPS = (帧数 - 1) / (时间间隔秒) */private void calculateFps() {final long intervalNanos = mLastFrameNanos - mStartTimeNanos;if (intervalNanos <= 0 || mCounter <= 1) return;long fps = (mCounter - 1) * 1_000_000_000L / intervalNanos;if (mIFPSCallBack != null) {try {mIFPSCallBack.fpsCallBack(fps);} catch (Throwable t) {Log.w(mTag, "fps callback error: " + t.getMessage());}}if (BuildConfig.DEBUG) {Log.d(mTag, "avg fps = " + fps + "  (frames=" + mCounter + ", windowNs=" + intervalNanos + ")");}}
}
使用示例
// 启动阶段 FPS 监控
private val mFpsTracer = FpsTracer("FPS_LAUNCH").apply {this.setIFPSCallBack { fps ->Log.d("fps_launch", "fps = $fps")}
}fun startLaunchFpsMonitor() {val startTime = System.currentTimeMillis()Log.d("fps_launch", "fps monitor start (20s)")mFpsTracer.start()handler.postDelayed({mFpsTracer.stop()val duration = (System.currentTimeMillis() - startTime) / 1000Log.d("fps_launch", "fps monitor stop after ${duration}s")}, 20_000) // 统计 20 秒
}

这段代码的目标是:在应用启动阶段,利用 Choreographer 的帧回调,在一个固定窗口(20s)内统计平均 FPS,并把结果输出到日志。它属于“旧方案”的实现范式——按回调次数/时间窗口估算 FPS

代码做了什么
  1. 创建监控器并设置回调

    • FpsTracer("FPS_LAUNCH"):声明一次“启动阶段”的 FPS 采样任务。
    • setIFPSCallBack { fps -> … }:当 FpsTracer 计算出 FPS(平均值)后,通过回调打印日志。
  2. 开启 20 秒的采样窗口

    • mFpsTracer.start():内部注册 Choreographer.postFrameCallback,每到一帧就累加计数。
    • handler.postDelayed({ … }, 20_000):在 20s 后自动 stop(),结束采样并触发一次计算与上报。
  3. 计算与上报

    • 结束时,FpsTracer.calculateFps()(帧数-1) / (时长) 得到窗口内的平均 FPS,通过 setIFPSCallBack 回传并打印。
关键点 & 注意事项
  • 统计口径:这是“窗口内平均 FPS”,不是逐帧实时 FPS,也不包含“掉了几帧、掉在第几秒”的细粒度信息。
  • 线程与生命周期
    • FpsTracer.start() / stop() 应在 主线程 调用,因为 Choreographer 依赖主线程 Looper
    • 建议在 冷启动首屏渲染前后调用,避免拉长采样窗口覆盖到用户停留/跳转等行为导致偏差。
  • 刷新率敏感:在 90Hz/120Hz 设备上也能工作,但只是平均值,无法反映高刷下“间歇性掉帧”的细节。
  • 性能开销:旧方案本身开销极低(只做计数),适合粗粒度巡检,不适合精细诊断。
为什么它“容易误判”
  • 旧方案仅按回调次数估算 FPS,默认认为“两次回调之间 >16.6ms 就掉帧”。
    但现实是:即便相邻两次回调时间差较大,只要 每次绘制都在各自的 VSync 周期内完成,用户不一定能感知到“掉帧”。
  • 因此它无法区分
    • “VSync 周期内的绘制完成(未掉帧)” vs “跨 VSync 的超时(真掉帧)”;
    • 无法累计“这 20s 里到底掉了多少帧、掉在何时”。
时序流程(旧方案)
App/业务代码FpsTracerChoreographerHandler(20s)start()postFrameCallback(callback)doFrame(frameTimeNanos)counter++(累计帧数)postFrameCallback(callback)(注册下一帧)loop[每一帧]postDelayed(20s, stop)stop()calculateFps() = (counter-1)/窗口时长IFPSCallBack(fps)App/业务代码FpsTracerChoreographerHandler(20s)

Android - FPS 监控新方案

旧方案的问题在于:只统计了回调次数,容易误判掉帧
例如:两次回调间隔大于 16.6ms 就会被算作掉帧,但实际上只要绘制在 VSync 周期内完成,用户并不会感知卡顿。

为了解决这个问题,新方案引入了“逐帧耗时统计 + 掉帧换算”

实现思路
  1. 获取 VSync 开始时间

    • Choreographer 内部,VSync 时间会写入 FrameInfo
    • 可以通过反射或者内部 Hook 拿到 mFrameInfo 中的 INTENDED_VSYNC 时间戳。
  2. 获取帧完成时间

    • doFrame() 回调最终在主线程消息队列里执行。
    • 可以通过 Looper.setMessageLogging() 拦截日志,拿到消息结束时间。
  3. 计算单帧耗时

    • 单帧耗时 = doFrame 完成时间 - VSync 开始时间
    • 如果耗时 > 16.6ms(60Hz 下),就说明错过了 VSync。
  4. 统计掉帧数

    • 掉帧数 = 单帧耗时 / VSync 周期(向下取整)。
    • 例如:一帧耗时 40ms,周期 16.6ms,则掉帧数 = 40 / 16.6 ≈ 2。
  5. 计算 FPS

    • 公式: FPS = (有效帧数 / (有效帧数 + 掉帧数)) * 刷新率
核心代码示例(简化版)
// RealFpsTracer.kt —— Android 新方案:逐帧耗时 + 掉帧换算
@file:Suppress("PrivateApi", "DiscouragedPrivateApi")package your.pkgimport android.content.Context
import android.os.Build
import android.os.Looper
import android.os.SystemClock
import android.util.Log
import android.view.Choreographer
import android.view.Display
import android.view.WindowManager
import android.util.Printer
import java.lang.reflect.Field
import java.lang.reflect.Method
import java.util.concurrent.CopyOnWriteArrayList
import kotlin.math.floorclass RealFpsTracer(context: Context,private val windowMs: Long = 10_000L,                    // 汇报窗口private val onReport: (FpsReport) -> Unit                // 窗口结束回调
) {data class FpsReport(val fps: Float,val frames: Int,val dropped: Int,val refreshRate: Float,val vsyncPeriodMs: Float,val costsMs: List<Long>                              // 每帧耗时分布(毫秒))private val tag = "RealFpsTracer"private val choreographer = Choreographer.getInstance()private val mainLooper = Looper.getMainLooper()private val costs = CopyOnWriteArrayList<Long>()private var sumDropped = 0private var isRunning = falseprivate var startUptime = 0L// 刷新率 & vsync 周期private val refreshRate: Floatprivate val vsyncPeriodMs: Float// 反射:FrameInfo.mFrameInfo[INTENDED_VSYNC = 1]private var frameInfoArrayField: Field? = nullprivate var frameInfoObjField: Field? = nullprivate var frameInfoArray: LongArray? = null// 当前帧的 “VSync 开始时间(ms)”@Volatile private var currentVsyncBeginMs: Long = 0L// Looper 打印机private var oldPrinter: Printer? = nullprivate val printer: Printer = Printer { line -> handleLooperLog(line) }// 标记:是否正在分发 FrameDisplayEventReceiver(即 doFrame 调度所在 message)@Volatile private var inFrameDispatch = falseinit {// 读取刷新率val rr = runCatching {val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManagerif (Build.VERSION.SDK_INT >= 30) {// Android 11+ 支持多模式,选用当前 display 的 refreshRatecontext.display?.refreshRate ?: wm.defaultDisplay.refreshRate} else {@Suppress("DEPRECATION")wm.defaultDisplay.refreshRate}}.getOrElse { 60f }refreshRate = if (rr > 0f) rr else 60fvsyncPeriodMs = 1000f / refreshRate// 预备反射 FrameInfoprepareFrameInfoReflection()}// ------------------- Public API -------------------fun start() {if (isRunning) returnisRunning = truestartUptime = SystemClock.uptimeMillis()costs.clear()sumDropped = 0// 安装 Looper 打印机(识别 FrameDisplayEventReceiver 起止)oldPrinter = mainLooper.messageLoggingmainLooper.messageLogging = printer// 注册帧回调:用于拿到 VSync 开始时间choreographer.postFrameCallback(frameCallback)// 到期停止并汇报choreographer.postFrameCallbackDelayed(stopCallback, windowMs)}fun stop() {if (!isRunning) returnisRunning = false// 卸载打印机if (mainLooper.messageLogging === printer) {mainLooper.messageLogging = oldPrinter}// 移除帧回调choreographer.removeFrameCallback(frameCallback)choreographer.removeFrameCallback(stopCallback)reportNow()}// ------------------- Core -------------------private val frameCallback = Choreographer.FrameCallback { frameTimeNanos ->// 1) 获取 VSync 开始时间(优先 FrameInfo.INTENDED_VSYNC;失败则退化为 frameTimeNanos)currentVsyncBeginMs = fetchIntendedVsyncMs(frameTimeNanos)// 2) 继续监听下一帧if (isRunning) {choreographer.postFrameCallback(frameCallback)}}private val stopCallback = Choreographer.FrameCallback {stop() // 调用 stop() 会负责 report}private fun handleLooperLog(line: String) {// AOSP 打印格式:// ">>>>> Dispatching to ... com.android.internal.view.Choreographer$FrameDisplayEventReceiver ..."// "<<<<< Finished to ... com.android.internal.view.Choreographer$FrameDisplayEventReceiver ..."val isDispatchStart = line.startsWith(">>>>> Dispatching to")val isDispatchEnd   = line.startsWith("<<<<< Finished to")val isFrameReceiver = line.contains("Choreographer\$FrameDisplayEventReceiver")if (isDispatchStart && isFrameReceiver) {inFrameDispatch = truereturn}if (isDispatchEnd && isFrameReceiver && inFrameDispatch) {// doFrame 对应消息执行完毕(即该帧的主线程任务完成)inFrameDispatch = falseif (!isRunning) returnval end = SystemClock.uptimeMillis()val begin = currentVsyncBeginMs.takeIf { it > 0 } ?: (end - vsyncPeriodMs.toLong())val cost = (end - begin).coerceAtLeast(0L)val dropped = floor(cost.toDouble() / vsyncPeriodMs).toInt()costs += costsumDropped += dropped}}private fun reportNow() {val frames = costs.sizeval fps = if (frames == 0) 0felse frames.toFloat() / (frames + sumDropped).toFloat() * refreshRateval report = FpsReport(fps = fps,frames = frames,dropped = sumDropped,refreshRate = refreshRate,vsyncPeriodMs = vsyncPeriodMs,costsMs = costs.toList())runCatching { onReport(report) }.onFailure {Log.w(tag, "report callback error: ${it.message}")}}// ------------------- FrameInfo Reflection -------------------private fun prepareFrameInfoReflection() {if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) returnrunCatching {val choreoCls = Class.forName("android.view.Choreographer")frameInfoObjField = choreoCls.getDeclaredField("mFrameInfo").apply { isAccessible = true }val frameInfoObj = frameInfoObjField!!.get(choreographer)val frameInfoCls = Class.forName("android.view.FrameInfo")frameInfoArrayField = frameInfoCls.getDeclaredField("mFrameInfo").apply { isAccessible = true }@Suppress("UNCHECKED_CAST")frameInfoArray = frameInfoArrayField!!.get(frameInfoObj) as LongArray}.onFailure {// 反射失败不致命,退化使用 frameTimeNanosframeInfoArray = null}}/** 优先取 INTENDED_VSYNC(index=1);失败则退化为 frameTimeNanos */private fun fetchIntendedVsyncMs(frameTimeNanos: Long): Long {frameInfoArray?.let { arr ->if (arr.size > 1) {val ns = arr[1] // INTENDED_VSYNC = 1if (ns > 0) return ns / 1_000_000L}}return frameTimeNanos / 1_000_000L}
}

iOS 的 FPS 统计(对齐 Android 新方案)

思路(与 Android 新方案一致)

  • CADisplayLink 拿到每帧回调时间戳 timestamp
  • 计算相邻两次回调的时间差 Δt(秒)。
  • 取设备目标刷新率 refreshRate(60/120 等),得到单个 VSync 周期 vsync = 1 / refreshRate(秒)。
  • Δt 跨过了 N 个周期,则认为 掉了 (N-1) 帧
  • 单帧掉帧数计算公式
    dropped = max(0, floor(Δt / vsync) - 1)
    

其中:

  • Δt = 相邻两帧的时间差(秒)
  • vsync = 单个刷新周期(1 / refreshRate,秒)

统计窗口(例如 10s)内的 FPS 计算公式:

有效帧数 = 回调次数
FPS = (有效帧数 / (有效帧数 + 掉帧数)) * refreshRate

这样口径与 Android 新方案一致,便于跨端对比。

代码(拷贝即用)

import UIKitstruct IOSFpsReport {let fps: Float           // 口径对齐:有效帧/(有效帧+掉帧)*刷新率let frames: Int          // 窗口内有效帧(回调次数)let dropped: Int         // 窗口内推算的掉帧数let refreshRate: Int     // 目标刷新率(Hz)let costsMs: [Double]    // 每帧 Δt(毫秒)
}final class IOSRealFpsTracer {typealias Report = (IOSFpsReport) -> Voidprivate var link: CADisplayLink?private var lastTs: CFTimeInterval = 0private var frames = 0private var dropped = 0private var costs: [Double] = []private let window: CFTimeIntervalprivate let onReport: Report// 目标刷新率(与 Android 的 refreshRate 含义一致)private let refreshRate: Intprivate var vsyncSec: Double { 1.0 / Double(refreshRate) }private var windowStart: CFTimeInterval = 0init(windowSeconds: CFTimeInterval = 10,refreshRate: Int = UIScreen.main.maximumFramesPerSecond,onReport: @escaping Report) {self.window = windowSecondsself.refreshRate = max(30, refreshRate) // 兜底self.onReport = onReport}func start() {guard link == nil else { return }resetWindow()let l = CADisplayLink(target: self, selector: #selector(tick))if #available(iOS 15.0, *) {l.preferredFrameRateRange = .init(minimum: 30,maximum: Float(refreshRate),preferred: Float(refreshRate))} else {l.preferredFramesPerSecond = refreshRate}l.add(to: .main, forMode: .common) // .common 防止滚动时被暂停link = l}func stop() {link?.invalidate()link = nilreportNow() // 结束时补一次}private func resetWindow() {frames = 0; dropped = 0; costs.removeAll(keepingCapacity: true)lastTs = 0; windowStart = CACurrentMediaTime()}@objc private func tick(_ link: CADisplayLink) {frames += 1let ts = link.timestampif lastTs == 0 { lastTs = ts; return }let dt = ts - lastTslastTs = ts// 记录每帧 Δt(毫秒)let dtMs = dt * 1000.0costs.append(dtMs)// 计算跨过多少个 vsync 周期,推算掉帧数let cycles = floor(dt / vsyncSec)if cycles >= 2 { // 跨 2 个周期以上才算掉帧dropped += Int(cycles) - 1}// 窗口到期出报表if (ts - windowStart) >= window {reportNow()resetWindow()}}private func reportNow() {guard frames > 0 else { return }// 对齐口径:FPS = (有效帧 / (有效帧 + 掉帧)) * 刷新率let fps = Float(Double(frames) / Double(frames + dropped) * Double(refreshRate))let report = IOSFpsReport(fps: fps,frames: frames,dropped: dropped,refreshRate: refreshRate,costsMs: costs)onReport(report)}
}

总结

FPS(帧率)是衡量应用流畅度的直观指标,它背后关联的是 CPU、GPU、渲染管线、缓冲机制、系统调度 的协作效率。

  • 刷新率(Hz) 是硬件的上限;
  • FPS 则体现软件能否跟上硬件的节奏。

在实现层面,Android 的 Choreographer 与 iOS 的 CADisplayLink 本质相同,都是 VSync 驱动的帧调度器。我们可以通过它们:

  • 在每一帧回调里统计相邻帧时间差;
  • 推算掉帧数;
  • 最终得到统一口径的 FPS:
 FPS = (有效帧数 / (有效帧数 + 掉帧数)) * refreshRate

📌 这意味着:

  • 双端口径统一 —— 无论 Android 还是 iOS,都能用同样的方法来统计 FPS。
  • 指标更准确 —— 不再只是粗略的平均值,而是结合掉帧情况,更贴近用户实际体验。
  • 优化有抓手 —— 掉帧数、耗时分布能帮助开发者找到卡顿原因,指导性能优化。

随着 120Hz 高刷屏 逐渐普及,帧间预算时间从 16.6ms 压缩到 8.3ms,性能优化的门槛更高。
这也是为什么 FPS 统计和跨端统一如此重要:只有先准确地“量”出来,才能有针对性地“改”进去。

👉 一句话总结
FPS 不是一个孤立的数字,而是一面镜子,映照出应用的流畅度和系统的调度效率。先准确度量,再精细优化,才能真正带来用户侧的丝滑体验。

http://www.dtcms.com/a/398200.html

相关文章:

  • redis之缓存
  • 新网站seo外包蓟县做网站公司
  • 六一儿童节网站制作设计公司可以是高新企业
  • VVIC 平台商品详情接口高效调用方案:从签名验证到数据解析全流程
  • 基于物联网的智能衣柜系统的设计(论文+源码)
  • 03)阿里 Arthas(阿尔萨斯)开源的 Java 诊断工具使用-排查web项目请求后响应超时或响应慢;trace、stack、profiler指令使用
  • RNN-Gauss / RNN-GMM 模型的结构
  • Spring框架接口之RequestBodyAdvice和ResponseBodyAdvice
  • Unity 性能优化 之 打包优化( 耗电量 | 发热量 | 启动时间 | AB包)
  • 北京南站在几环山西路桥建设集团网站
  • 北京专业网站建设公司哪家好网站及备案
  • RabbitMQ-保证消息不丢失的机制、避免消息的重复消费
  • 分布式之RabbitMQ的使用(1)
  • 基于Java后端与Vue前端的MES生产管理系统,涵盖生产调度、资源管控及数据分析,提供全流程可视化支持,包含完整可运行源码,助力企业提升生产效率与管理水平
  • 阿里云ACP云计算和大模型考哪个?
  • RabbitMQ C API 实现 RPC 通信实例
  • Ingress原理:七层流量的路由管家
  • 代理网站推荐做网站公司是干什么的
  • 个人建设门户网站 如何备案网址域名注册信息查询
  • React 19 vs React 18全面对比,掌握最新前端技术趋势
  • 链改2.0倡导者朱幼平:内地RWA代币化是违规的,但RWA数资化是可信可行的!
  • iOS 混淆后崩溃分析与符号化实战,映射表管理、自动化符号化与应急排查流程
  • 【JavaSE】【网络原理】网络层、数据链路层简单介绍
  • PyTorch 神经网络工具箱核心内容
  • Git高效开发:企业级实战指南
  • 外贸营销型网站策划中seo层面包括影楼网站推广
  • ZooKeeper详解
  • RabbitMQ如何构建集群?
  • 【星海随笔】RabbitMQ开发篇
  • 深入理解 RabbitMQ:消息处理全流程与核心能力解析