iOS 流畅度监控的一个方案

在监控画面是否卡顿时,直接使用主线程 RunLoop 的运行耗时来判断并不直观。业界通常使用屏幕刷新率(FPS)来衡量流畅度,下面就来讨论基于 CADisplayLink 的帧率监控方案。

CADisplayLink 是一个与屏幕刷新率同步的定时器,本质上它是一个特殊的 RunLoop 事件源(CFRunLoopSource),使用时需要添加到 RunLoop 中:

objective-c 复制代码
CADisplayLink *link = [CADisplayLink displayLinkWithTarget:self selector:@selector(update:)];
[link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];

实现原理:
CADisplayLink 内部封装了一个 CFRunLoopSource,注册到 RunLoop 后,会等待 VSync 信号的到来。每次垂直同步信号产生时,系统会把这个 Source 标记为待处理,RunLoop 被唤醒后分发回调。

NSTimer 相比,CADisplayLink 更精准:

  • Timer 基于时间触发,依赖 RunLoop 分发。如果 RunLoop 正在执行耗时任务,定时器可能被延迟甚至跳过,无法保证与屏幕刷新同步。
  • CADisplayLink 由硬件 VSync 驱动,能保证每帧都被调用(只要回调执行时间不超过一帧)。此外,它还支持 preferredFramesPerSecond 属性,可以指定期望的帧率,内部通过有规律地跳帧来实现。

当然,如果回调本身的执行时间超过一帧的时长,下一次 VSync 到来时上一次回调可能尚未结束,就会导致实际丢帧。

scss 复制代码
┌─────────────────────────────────────────────────────────────┐
│                      硬件层                                  │
│  Display Controller → 产生 VSync 中断信号                    │
└─────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────┐
│                     内核层 (iOS)                             │
│  IOKit.framework → 接收 VSync 中断 → 转换为 Mach 消息       │
│  CoreAnimation Server 进程接收并分发                         │
└─────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────┐
│                     用户层 (App)                             │
│  CADisplayLink → 注册到 RunLoop → 接收回调 → 执行方法       │
└─────────────────────────────────────────────────────────────┘

CADisplayLink 与 Core Animation 共享同一个 VSync 源,因此它的回调时机和屏幕刷新严格对齐。

二、监控帧率、掉帧与冻帧

CADisplayLink 提供了两个关键属性:

  • duration:每帧的理论时间间隔(例如 60 Hz 下约为 0.0167 秒)
  • timestamp:当前回调对应的时间戳

利用这些属性就可以实现帧率、掉帧数和冻帧检测。

1. 帧率(FPS)

每次回调时让计数器 fpsCount++,同时记录第一帧的 timestamp。当累积时长 delta = 当前timestamp - 第一帧timestamp ≥ 1秒 时,计算:

ini 复制代码
FPS = fpsCount / delta

然后重置计数器,并将"第一帧时间戳"更新为当前帧,继续下一轮统计。这样得到的是一段时间内的平均帧率,更平滑。

2. 掉帧计算

通过计算相邻两次回调的实际时间间隔,可以知道中间丢了多少帧:

ini 复制代码
实际间隔 duration = 当前帧timestamp - 上一帧timestamp
掉帧数 = (duration - displayLink.duration) / displayLink.duration

为避免单次微小的抖动产生噪声,通常只关注连续掉帧的情况,并可以按严重程度分级统计:

  • drop3:单次掉帧 ≥ 3 帧(轻度卡顿)
  • drop7:单次掉帧 ≥ 7 帧(严重卡顿)

3. 冻帧(Freeze)检测

当两帧之间的时间差 ≥ 700 mskFreezeFrameLimitValue)时,判定为主线程"冻死",画面完全无响应。

此时需要:

  • 采集当前主线程的调用栈,用于定位卡死位置。
  • 避免重复采集:一次长时间的冻帧可能连续触发多次判定条件,因此一旦抓到调用栈,就暂停冻帧检测(例如标记 isFreezeCaptured = YES),直到帧率恢复后再重新开启。

一个简化版冻帧检测逻辑示例:

objective-c 复制代码
- (void)update:(CADisplayLink *)link {
    if (_lastTimestamp == 0) {
        _lastTimestamp = link.timestamp;
        return;
    }
    
    CFTimeInterval interval = link.timestamp - _lastTimestamp;
    _lastTimestamp = link.timestamp;
    
    // 掉帧数
    NSInteger dropped = round((interval - link.duration) / link.duration);
    if (dropped >= 7) {
        // 记录严重掉帧
    }
    
    // 冻帧
    if (interval >= 0.7 && !_isFreezeCaptured) {
        _isFreezeCaptured = YES;
        // 采集主线程堆栈,上报冻帧事件
        [self captureMainThreadStack];
    }
    
    // FPS 统计
    _fpsCount++;
    CFTimeInterval elapsed = link.timestamp - _firstTimestamp;
    if (elapsed >= 1.0) {
        CGFloat fps = _fpsCount / elapsed;
        _fpsCount = 0;
        _firstTimestamp = link.timestamp;
        // 上报 FPS
        
        // 如果之前因为冻帧暂停了,此处可以恢复
        _isFreezeCaptured = NO;
    }
}
相关推荐
美狐美颜sdk14 小时前
直播APP开发如何实现美颜功能?低成本美颜SDK方案推荐
android·人工智能·ios·第三方美颜sdk·视频美颜sdk
CocoaKier15 小时前
X未提前通知,突然停用twitter授权登录域名,大量X三方登录异常!
android·ios
2501_9159184117 小时前
Linux 上生成 AppStoreInfo.plist,App Store 上架 iOS
android·ios·小程序·https·uni-app·iphone·webview
资源分享助手17 小时前
Codex iOS连接失败解决方法 iOS 可以完成 SSH 认证,但始终无法建立稳定 Codex 会话
ios·ssh·codex
我命由我1234518 小时前
Dart - 数字类型、布尔类型、列表类型
android·开发语言·flutter·ios·uni-app·android jetpack·移动端
一朵盆栽18 小时前
uni-app用Windows系统开发iOS端
ios·uni-app·cocoa
TO_ZRG19 小时前
iOS 证书校验
macos·ios·cocoa
人月神话-Lee1 天前
【图像处理】Sobel 边缘检测——让机器“看见“轮廓
图像处理·人工智能·计算机视觉·ios·ai编程·swift
开开心心loky2 天前
[OC 底层] (三) 方法缓存与消息发送机制
macos·ios·缓存·objective-c·cocoa