做者 / Ady Abraham, Software Engineerhtml
長久以來,手機屏幕刷新率都是 60Hz。應用和遊戲開發者也習慣了假定刷新率爲 60Hz,也就是每 16.6ms 生成一幀,並且這樣開發出來的應用和遊戲都會正常進行。但如今的狀況已經不一樣了。最新的旗艦級設備每每會搭載刷新率更高的屏幕,能夠帶來更流暢的動畫效果、更低的延遲,從而得到更好的總體用戶體驗。還有一些設備支持可變刷新率,好比 Pixel 4,它支持 60Hz 和 90Hz 兩種刷新率。android
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 來生成一幀。ide
爲了詳細說明這一點,咱們來看看 Android UI 的渲染流水線。咱們能夠將幀渲染大體分爲五個流水線階段:工具
整個流水線由 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 來渲染一幀畫面,這時間就很是窘迫了:ui
爲了緩解這種狀況,Android 的 UI 子系統採用了預先渲染 (render ahead,指維持一幀的啓動時間不變,但推遲其呈現時間) 來深化流水線,並將幀的呈現時間推遲一個 vsync。這樣一來,應用能夠有 21ms 的時間來生成一幀,同時確保維持 90Hz 的吞吐量。google
一些應用,包括大多數遊戲,都有本身自定義的渲染流水線。這些流水線可能會有更多或更少的階段,具體取決於它們要完成的任務。通常來講,流水線越深,能夠並行執行的階段就越多,總體的吞吐量也會相應增長。但另外一方面,這樣可能會增長單幀的延遲 (延遲量爲 number_of_pipeline_stages x longest_pipeline_stage)。這中間如何取捨須要開發者審慎考慮。spa
如上所述,可變刷新率容許咱們使用更多樣的渲染頻率。對於能夠控制渲染速度的遊戲,以及須要以特定速率呈現內容的視頻播放器來講,這一點尤爲有用。例如,要在 60Hz 的顯示器上播放 24fps 的視頻,咱們須要使用 3:2 pulldown 算法,這就會產生抖動。可是,若是設備的屏幕能夠原生顯示 24fps 的內容 (24/48/72/120Hz),就無需使用 pulldown 算法,天然也就不會出現抖動了。線程
設備運行時的刷新率是由 Android 平臺控制的。應用和遊戲能夠經過多種方法影響刷新率 (下面會有解釋),但最終結果由平臺決定。尤爲是當屏幕上同時有多個應用時,這一點相當重要: 平臺須要知足全部應用的刷新率需求。24fps 視頻播放器就是一個很好的例子。24Hz 對於視頻播放來講可能很好,但對於響應式 UI 來講就很糟糕了。若是一個推送通知的動畫只有 24Hz,感受就會很扎眼。在這種狀況下,平臺會選擇讓屏幕上的內容都顯示良好的刷新率。
爲此,應用可能須要知道當前設備的刷新率。能夠經過如下方法來實現:
SDK
NDK
應用能夠經過在其 Window 或 Surface 上設置幀率來影響設備刷新率。這是 Android 11 中引入的一個新功能,容許平臺瞭解應用的渲染需求。應用能夠調用如下方法之一:
SDK
NDK
關於如何使用這些 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 的幀同步庫。這個庫能夠爲您的遊戲妥善處理幀同步,並使用前文中的方法來處理多種刷新率。