圖形顯示過程
幀的渲染過程中一些關鍵組件的流程圖
Image Stream Producers(圖像生產者)
任何可以產生圖形信息的組件都統稱為圖像的生產者,比如OpenGL ES, Canvas 2D, 和 媒體解碼器等。
Image Stream Consumers(圖像消費者)
SurfaceFlinger是最常見的圖像消費者,Window Manager將圖形信息收集起來提供給SurfaceFlinger,SurfaceFlinger接受后經過合成再把圖形信息傳遞給顯示器。同時,SurfaceFlinger也是唯一一個能夠改變顯示器內容的服務。SurfaceFlinger使用OpenGL和Hardware Composer來生成surface.
某些OpenGL ES 應用同樣也能夠充當圖像消費者,比如相機可以直接使用相機的預覽界面圖像流,一些非GL應用也可以是消費者,比如ImageReader 類。
Window Manager
Window Manager是一個用于控制window的系統服務,包含一系列的View。每個Window都會有一個surface,Window Manager會監視window的許多信息,比如生命周期、輸入和焦點事件、屏幕方向、轉換、動畫、位置、轉換、z-order等,然后將這些信息(統稱window metadata)發送給SurfaceFlinger,這樣,SurfaceFlinger就能將window metadata合成為顯示器上的surface。
Hardware Composer
為硬件抽象層(HAL)的子系統。SurfaceFlinger可以將某些合成工作委托給Hardware Composer,從而減輕OpenGL和GPU的工作。此時,SurfaceFlinger扮演的是另一個OpenGL ES客戶端,當SurfaceFlinger將一個緩沖區或兩個緩沖區合成到第三個緩沖區時,它使用的是OpenGL ES。這種方式會比GPU更為高效。
一般應用開發都要將UI數據使用Activity這個載體去展示,典型的Activity顯示流程為:
- startActivity啟動Activity;
- 為Activity創建一個window(PhoneWindow),并在WindowManagerService中注冊這個window;
- 切換到前臺顯示時,WindowManagerService會要求SurfaceFlinger為這個window創建一個surface用來繪圖。SurfaceFlinger創建一個”layer”(surface)。(以想象一下C/S架構,SF對應Server,對應Layer;App對應Client,對應Surface),這個layer的核心即是一個BufferQueue,這時候app就可以在這個layer上render了;
將所有的layer進行合成,顯示到屏幕上。
一般app而言,在任何屏幕上起碼有三個layer:
- 屏幕頂端的status bar
- 屏幕下面的navigation bar
- 還有就是app的UI部分。
一些特殊情況下,app的layer可能多余或者少于3個,例如對全屏顯示的app就沒有status bar,而對launcher,還有個為了wallpaper顯示的layer。status bar和navigation bar是由系統進行去render,因為不是普通app的組成部分嘛。而app的UI部分對應的layer當然是自己去render,所以就有了第4條中的所有layer進行“合成”。
GUI框架
Hardware Composer
那么android是如何使用這兩種合成機制的呢?這里就是Hardware Composer的功勞。處理流程為:
- SurfaceFlinger給HWC提供layer list,詢問如何處理這些layer;
- HWC將每個layer標記為overlay或者GLES composition,然后回饋給SurfaceFlinger;
- SurfaceFlinger需要去處理那些GLES的合成,而不用去管overlay的合成,最后將overlay的layer和GLES合成后的buffer發送給HWC處理。
借用google一張圖說明,可以將上面講的很多概念展現,很清晰。地址位于 https://source.android.com/devices/graphics/
關于幀率
即 Frame Rate,單位 fps,是指 gpu 生成幀的速率,如 33 fps,60fps,越高越好。
但是對于快速變化的游戲而言,你的FPS很難一直保持同樣的數值,他會隨著你所看到的顯示卡所要描畫的畫面的復雜程度而變化。
VSync
安卓系統中有 2 種 VSync 信號:
- 屏幕產生的硬件 VSync: 硬件 VSync 是一個脈沖信號,起到開關或觸發某種操作的作用。
- 由 SurfaceFlinger 將其轉成的軟件 Vsync 信號:經由 Binder 傳遞給 Choreographer。
單層緩沖引發“畫面撕裂”問題
如上圖,CPU/GPU 向 Buffer 中生成圖像,屏幕從 Buffer 中取圖像、刷新后顯示。這是一個典型的生產者——消費者模型。理想的情況是幀率和刷新頻率相等,每繪制一幀,屏幕顯示一幀。而實際情況是,二者之間沒有必然的大小關系,如果沒有鎖來控制同步,很容易出現問題。
所謂”撕裂”就是一種畫面分離的現象,這樣得到的畫像雖然相似但是上半部和下半部確實明顯的不同。這種情況是由于幀繪制的頻率和屏幕顯示頻率不同步導致的,比如顯示器的刷新率是75Hz,而某個游戲的FPS是100. 這就意味著顯示器每秒更新75次畫面,而顯示卡每秒更新100次,比你的顯示器快33%。
雙緩沖
兩個緩存區分別為 Back Buffer 和 Frame Buffer。GPU 向 Back Buffer 中寫數據,屏幕從 Frame Buffer 中讀數據。VSync 信號負責調度從 Back Buffer 到 Frame Buffer 的復制操作,可認為該復制操作在瞬間完成。
雙緩沖的模型下,工作流程這樣的:
在某個時間點,一個屏幕刷新周期完成,進入短暫的刷新空白期。此時,VSync 信號產生,先完成復制操作,然后通知 CPU/GPU 繪制下一幀圖像。復制操作完成后屏幕開始下一個刷新周期,即將剛復制到 Frame Buffer 的數據顯示到屏幕上。
在這種模型下,只有當 VSync 信號產生時,CPU/GPU 才會開始繪制。這樣,當幀率大于刷新頻率時,幀率就會被迫跟刷新頻率保持同步,從而避免“tearing”現象。
VSYNC 偏移
應用和SurfaceFlinger的渲染回路必須同步到硬件的VSYNC,在一個VSYNC事件中,顯示器將顯示第N幀,SurfaceFlinger合成第N+1幀,app合成第N+2幀。
使用VSYNC同步可以保證延遲的一致性,減少了app和SurfaceFlinger的錯誤,以及顯示在各個階段之間的偏移。然而,前提是app和SurfaceFlinger每幀時間的變化并不大。因此,從輸入到顯示的延遲至少有兩幀。
為了解決這個問題,您可以使用VSYNC偏移量來減少輸入到顯示的延遲,其方法為將app和SurfaceFlinger的合成信號與硬件的VSYNC關聯起來。因為通常app的合成耗時是小于兩幀的(33ms左右)。
VSYNC偏移信號細分為以下3種,它們都保持相同的周期和偏移向量:
- HW_VSYNC_0:顯示器開始顯示下一幀。
- VSYNC:app讀取輸入并生成下一幀。
- SF VSYNC:SurfaceFlinger合成下一幀的。
收到VSYNC偏移信號之后, SurfaceFlinger 才開始接收緩沖區的數據進行幀的合成,而app才處理輸入并渲染幀,這些操作都將在16.7ms完成。
Jank 掉幀
注意,當 VSync 信號發出時,如果 GPU/CPU 正在生產幀數據,此時不會發生復制操作。屏幕進入下一個刷新周期時,從 Frame Buffer 中取出的是“老”數據,而非正在產生的幀數據,即兩個刷新周期顯示的是同一幀數據。這是我們稱發生了“掉幀”(Dropped Frame,Skipped Frame,Jank)現象。
流暢性解決方案思路
- 從dumpsys SurfaceFlinger --latency中獲取127幀的數據
- 上面的命令返回的第一行為設備本身固有的幀耗時,單位為ns,通常在16.7ms左右
- 從第二行開始,分為3列,一共有127行,代表每一幀的幾個關鍵時刻,單位也為ns
第一列t1: when the app started to draw (開始繪制圖像的瞬時時間)
第二列t2: the vsync immediately preceding SF submitting the frame to the h/w (VSYNC信令將軟件SF幀傳遞給硬件HW之前的垂直同步時間),也就是對應上面所說的軟件Vsync
第三列t3: timestamp immediately after SF submitted that frame to the h/w (SF將幀傳遞給HW的瞬時時間,及完成繪制的瞬時時間)
- 將第i行和第i-1行t2相減,即可得到第i幀的繪制耗時,提取出每一幀不斷地dump出幀信息,計算出
一些計算規則
計算fps:
每dumpsys SurfaceFlinger一次計算匯總出一個fps,計算規則為:
frame的總數N:127行中的非0行數
繪制的時間T:設t=當前行t2 - 上一行的t2,求出所有行的和∑t
fps=N/T (要注意時間轉化為秒)
計算中一些細節問題
一次dumpsys SurfaceFlinger會輸出127幀的信息,但是這127幀可能是這個樣子:
...
0 0 0
0 0 0
0 0 0
575271438588 575276081296 575275172129
575305169681 575309795514 575309142441
580245208898 580250445565 580249372231
580279290043 580284176346 580284812908
580330468482 580334851815 580333739054
0 0 0
0 0 0
...
575271438588 575276081296 575275172129
575305169681 575309795514 575309142441
出現0的地方是由于buffer中沒有數據,而非0的地方為繪制幀的時刻,因此僅計算非0的部分數據
觀察127行數據,會發現偶爾會出現9223372036854775808這種數字,這是由于字符溢出導致的,因此這一行數據也不能加入計算
不能單純的dump一次計算出一個fps,舉個例子,如果A時刻操作了手機,停留3s后,B時刻再次操作手機,按照上面的計算方式,則t>3s,并且也會參與到fps的計算去,從而造成了fps不準確,因此,需要加入一個閥值判斷,當t大于某個值時,就計算一次fps,并且把相關數據重新初始化,這個值一般取500ms
如果t<16.7ms,則直接按16.7ms算,同樣的總耗時T加上的也是16.7
計算jank的次數:
如果t3-t1>16.7ms,則認為發生一次卡頓
流暢度得分計算公式
設目標fps為target_fps,目標每幀耗時為target_ftime=1000/target_fps
從以下幾個維度衡量流暢度:
- fps: 越接近target_fps越好,權重分配為40%
- 掉幀數:越少越好,權重分配為40%
- 超時幀:拆分成以下兩個維度
- 超時幀的個數,越少越好,權重分配為5%
- 最大超時幀的耗時,越接近target_ftime越好,權重分配為15%
end_time = round(last_frame_time / 1000000000, 2)
T = utils.get_current_time()
fps = round(frame_counts * 1000 / total_time, 2)
# 計算得分
g = fps / target
if g > 1:
g = 1
if max_frame_time - kpi <= 1:
max_frame_time = kpi
h = kpi / max_frame_time
score = round((g * 50 + h * 10 + (1 - over_kpi_counts / frame_counts) * 40), 2)
參考文章:
http://windrunnerlihuan.com/2017/05/21/VSync%E4%BF%A1%E5%8F%B7/