优化可变刷新率显示
Optimize for variable refresh rate displays
2021年6月9日
一句话判断
iPhone 13 Pro 的 ProMotion(自适应刷新率)不只是硬件更新——如果你的 App 还在用固定 60fps 的渲染循环,你在这些设备上既浪费电量又浪费了流畅度。
这场 Session 讲了什么
Session 介绍了如何在 iPhone 13 Pro 系列的 ProMotion 显示屏上优化应用渲染。ProMotion 支持从 10Hz 到 120Hz 的动态刷新率调整——当内容静止时降低到 10Hz 省电,滑动或动画时提升到 120Hz 保证丝滑。关键在于:这个切换不是自动的,需要 App 配合使用正确的渲染 API。
UIApp 和游戏的优化策略不同。对于基于 UIKit/SwiftUI 的 App,CADisplayLink 的 preferredFrameRateRange 属性是核心 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 建议将渲染帧率和逻辑更新频率解耦:用 CVDisplayLink 或 ProcessInfo.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,检查是否有
CADisplayLink或MTLDrawable的配置阻止了升频。
还有什么值得关注
UIScrollView在 iOS 15 中默认支持 ProMotion,不需要额外配置——你的列表视图在 ProMotion 设备上自动获得更流畅的滚动。- SwiftUI 的动画也自动适配 ProMotion,但自定义的
TimelineView需要手动设置合适的 schedule。 - iPad Pro 从 2017 年就支持 ProMotion,但这次 Session 的 API 更新也适用于那些旧设备。