给Android应用和游戏开发工程师的最新参考。

Android屏幕刷新率java代码 安卓显示屏幕刷新率_android

       长时间以来,手机显示屏幕的刷新率一直是固定的60Hz. 从2019年开始,由于游戏等应用对响应速度的需求越来越高,越来越多的手机开始在高刷新率(90/120Hz)的屏幕硬件上进行尝试,其中典型厂商的有小米、三星、摩托罗拉等。国内的天马、BOE等屏幕生产商也开始有能力供货高刷新率屏幕面板。

       当手机屏幕都是60Hz刷新率的时候,应用和游戏开发者只需要认定每一帧的处理时间16.6毫秒,并据此来优化自己的代码,显示的一切就都可以掌控。但以后就不是这样了。新的旗舰手机甚至中低档手机,都开始装配高刷新率的显示屏幕来提供更流畅动画,更低的延迟,更优秀的整体用户体验。有些手机更是同时支持多种刷新率,比如Pixel 4,Motorola edge,都同时支持60Hz 和 90Hz. 作者本次就是拿Pixel 4和Motorola edge作为参考机来了解高刷新率feature的。

       运行在60Hz刷新率模式的屏幕,每16.6毫秒刷新一次显示内容。这意味着一副画面显示的时间长度都是16.6毫秒的整数倍(16.6毫秒,33.3毫秒,50毫秒,等等)。但对于支持多刷新率的屏幕,想要保证显示刷新的速度和流畅性,应用开发工程师则需要考虑更多。比如,游戏开发工程师通常简单地假设手机一直以60Hz刷新,当在游戏运行中发现不能维持60fps流畅帧率的时候,就把游戏画面的帧率降低到30fps,从而保证用户仍然不会感觉到卡顿(因为此时屏幕每16.6毫秒更新一帧,那么更低的帧率就是每33.3毫秒更新一帧)。但一款游戏不能只适合运行在60Hz的设备上,当它被安装到90Hz刷新的手机上时,为了维持流畅的刷新,应用开发工程师需要把帧率降低到45fps(因为此时屏幕每11.1毫秒更新一帧,那么更低的帧率就是每22.2毫秒更新一帧)。在90hz的手机上,相同的游戏可以给用户提供更顺畅的显示体验(90/45fps比60/30fpx感觉更平滑)。一款同时支持90Hz和120Hz刷新率的手机,它在120、90、60(120/2)、45(90/2)、40(120/3)、30(90/3)、24(120/5)fps的帧率下,都能提供平滑的显示。

Rendering at high rates

       Render的速度越快,越难以维持该render速度,这是因为更高的render速度意味着同样的工作但更短的处理时间。在90Hz的刷新率下,应用只有11.1毫秒来处理完一帧数据。而在60Hz的刷新率下,应用就有16.6毫秒来完成相同的工作。

       为了更直观的说明问题,我们来看一下Android UI的render流水线,把应用的帧显示过程拆分成5个阶段:

  1. 应用的UI thread 收到 input events,调用应用的callbacks并更新 the View hierarchy’s list of recorded drawing commands;
  2. 应用的RenderThread发送 the recorded commands到GPU;
  3. GPU进行绘图工作;
  4. 负责各应用窗口显示工作的系统服务SurfaceFlinger,对各应用的显示内容进行合成并把合成后的frame发送给HAL(有些硬件合成的过程就是往屏幕发送数据的过程);
  5. frame开始扫描并更新到屏幕上(比如通过MIPI接口)。

       整个流水线是由Android Choreographer控制的. Choreographer的运行又基于屏幕的vertical synchronization (vsync) events。vsync event代表显示硬件开始扫描输出图像到屏幕上(一行一行的显示). 基于vsync events,应用和SurfaceFlinger有不同的唤醒时间偏移量。下图显示了Pixel4在60Hz刷新率模式下的帧处理流水线。 应用在vsync event 发生后2毫秒被唤醒,而SurfaceFlinger是在vsync event发生后的6毫秒被唤醒. 这样应用就有20毫秒(阶段2+阶段3)的时间去处理一帧数据,而SurfaceFlinger也有10毫秒的时间去合成显示数据(阶段4)。这个处理时间对于一般的硬件平台都是可以胜任的。

Android屏幕刷新率java代码 安卓显示屏幕刷新率_Android屏幕刷新率java代码_02

       当手机运行在90Hz刷新率模式下的时候,应用仍是是在vsync event发生后2毫秒被唤醒,而SurfaceFlinger是在vsync event发生后的1毫秒被唤醒,这样为了给Surface留下足够的10毫秒时间去合成数据。但是,如果仍然要求数据在第2帧就往屏幕上发送的话,应用就只有10毫秒的时间去画图了,如下图。 这是一个非常短的时间,稍微复杂一些的显示工作,对于大部分平台的AP和GPU性能,都难以这么短的时间内完成画图工作。

Android屏幕刷新率java代码 安卓显示屏幕刷新率_framebuffer_03

       为了减轻画图的负担,Android的UI系统(hwui) 使用一种 “render ahead”机制 (SurfaceFlinger合成时间推迟一帧,但仍然在vsync event发生后1毫秒开始) 把显示时间延迟一个vsync period来拉长一帧数据的流水线时长. 这样应用就有21ms的时间去处理数据(画图,包括AP render和GPU渲染),而仍然能保持90Hz的帧率。

Android屏幕刷新率java代码 安卓显示屏幕刷新率_android_04

       有些应用,包括大部分游戏,会根据他们的画图的工作量来定制自己的render流水线,增加或减少render阶段数量。一般来说,因为画图工作量太大,会增加render流水线的长度,即在阶段2和阶段3增加并行处理阶段来提升计算效率。然而,这样的折中方案又会引入更大的帧延时 (the latency will be number_of_pipeline_stages x longest_pipeline_stage),需要综合权衡考量。

Taking advantage of multiple refresh rates

       如上所述,多刷新率的支持,给应用程序提供了更多的可用render帧率选择。这一点对于经常需要改变显示帧率的游戏应用,和需要以一个固定的帧率来显示内容的视频应用来说尤其有用。例如,要在60Hz的刷新率模式下播放24fps的视频,则需要使用3:2 pulldown算法(如下图),理论上说是有抖动的(但一般人感觉不到)。然而,如果设备支持合适的屏幕刷新率并因而可以支持相匹配的render帧率(比如120Hz正好是24的倍频)的话,则不再需要pulldown算法,也就不会有抖动问题。

Android屏幕刷新率java代码 安卓显示屏幕刷新率_Android屏幕刷新率java代码_05

       设备何时以什么刷新率运行是Android平台控制的。应用或者游戏可以通过多种方法来影响刷新率的切换,最终的刷新率有Android平台来决定。当不止一个应用程序(比如Motorola的多窗口模式)在屏幕上显示时,这一点很关键,平台需要给出一个能够让所有前台应用流畅显示的刷新率。假设有个手机支持24Hz的屏幕刷新率,我们拿一个把平台刷新率设置为24Hz的视频播放器应用为例,当它播放24fps的视频时,我们肯定感觉非常流畅。但是24fps对于手机的系统信息通知动画的播放来说,就很糟糕了,我们能感觉弹出信息框到抖动。在这种情况下,平台需要能综合考虑各种因素,给出一个合适的刷新率来满足不同的内容,从而让屏幕上的不同内容看起来都很流程。

       基于这个原因,应用很可能想知道设备当前的刷新率,据此来调整自己的显示速度。有以下方法可以拿到这个信息:

  • SDK:
  • NDK

       应用也可以通过window或者Surface的接口来改变设备刷新率。这是在Android R上新引入的接口。这个接口是的Android系统能够知道调用这些接口的应的帧率信息,从而更好的调整刷新率。应用可以调用以下接口:

  • SDK
  • NDK

       请参考 frame rate guide 来熟悉这些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,则系统最终只能选择60Hz 或者 48Hz,从而无法满足其中一个应用的需求。

Takeaways(划重点)

  • 屏幕刷新率不只是60Hz -------- 不要假设设备只支持60Hz,也不要依赖基于此假设的历史经验。
  • 屏幕刷新并不是固定不变的 ----------- 如果你的应用对刷新率敏感,你应该注册一个回调去监测刷新率的变化并依此对你的显示代码做相应的调整。
  • 如果你不是使用 Android UI toolkit 并自己定制了custom renderer,你最好根据系统刷新率的变化对你的rendering pipeline 进行调整。 你可以通过调用eglPresentationTimeANDROID或者VkPresentTimesInfoGOOGLE来设置presentation timestamp以增加pipeline的深度。 presentation timestamp 会告诉SurfaceFlinger什么时间显示这一帧图像。如果 它被设置为在将来的某个vsync period显示,就可以延长render流水线的时间,从而让应用有更多的时间去画图。在上面的Android UI 例子中,present time被设置成了 frameTimeNanos1 + 2 * vsyncPeriod2,即延迟2个vsync period再显示。
  • 使用setFrameRate API告诉平台你希望的帧率,平台会根据不用的帧率需求选择一个合适的刷新率。
  • 只有在 setFrameRate API不存在的时候,或者你想让系统运行在某个特定刷新率模式的时候,使用preferredDisplayModeId 。
  • 最后,你最好认真熟悉 Android Frame Pacing library。这个库负责你的游戏帧处理的合理调度处理,并使用前面提过的接口来处理多刷新率的切换策略。