作者 / Ady Abraham, Software Engineer
长久以来,手机屏幕刷新率都是 60Hz。利用和游戏开发者也习惯了假设刷新率为 60Hz,也就是每 16.6ms 生成一帧,而且这样开发进去的利用和游戏都会失常进行。但当初的状况曾经不同了。最新的旗舰级设施往往会搭载刷新率更高的屏幕,能够带来更晦涩的动画成果、更低的提早,从而取得更好的整体用户体验。还有一些设施反对可变刷新率,比方 Pixel 4,它反对 60Hz 和 90Hz 两种刷新率。
60Hz 的屏幕每 16.6ms 刷新一次显示内容。这意味着图像显示的工夫是 16.6ms 的倍数 (16.6ms、33.3ms、50ms 等)。反对多种刷新率的屏幕则带来了更多的抉择,这些屏幕能以不同的速度进行渲染,并且不会呈现抖动。例如,一个无奈维持 60fps 渲染的游戏,在 60Hz 的屏幕上必须一路降到 30fps 能力确保晦涩无抖动 (因为显示器只能以 16.6ms 的倍数周期出现图像,所以 60Hz 的下一档可用帧速是每 33.3ms 显示一帧,即 30fps)。而在 90Hz 设施上,同样的游戏只须要降落到 45fps (每帧 22.2ms) 即可,这就为用户带来了更晦涩的体验。而同时反对 90Hz 和 120Hz 的设施,则能够用每秒 120、90、60 (120/2)、45 (90/2)、40 (120/3)、30 (90/3)、24 (120/5) 等帧率流畅地出现内容。
高频率渲染
渲染频率越高,就越难维持帧率,因为只有更少的工夫实现雷同的工作量。要在 90Hz 下进行渲染,利用须要在 11.1ms 内生成一帧,与此相比,在 60Hz 时则有 16.6ms 来生成一帧。
为了具体阐明这一点,咱们来看看 Android UI 的渲染流水线。咱们能够将帧渲染大抵分为五个流水线阶段:
- 利用的 UI 线程解决输出事件,调用利用的回调,并更新视图 (View) 层次结构中记录的绘图命令列表;
- 利用的 RenderThread 将记录的命令发送到 GPU;
- GPU 绘制这一帧;
- SurfaceFlinger 是负责在屏幕上显示不同利用窗口的零碎服务,它会组合出屏幕应该最终显示出的内容,并将画面提交给屏幕的硬件形象层 (HAL);
- 屏幕最终出现该帧的内容。
整个流水线由 Android Choreographer 管制。Choreographer 基于显示垂直同步 (vsync) 事件,它示意屏幕开始扫描出图像并更新显示像素的工夫点。尽管 Choreographer 基于 vsync 事件,但对利用和 SurfaceFlinger 来说,其唤醒偏移量不同。下图展现了在 Pixel 4 设施上运行的流水线,利用在 vsync 事件后 2ms 被唤醒,SurfaceFlinger 则在 vsync 事件后 6ms 被唤醒。这样一来,利用产生一帧画面的工夫为 20ms,SurfaceFlinger 组合画面内容的工夫则为 10ms。
当以 90Hz 频率运行时,利用仍然在 vsync 事件后 2ms 被唤醒。然而,SurfaceFlinger 在 vsync 事件后 1ms 被唤醒,同样有 10ms 的工夫来合成屏幕内容。但这样一来利用只有 10ms 来渲染一帧画面,这工夫就十分困顿了:
为了缓解这种状况,Android 的 UI 子系统采纳了事后渲染 (render ahead,指维持一帧的启动工夫不变,但推延其出现工夫) 来深入流水线,并将帧的出现工夫推延一个 vsync。这样一来,利用能够有 21ms 的工夫来生成一帧,同时确保维持 90Hz 的吞吐量。
一些利用,包含大多数游戏,都有本人自定义的渲染流水线。这些流水线可能会有更多或更少的阶段,具体取决于它们要实现的工作。一般来说,流水线越深,能够并行执行的阶段就越多,整体的吞吐量也会相应减少。但另一方面,这样可能会减少单帧的提早 (提早量为 number_of_pipeline_stages x longest_pipeline_stage)。这两头如何取舍须要开发者审慎考虑。
利用可变刷新率
如上所述,可变刷新率容许咱们应用更多样的渲染频率。对于能够管制渲染速度的游戏,以及须要以特定速率出现内容的视频播放器来说,这一点尤其有用。例如,要在 60Hz 的显示器上播放 24fps 的视频,咱们须要应用 3:2 pulldown 算法,这就会产生抖动。然而,如果设施的屏幕能够原生显示 24fps 的内容 (24/48/72/120Hz),就无需应用 pulldown 算法,天然也就不会呈现抖动了。
设施运行时的刷新率是由 Android 平台管制的。利用和游戏能够通过多种办法影响刷新率 (上面会有解释),但最终后果由平台决定。尤其是当屏幕上同时有多个利用时,这一点至关重要: 平台须要满足所有利用的刷新率需要。24fps 视频播放器就是一个很好的例子。24Hz 对于视频播放来说可能很好,但对于响应式 UI 来说就很蹩脚了。如果一个推送告诉的动画只有 24Hz,感觉就会很刺眼。在这种状况下,平台会抉择让屏幕上的内容都显示良好的刷新率。
为此,利用可能须要晓得以后设施的刷新率。能够通过以下办法来实现:
-
SDK
- 通过 DisplayManager.DisplayListener) 注册一个显示监听器,并通过 Display.getRefreshRate) 查问刷新率。
-
NDK
- 应用 AChoreographer_registerRefreshRateCallback 注册回调 (API 级别 30)。
利用能够通过在其 Window 或 Surface 上设置帧率来影响设施刷新率。这是 Android 11 中引入的一个新性能,容许平台理解利用的渲染需要。利用能够调用以下办法之一:
-
SDK
- Surface.setFrameRate)
- SurfaceControl.Transaction.setFrameRate)
-
NDK
- ANativeWindow_setRrameRate
- ASurfaceTransaction_setFrameRate
对于如何应用这些 API,请参考 帧率指南 文档。
零碎会依据 Window 或 Surface 上设置的帧率抉择最合适的刷新率。
在较旧的 Android 版本 (Android 11 之前) 中并不存在 setFrameRate API,这时利用依然能够通过间接将 WindowManager.LayoutParams.preferredDisplayModeId 设置为 Display.getSupportedModes) 中的可用模式之一来影响刷新率。从 Android 11 开始,咱们不倡议大家采纳这种办法,因为平台会不晓得利用的渲染用意。例如,如果一个设施反对 48Hz、60Hz 和 120Hz,屏幕上有两个利用别离调用 setFrameRate(60, …) 和 setFrameRate(24, …),那么平台能够抉择 120Hz 来同时满足这两个利用。而如果这些利用应用了 preferredDisplayModeId,它们很可能会把模式设置为 60Hz 和 48Hz,那这时平台就无奈应用 120Hz 了。这时平台只能从 60Hz 或 48Hz 中抉择一个,从而影响到另一个利用的显示成果。
总结
刷新率不肯定是 60Hz——不要想当然地认为它肯定会是 60Hz,也不要基于历史教训作出硬性假如。
刷新率并不总是恒定的——如果您想理解理论的刷新率,就须要注册一个回调来通晓刷新率的变动,并相应地更新您利用外部的数据。
如果您没有应用 Android UI 工具包,而应用自定义的渲染器,请思考依据以后的刷新率来扭转您的渲染流水线。通过应用 OpenGL 上的 eglPresentationTimeANDROID 或 Vulkan 上的 VkPresentationTimesInfoGOOGLE 设置一个出现工夫戳,即可深入流水线。设置出现工夫戳能够向 SurfaceFlinger 批示何时出现图像。如果设置为将来的几帧,它就会依照设置的帧数加深流水线。前文例子中的 Android UI 将出现工夫设置成了 frameTimeNanos + 2 * vsyncPeriod。
注: frameTimeNanos 从 Choreographer 获取;vsyncPeriod 从 Display.getRefreshRate() 获取。
应用 setFrameRate API 通知平台您的渲染用意,平台会抉择适合的刷新率来匹配不同的需要。
您应该只在必要时才应用 preferredDisplayModeId: 当 setFrameRate API 不可用时,或是当您须要应用十分特定的模式时。
最初,请您深刻理解一下 Android 的帧同步库。这个库能够为您的游戏妥善处理帧同步,并应用前文中的办法来解决多种刷新率。