要实现高精度的 UI 动画或精准的 FPS 统计,核心思路是摆脱主线程业务逻辑的干扰,直接监听屏幕刷新信号(Vsync)与 RunLoop 的状态切换。
以下是结合这三大组件实现高级性能监控和动态渲染的深度方案:
1. 高精度 FPS 统计:CADisplayLink + Observer
传统的 NSTimer 因为受 RunLoop 任务积压影响,无法精准反映屏幕真实的刷新情况。
实现方案:
-
利用
CADisplayLink模拟 Vsync:CADisplayLink会在每次屏幕刷新时触发。通过计算两次触发之间的时间戳差值,可以得出当前真实的帧率。 -
结合
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 实现“分批处理”。
高级玩法:
- 注册一个监听
kCFRunLoopBeforeWaiting的Observer。 - 创建一个任务队列(Task Queue),存储耗时的 UI 操作(如预加载图片、计算文字高度)。
- 每次 Observer 回调时只执行一个任务。
- 执行完后,如果不满足特定条件,不主动唤醒 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下运行,否则滑动列表时统计数据会停滞。 - 动画精度:如果涉及物理引擎,建议使用
CADisplayLink的targetTimestamp属性来预测下一帧的位置,而不是使用当前时间。