7-15.【OC】【RunLoop】如何结合 RunLoop / Timer / Observer 来做高精度 UI 动画或 FPS 统计?

3 阅读3分钟

要实现高精度的 UI 动画或精准的 FPS 统计,核心思路是摆脱主线程业务逻辑的干扰,直接监听屏幕刷新信号(Vsync)与 RunLoop 的状态切换

以下是结合这三大组件实现高级性能监控和动态渲染的深度方案:


1. 高精度 FPS 统计:CADisplayLink + Observer

传统的 NSTimer 因为受 RunLoop 任务积压影响,无法精准反映屏幕真实的刷新情况。

实现方案:

  1. 利用 CADisplayLink 模拟 Vsync

    CADisplayLink 会在每次屏幕刷新时触发。通过计算两次触发之间的时间戳差值,可以得出当前真实的帧率。

  2. 结合 Observer 识别“伪高帧率”

    有时候 CADisplayLink 正常触发,但主线程因为处理 Source0 任务过久,导致 UI 根本没时间渲染。这时需要用 Observer 监听 BeforeWaiting 状态。

Swift

// FPS 计数器核心逻辑
displayLink = CADisplayLink(target: self, selector: #selector(tick(link:)))
displayLink?.add(to: .main, forMode: .common)

@objc func tick(link: CADisplayLink) {
    if lastTime == 0 { lastTime = link.timestamp; return }
    count += 1
    let delta = link.timestamp - lastTime
    if delta < 1 { return } // 每秒计算一次
    let fps = Double(count) / delta
    print("当前真实 FPS: (fps)")
    lastTime = link.timestamp
    count = 0
}

2. 高精度 UI 动画:解耦计算与渲染

如果你要做复杂的路径动画(如高性能图表、粒子系统),直接在 UIView.animate 里做会导致主线程压力过大。

实现策略:

  • Timer/GCD 作为生产者:在子线程计算动画的下一帧坐标数据,避开主线程干扰。
  • RunLoop Observer 作为消费者:监听 kCFRunLoopBeforeWaiting 状态。
  • 渲染时机:只有在 RunLoop 准备进入休眠、即将提交渲染数据给 GPU 之前,才从子线程缓存中取出坐标,更新 UI。

这样做的好处:确保你的 UI 更新逻辑永远不会由于前面的业务代码(Source0)太长而“错过”当前这一帧的渲染窗口。


3. 性能优化:空闲任务分发(Task Scheduler)

很多卡顿是因为在一次 RunLoop 循环中处理了太多 Source0 任务。我们可以利用 Observer 实现“分批处理”。

高级玩法:

  1. 注册一个监听 kCFRunLoopBeforeWaitingObserver
  2. 创建一个任务队列(Task Queue),存储耗时的 UI 操作(如预加载图片、计算文字高度)。
  3. 每次 Observer 回调时只执行一个任务
  4. 执行完后,如果不满足特定条件,不主动唤醒 RunLoop,等待下次事件触发再顺带执行下一个任务。

Objective-C

// 伪代码:利用观察者在空闲时段处理任务
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
    if (taskQueue.count == 0) return;
    autoreleasepool {
        Task block = [taskQueue pop];
        block(); // 执行一个任务就结束,把控制权交回给 RunLoop
    }
});

4. 三大组件协同总结

组件在高精度任务中的角色关键动作
Timer (CADisplayLink)信号源严格对齐 16.7ms (60Hz) 的节奏。
RunLoop Mode过滤器使用 CommonModes 确保在滑动时不掉帧。
Observer调度员BeforeWaiting 拦截渲染时机,执行延迟加载。

💡 核心建议:

  • FPS 统计:必须使用 CADisplayLink 并在 CommonModes 下运行,否则滑动列表时统计数据会停滞。
  • 动画精度:如果涉及物理引擎,建议使用 CADisplayLinktargetTimestamp 属性来预测下一帧的位置,而不是使用当前时间。