Optimize for variable refresh rate displays
Graphics & Games 进阶 23m

优化可变刷新率显示

Optimize for variable refresh rate displays

2021年6月9日

在 Apple 官方观看视频

一句话判断

iPhone 13 Pro 的 ProMotion(自适应刷新率)不只是硬件更新——如果你的 App 还在用固定 60fps 的渲染循环,你在这些设备上既浪费电量又浪费了流畅度。

这场 Session 讲了什么

Session 介绍了如何在 iPhone 13 Pro 系列的 ProMotion 显示屏上优化应用渲染。ProMotion 支持从 10Hz 到 120Hz 的动态刷新率调整——当内容静止时降低到 10Hz 省电,滑动或动画时提升到 120Hz 保证丝滑。关键在于:这个切换不是自动的,需要 App 配合使用正确的渲染 API。

UIApp 和游戏的优化策略不同。对于基于 UIKit/SwiftUI 的 App,CADisplayLinkpreferredFrameRateRange 属性是核心 API——告诉系统你的内容期望的帧率范围,系统在这个范围内根据实际情况调整。对于 Metal 游戏,帧率控制需要更精细:通过 MTLDrawable.present(at:)presentAfterMinimumDuration 指定每一帧的期望呈现时间。Session 还讨论了高帧率对电池的影响——120fps 的功耗大约是 60fps 的 1.5 倍,所以该用高帧率时用,该省电时省。

值得深挖的点

CADisplayLink 的 preferredFrameRateRange 如何影响实际帧率

preferredFrameRateRange 是一个建议值,不是强制值。系统综合考虑你的建议、当前电量状态、温度和 GPU 负载来决定实际帧率。设置 CAFrameRateRange(minimum:60, maximum:120, preferred:120) 表示你希望最高 120fps 但可以接受 60fps。对于用户正在滑动列表的场景,这很合适——滑动时 120fps,停下来后系统自动降到 60fps 甚至更低。但如果你的场景是”永远需要最高帧率”(比如 VR 预览),设置 preferred:120 并配合 CAMetalLayer.presentsWithTransaction = true 可以减少延迟。

固定时间步长(Fixed Timestep)与可变帧率的冲突

很多游戏引擎使用固定时间步长物理模拟。在 ProMotion 设备上,帧率可能在一秒内从 60 变到 120 再变回 60,如果你的物理模拟绑定帧率,模拟结果会不稳定。Session 建议将渲染帧率和逻辑更新频率解耦:用 CVDisplayLinkProcessInfo.thermalState 监控实际帧率,逻辑更新始终用固定频率(比如 60Hz),渲染帧率根据显示内容动态调整。

代码片段

// 为 CADisplayLink 设置期望的帧率范围
// 适用于 UIKit 或 SwiftUI 中的动画和列表滚动
class ScrollOptimizer {
    private var displayLink: CADisplayLink?
    
    func startTracking() {
        displayLink = CADisplayLink(target: self, selector: #selector(frameUpdate))
        
        // 告诉系统我们期望 60-120fps 的范围
        // 滑动时系统会给 120fps,静止时降到更低的帧率省电
        displayLink?.preferredFrameRateRange = CAFrameRateRange(
            minimum: 60,
            maximum: 120,
            preferred: 120
        )
        
        displayLink?.add(to: .main, forMode: .common)
    }
    
    @objc private func frameUpdate(_ link: CADisplayLink) {
        // 根据实际帧间隔做渲染更新
        let frameDuration = link.targetTimestamp - link.timestamp
        updateAnimations(duration: frameDuration)
    }
}
// Metal 游戏中控制帧呈现时机
func renderFrame(commandBuffer: MTLCommandBuffer, 
                 drawable: CAMetalDrawable) {
    // 渲染操作...
    
    // 指定期望的帧持续时间
    // 8ms 对应约 120fps,16.67ms 对应 60fps
    let desiredDuration: Double = needsHighFramerate ? 0.008 : 0.01667
    
    drawable.presentAfterMinimumDuration = desiredDuration
    commandBuffer.commit()
}
// 监控热状态,动态调整帧率期望
func updateFrameRateForThermalState() {
    let thermalState = ProcessInfo.processInfo.thermalState
    
    switch thermalState {
    case .nominal:
        // 正常温度,可以使用最高帧率
        displayLink?.preferredFrameRateRange = CAFrameRateRange(
            minimum: 60, maximum: 120, preferred: 120)
    case .serious, .critical:
        // 温度过高,主动降低帧率避免降频
        displayLink?.preferredFrameRateRange = CAFrameRateRange(
            minimum: 30, maximum: 60, preferred: 60)
    default:
        break
    }
}

最佳实践

  • 不要把 preferredFrameRateRange 设为固定 120fps——只在用户主动交互(滑动、拖拽、游戏进行中)时请求高帧率,空闲时让系统降到低帧率省电。
  • 游戏引擎的物理模拟和渲染循环必须解耦。固定 60Hz 逻辑更新 + 可变渲染帧率是 ProMotion 设备上的最优方案。
  • 在 Xcode 中用 Metal System Trace 模板验证帧率切换是否生效。如果帧率一直卡在 60fps,检查是否有 CADisplayLinkMTLDrawable 的配置阻止了升频。

还有什么值得关注

  • UIScrollView 在 iOS 15 中默认支持 ProMotion,不需要额外配置——你的列表视图在 ProMotion 设备上自动获得更流畅的滚动。
  • SwiftUI 的动画也自动适配 ProMotion,但自定义的 TimelineView 需要手动设置合适的 schedule。
  • iPad Pro 从 2017 年就支持 ProMotion,但这次 Session 的 API 更新也适用于那些旧设备。
WWDC 2021