主页 > 新闻资讯 > ​在 Android 上进行高刷新率渲染

​在 Android 上进行高刷新率渲染

作者/AdyAbraham,SoftwareEngineer
长久以来,手机屏幕刷新率都是60Hz。应用和游戏开发者也习惯了假定刷新率为60Hz,也就是每16.6ms生成一帧,而且这样开发出来的应用和游戏都会正常进行。
但现在的情况已经不同了。最新的旗舰级设备往往会搭载刷新率更高的屏幕,可以带来更流畅的动画效果、更低的延迟,从而获得更好的整体用户体验。还有一些设备支持可变刷新率,比如Pixel4,它支持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来生成一帧。为了详细说明这一点,我们来看看AndroidUI的渲染流水线。我们可以将帧渲染大致分为五个流水线阶段:
 
应用的UI线程处理输入事件,调用应用的回调,并更新视图(View)层次结构中记录的绘图命令列表;
 
应用的RenderThread将记录的命令发送到GPU;
 
GPU绘制这一帧;
 
SurfaceFlinger是负责在屏幕上显示不同应用窗口的系统服务,它会组合出屏幕应该最终显示出的内容,并将画面提交给屏幕的硬件抽象层(HAL);
 
屏幕最终呈现该帧的内容。
 
整个流水线由AndroidChoreographer控制。Choreographer基于显示垂直同步(vsync)事件,它表示屏幕开始扫描出图像并更新显示像素的时间点。虽然Choreographer基于vsync事件,但对应用和SurfaceFlinger来说,其唤醒偏移量不同。下图展示了在Pixel4设备上运行的流水线,应用在vsync事件后2ms被唤醒,SurfaceFlinger则在vsync事件后6ms被唤醒。这样一来,应用产生一帧画面的时间为20ms,SurfaceFlinger组合画面内容的时间则为10ms。当以90Hz频率运行时,应用依然在vsync事件后2ms被唤醒。然而,SurfaceFlinger在vsync事件后1ms被唤醒,同样有10ms的时间来合成屏幕内容。但这样一来应用只有10ms来渲染一帧画面,这时间就非常窘迫了:为了缓解这种情况,Android的UI子系统采用了预先渲染(renderahead,指维持一帧的启动时间不变,但推迟其呈现时间)来深化流水线,并将帧的呈现时间推迟一个vsync。这样一来,应用可以有21ms的时间来生成一帧,同时确保维持90Hz的吞吐量。一些应用,包括大多数游戏,都有自己自定义的渲染流水线。这些流水线可能会有更多或更少的阶段,具体取决于它们要完成的任务。一般来说,流水线越深,可以并行执行的阶段就越多,整体的吞吐量也会相应增加。但另一方面,这样可能会增加单帧的延迟(延迟量为number_of_pipeline_stagesxlongest_pipeline_stage)。这中间如何取舍需要开发者审慎考虑。利用可变刷新率如上所述,可变刷新率允许我们使用更多样的渲染频率。对于可以控制渲染速度的游戏,以及需要以特定速率呈现内容的视频播放器来说,这一点尤其有用。例如,要在60Hz的显示器上播放24fps的视频,我们需要使用3:2pulldown算法,这就会产生抖动。但是,如果设备的屏幕可以原生显示24fps的内容(24/48/72/120Hz),就无需使用pulldown算法,自然也就不会出现抖动了。
 
3:2pulldown算法https://en.wikipedia.org/wiki/Three-two_pull_down
 
设备运行时的刷新率是由Android平台控制的。应用和游戏可以通过多种方法影响刷新率(下面会有解释),但最终结果由平台决定。尤其是当屏幕上同时有多个应用时,这一点至关重要:平台需要满足所有应用的刷新率需求。24fps视频播放器就是一个很好的例子。24Hz对于视频播放来说可能很好,但对于响应式UI来说就很糟糕了。如果一个推送通知的动画只有24Hz,感觉就会很扎眼。在这种情况下,平台会选择让屏幕上的内容都显示良好的刷新率。为此,应用可能需要知道当前设备的刷新率。可以通过以下方法来实现:
 
SDK
 
通过DisplayManager.DisplayListener注册一个显示监听器,并通过Display.getRefreshRate查询刷新率。
 
NDK
 
使用AChoreographer_registerRefreshRateCallback注册回调(API级别30)。
 
DisplayManager.DisplayListenerhttps://developer.android.google.cn/reference/android/hardware/display/DisplayManager#registerDisplayListener(android.hardware.display.DisplayManager.DisplayListener,%20android.os.Handler)
 
Display.getRefreshRatehttps://developer.android.google.cn/reference/android/view/Display#getRefreshRate
 
AChoreographer_registerRefreshRateCallbackhttps://developer.android.google.cn/ndk/reference/group/choreographer#achoreographer_registerrefreshratecallback
 
应用可以通过在其Window或Surface上设置帧率来影响设备刷新率。这是Android11中引入的一个新功能,允许平台了解应用的渲染需求。应用可以调用以下方法之一:
 
SDK
 
Surface.setFrameRatehttps://developer.android.google.cn/reference/android/view/Surface#setFrameRate(float,%20int)
 
SurfaceControl.Transaction.setFrameRatehttps://developer.android.google.cn/reference/android/view/SurfaceControl.Transaction.html#setFrameRate(android.view.SurfaceControl,%20float,%20int)
 
NDK
 
ANativeWindow_setRrameRatehttps://developer.android.google.cn/ndk/reference/group/a-native-window#anativewindow_setframerate
 
ASurfaceTransaction_setFrameRatehttps://developer.android.google.cn/ndk/reference/group/native-activity#asurfacetransaction_setframerate
 
关于如何使用这些API,请参考帧率指南文档。
 
帧率指南https://developer.android.google.cn/guide/topics/media/frame-rate
 
系统会根据Window或Surface上设置的帧率选择最合适的刷新率。在较旧的Android版本(Android11之前)中并不存在setFrameRateAPI,这时应用仍然可以通过直接将WindowManager.LayoutParams.preferredDisplayModeId设置为Display.getSupportedModes中的可用模式之一来影响刷新率。从Android11开始,我们不建议大家采用这种方法,因为平台会不知道应用的渲染意图。例如,如果一个设备支持48Hz、60Hz和120Hz,屏幕上有两个应用分别调用setFrameRate(60,…)和setFrameRate(24,…),那么平台可以选择120Hz来同时满足这两个应用。而如果这些应用使用了preferredDisplayModeId,它们很可能会把模式设置为60Hz和48Hz,那这时平台就无法使用120Hz了。这时平台只能从60Hz或48Hz中选择一个,从而影响到另一个应用的显示效果。
 
WindowManager.LayoutParams.preferredDisplayModeIdhttps://developer.android.google.cn/reference/android/view/WindowManager.LayoutParams#preferredDisplayModeId
 
Display.getSupportedModeshttps://developer.android.google.cn/reference/android/view/Display#getSupportedModes
 
总结刷新率不一定是60Hz——不要想当然地认为它一定会是60Hz,也不要基于历史经验作出硬性假设。刷新率并不总是恒定的——如果您想了解实际的刷新率,就需要注册一个回调来知晓刷新率的变动,并相应地更新您应用内部的数据。如果您没有使用AndroidUI工具包,而使用自定义的渲染器,请考虑根据当前的刷新率来改变您的渲染流水线。通过使用OpenGL上的eglPresentationTimeANDROID或Vulkan上的VkPresentationTimesInfoGOOGLE设置一个呈现时间戳,即可深化流水线。设置呈现时间戳可以向SurfaceFlinger指示何时呈现图像。如果设置为未来的几帧,它就会按照设置的帧数加深流水线。前文例子中的AndroidUI将呈现时间设置成了frameTimeNanos+2*vsyncPeriod。注:frameTimeNanos从Choreographer获取;vsyncPeriod从Display.getRefreshRate获取。
 
eglPresentationTimeANDROIDhttps://www.khronos.org/registry/EGL/extensions/ANDROID/EGL_ANDROID_presentation_time.txt
 
VkPresentationTimesInfoGOOGLEhttps://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkPresentTimesInfoGOOGLE.html
 
使用setFrameRateAPI告诉平台您的渲染意图,平台会选择合适的刷新率来匹配不同的需求。您应该只在必要时才使用preferredDisplayModeId:当setFrameRateAPI不可用时,或是当您需要使用非常特定的模式时。最后,请您深入了解一下Android的帧同步库。这个库可以为您的游戏妥善处理帧同步,并使用前文中的方法来处理多种刷新率。
 
实现适当的帧同步