前言
本文將會作為開啟SurfaceFlinger的系列第一篇文章。然而SurfaceFlinger幾乎貫通了整個Android領域中所有的知識。從HAL硬件抽象層到Framework層,從CPU繪制到OpenGL等硬件繪制。
為了讓整個系列的書寫更有邏輯性。這一次我將一反常態,先把整個架構的設計思想概述寫出來,作為后面的系列文章的指導。本文之后都會將SurfaceFlinger稱為SF。
遇到什么疑問,可以來本文下討論:http://www.lxweimin.com/p/c954bcceb22a
正文
SF的渲染第一定律:
SF是整個Android系統渲染的核心進程。所有應用的渲染邏輯最終都會來到SF中進行處理,最終會把處理后的圖像數據交給CPU或者GPU進行繪制。
姑且讓我們先把這句話當作Android渲染系統的第一定律。SF在整個Android系統中,并非擔當渲染的角色,而是作為圖元拋射機一樣,把所有應用進程傳遞過來的圖元數據加工處理后,交給CPU和GPU做真正的繪制。
SF的渲染第二定律:
在每一個應用中都以Surface作為一個圖元傳遞單元,向SF這個服務端傳遞圖元數據。
這是Android渲染體系的第二定律。把這兩個規律組合起來就是如下一個簡單示意圖。
SF的渲染第三定律:
SF是以生產者以及消費者為核心設計思想,把每一個應用進程作為生產者生產圖元保存到SF的圖元隊列中,SF則作為消費者依照一定的規則把生產者存放到SF中的隊列一一處理。
用圖表示就如下:
SF體系渲染的第四定律:
為了能夠跨進程的傳輸大容量的圖元數據,使用了匿名共享內存內存作為工具把圖元數據都輸送到SF中處理。
眾所周知,我們需要從應用進程跨進程把圖元數據傳輸到SF進程中處理,就需要跨進程通信,能考慮的如socket這些由于本身效率以及數據拷貝了2份(從物理內存頁層面上來看),確實不是很好的選擇。一個本身拷貝大量的數據就是一個疑問。那么就需要那些一次拷貝的進程間通信方式,首先能想到的當然是Binder,然而Binder進程間通信,特別是應用的通信數據總量只有1M不到的大小加上應用其他通信,勢必會出現不足的問題。
為了解決這個問題,Android使用共享內存,使用的是匿名共享內存(Ashmem)。匿名共享內存也是一種拷貝一次的進程間通信方式,其核心比起binder的復雜的mmap更加接近Linux的共享內存的概念。
SF體系渲染的第五定律:
SF底層有一個時間鐘在不斷的循環,或從硬件中斷發出,或從軟件模擬發出計時喚起,每隔一段時間都會獲取SF中的圖元隊列通過CPU/GPU繪制在屏幕。
第五定律的誕生實際上很符合Android系統的設計情況,除了需要Android應用有辦法通知SF需要渲染的模式,當然需要SF自己不斷的把圖元繪制到屏幕的行為的自己回調自己的行為,SF自己不斷的繪制在SF中的圖元數據。
其中EventThread扮演一個極其重要的角色,在SF中設計大致如下:
Vsync的介紹
這里面出現了一個新的名次VSync,其實這就是我們玩游戲經常說的垂直同步信號。我以前用渣電腦玩游戲的時候,經常掉幀數卡的不行,之后關閉了垂直信號后感覺好了點,讓我有一段時間以為這是個不好的東西。
這里就先介紹一下Android曾經迭代為ui體驗更好上的努力,黃油計劃。黃油計劃故名思議就是為了讓系統的ui表現如黃油表面一樣順滑。為此誕生了兩個重要的概念Vsync以及Triple Buffer,即垂直信號和三重緩沖。
雙緩沖的概念大家應該都熟悉,在OpenGL我已經解釋過了,雙緩沖就是渲染第一幀的同時已經在繪制第二幀的內容,等到第二幀繪制完畢后就顯示出來。這么做的好處很明顯,如果一幀畫完,才開始畫下一幀,勢必有一個計算的過程導致ui交互遲緩。
通過這種方式顯示前一幀的時候提前繪制好下一幀圖元,放在背后等待時機交換,這樣就能從感官上流暢不少。
這么做理想十分顯示,但是怎么找到一個合適的時機進行交換前后兩幀這是一個問題?如果有人在想那就按照屏幕刷新頻率來,一般按照通用屏幕刷新60fps也就是約16ms刷新一次即可。
理想是很豐滿,但是現實很骨干,這么做好像沒有問題,我們深入考慮一下,其實這個過程中有兩個變量,一個是繪圖速度,一個是顯示速度。就算是繪圖速度中也有分CPU和GPU的繪制速度。
這里就沿用一下當年google在宣傳黃油計劃時候的示意圖。讓我們先看看沒有緩沖正常運作的示意圖:
最好的情況就是上圖,在顯示第0幀的時候,CPU/GPU合成繪制完成第1幀在16ms內,當vsync信號來了,就把第1幀交換到顯示屏顯示。
vsync是什么?玩游戲的時候經常看到垂直同步就是它。它的作用是通過屏幕硬件中斷來告訴系統應該什么時候刷新屏幕。通過這樣的方式,大致上16ms的發送一次中斷讓系統刷新。
但是很可能出現下面這種情況,CPU因為繁忙來不及,顯示完第一幀的時候,還沒空渲染第二幀,就算SF接受到了Vsync的信號,也只能拿出已經渲染好的第一幀顯示在屏幕上。這樣就重復顯示了第一幀,Google開發團隊稱這種為jank。
能看到顯示第一幀因為第二幀沒準備好,只能重復顯示第一幀了。
再來看看帶著多重緩沖的的工作原理流程:
能看到此時就不是簡單的第一第二幀,而是分為A緩沖,B緩沖。能看到在正常情況下,先顯示A緩沖的內容,同時準備B緩沖,當一切正常的時候,B緩沖應該在下一個vsync來之前準備好,一旦vsync到來則顯示B緩沖,A緩沖回到后臺繼續繪制。
那么這種方式一旦遇到jank會是怎么一個情況呢?
如果是雙緩沖好像沒有問題,但是一旦出現jank了之后,之后顯示屏就會不斷的出現jank。如果緩沖A在顯示,而B準備的時間超過16ms,就會導致A緩沖區重復顯示,而B當b顯示的時候,A也很可能準備時間不足16ms導致無法繪制完成,只能重復顯示B緩沖的內容。
這種方式更加的危險,為了解決這個問題,Google引入三重緩沖。
當三重緩沖處理jank的原理流程圖:
能看到為了避免后面連鎖式的錯誤,引入三重緩沖就為了讓空閑出來的等待時間,能夠做更多的事情。就如同雙緩沖遇到jank之后,一旦B緩沖CPU+GPU的時間超過了下一個vsync的時間,能夠發現其實CPU和GPU有一段時間都沒有事情做,光等待下一次Vsync的到來,才會導致整個系統后面的繪制出現連鎖式的出現jank。
而三緩沖的出現,在重復顯示A緩沖區的時候,CPU不會光等待而是會準備C緩沖區的圖元,之后就能把C緩沖區接上。這就是Google所說的三重緩沖區的來源。
不過絕大多數情況都緩沖策略是由SF系統自己決定的,一般我們常說的雙緩沖,三緩沖指的就是這個。
實際上這種方式也可以用到音視頻的編寫優化,里面常用的緩沖區設計和這里也有同工異曲的之妙,但是沒有系統如此極致。如果閱讀過系統的videoView源碼就能看到NullPlayer本質上就是借助Surface圖元緩沖區來達到極致的體驗,不過VideoView也有設計不合理的地方,之后研讀完Android的渲染體系,讓我們來分析分析這些源碼。
但是這一部分的知識,不足以讓我們去理解定律5.其實每一次Vsync從硬件/軟件過來的時候,Dispsync都會嘗試著通知SF和app,這是完全沒有問題,但是后面那個Phase相位又是什么東西?
其實這就是系統的設計的巧妙,我們如果同時把信號通知同時告訴app和sf會導致什么結果?
如果此時app后返回了圖元,但是sf已經執行了刷新合成繪制行為(很有可能,因為app到sf傳輸圖元速度必定比sf自己通知自己慢),此時就會導致類似jank的問題,導致下一個vsync還是顯示當前幀數,因此需要如下一個時間差,先通知app后通知sf,如下圖:
加上這個理解就能明白第五定律。關于第五點的討論,在Vsync同步信號原理有詳細討論。
小結
這五大定律是指導SF設計的核心思想,從Android4.1一直到9.0都沒有太大的變化。只要抓住這五個核心思想,我們閱讀起SF的難度就會下降不少。
那么SF的體系和我之前聊過的Skia有什么關系呢?又和頂層的View的繪制流程有什么關系呢?
我們按照角色區分一下:
- framework面向開發者所有的View是便于開發的控件,里面僅僅只是提供了當前View各種屬性以及功能。
- 而Android底層的Skia是Android對于屏幕上的畫筆,經過View繪制流程的onDraw方法回調,把需要繪制的東西通過Skia繪制成像素圖元保存起來
- SF則是最后接受Skia的繪制結果,最后繪制到屏幕上。
所以說,Skia是Android渲染核心這句話沒錯,但是最終還是需要Skia和系統所提供起來,才是一個Android完整渲染體系。
經過這一層層的屏蔽,讓開發者不需要對Android底層的渲染體系有任何理解,也能繪制出不錯的效果。
最后會把繪制結果傳輸到屏幕中。
因此,本次計劃將會從底層核心,慢慢向上剖析,直到View的繪制流程,讓我們那徹底通讀整個android的渲染體系。
計劃
本次計劃SurfaceFlinger的文章將會通過如下模塊一一解析(但是不代表一個模塊就只有一篇,也不代表最終順序,僅僅代表你將會閱讀到什么內容):
-
圖元核心傳輸工具,匿名共享內存ashmem驅動的核心原理,ashmem原理圖大致如下:
ashmem.png
-
詳見匿名內存ashmem源碼分析。然而在Android高版本,已經放棄了ashemem,改用ion驅動。ion的原理圖大致如下:
關于ion的分析,詳見ion驅動源碼淺析
ion實際上是生成DMA直接訪問內存。原本ashmem的方式需要從GPU訪問到CPU再到內存中的地址。但是在這里就變成了GPU直接訪問修改DMA,CPU也能直接修改DMA。這就是最大的變化。
- SurfaceFlinger的啟動。
詳見SurfaceFlinger 的初始化,原理圖大致如下:
SF 初始化結構.png
- SurfaceFlinger的啟動。
- 開機沒有Activity,只能直接使用SF機制加上OpenGL es顯示開機動畫,來看看從linux開機動畫到Android開機動畫 BootAnimation 。
詳見系統啟動動畫,原理圖大致如下:
開機動畫啟動原理.jpg
- 開機沒有Activity,只能直接使用SF機制加上OpenGL es顯示開機動畫,來看看從linux開機動畫到Android開機動畫 BootAnimation 。
- 理解應用進程如何和SF構建起聯系。
詳見Vsync同步信號原理。SF是通過一個名為Choreographer監聽VSync進而得知繪制周期的。原理圖大致如下:
VSync回調機制.jpg
- 理解應用進程如何和SF構建起聯系。
- SF硬件抽象層hal的理解和運作,理解SF如何和底層HWC/fb驅動關聯起來。
詳見SurfaceFlinger 的HAL層初始化
其核心數據結構如下:
底層硬件回調和SF之間的關聯原理圖如下:
- SF是如何連通DisplayManagerService[略,之后有機會進行補充],只是簡單的通過SurfaceFlinger獲取屏幕信息放在Framework層管理。
- Android端在opengl es的核心原理,看看Android對opengl es上做了什么封裝。
這個模塊分為兩部分解析:
一個是正常的OpenGL es使用流程中,軟件模擬每一個關鍵步驟的工作原理是什么,Android在其中進行了什么優化。詳見OpenGL es上的封裝(上)
其中有一個十分關鍵的數據結構,UML圖如下:
紋理結構.png
- Android端在opengl es的核心原理,看看Android對opengl es上做了什么封裝。
一個紋理在OpenGL es中是如何合成繪制的,并且Android進行了本地紋理的優化,詳見OpenGL es上的封裝(下),整個OpenGL es的繪制原理如下:
- 圖元是怎么通過hal層生產出圖元數據;應用的圖元數據又是獲取到應用,如何進入SurfaceFlinger的緩沖隊列。
詳見GraphicBuffer的誕生。其中涉及了幾個重要的數據結構:
GraphicBuffer生成體系.png
- 圖元是怎么通過hal層生產出圖元數據;應用的圖元數據又是獲取到應用,如何進入SurfaceFlinger的緩沖隊列。
同時運行原理圖如下:
- 應用的圖元數據是如何消費的。
詳見圖元的消費,交換緩沖繪制參數,本質是取出一個GraphicBuffer存到緩沖隊列的時間和當前時間預計顯示最接近的一個,渲染到屏幕中。同時把上一幀的GraphicBuffer放到空閑隊列中。
- 應用的圖元數據是如何消費的。
其中,我們需要記住下面這個SF中緩沖隊列設計的數據結構:
- SF是如何通過HWC合成圖層,如何合并各個Layer,輸出到opengles中處理。
大致上可以分為如下如下7步驟:
1.preComposition 預處理合成
2.rebuildLayerStacks 重新構建Layer棧
3.setUpHWComposer HWC的渲染或者準備
這三步驟,我稱為繪制準備,詳見圖元的合成(上) 繪制的準備
在繪制準備的過程中,最重要的是區分了如下幾種繪制模式,已經存儲相關的數據到HWC的Hal層中。
- SF是如何通過HWC合成圖層,如何合并各個Layer,輸出到opengles中處理。
Composition的Layer的Type | hasClientComposition | hasDeviceComposition | 渲染方式 |
---|---|---|---|
HWC2::Composition::Client | true | - | OpenGL es |
HWC2::Composition::Device | - | true | HWC |
HWC2::Composition::SolidColor | - | true | HWC |
HWC2::Composition::Sideband | - | true | HWC或者OpenGL es |
4.doDebugFlashRegions 打開debug繪制模式
5.doTracing 跟蹤打印
6.doComposition 合成圖元
7.postComposition 圖元合成后的vysnc等收尾工作。
后面四個步驟,我們只需要關注最后兩個步驟即可。詳見圖元的合成(下)
整一套的從消費到合成的流程原理圖大致如下:
在合成的過程中,分為HWC和OpenGL es兩種,兩者負責的角色大致如下:
當然,在Android渲染體系中,也不是只有一對生產者消費者模型:
- SF的Vsync原理,以及相位差計算原理
整個VSync發送中有三種發送周期:硬件發送VSync周期,軟件發送VSync周期,app處理VSync周期,sf處理VSync周期。
詳見Vsync同步信號原理
- SF的Vsync原理,以及相位差計算原理
Android為了方便,會暫時把整個周期看成一個周期連續性的函數,計算原理如下:
其實就是獲取每一個采樣點相位,計算采樣點相位的平均值就是理想相位。同理,周期也是計算采樣點的平均周期,從而計算出一個合適的軟件發送VSync軸。
最后在軟件渲染的基礎上,app的VSync和sf的VSync各自進行延時接受處理,避免出現定律的時序沖突,就是上面那一副藍色的圖。
- SF的fence 同步柵工作原理
詳見fence原理
想要弄懂Fence,需要先了解GraphicBuffer的狀態變更:
大致分為如下幾個狀態:dequeue(出隊到應用中繪制),queue(入隊到SF緩沖區等待消費),acquire(選擇渲染的GraphicBuffer),free(消費完畢后等待dequeue)
GraphicBuffer狀態流轉.png
- SF的fence 同步柵工作原理
Fence的狀態更簡單,有acquire,release,retried狀態流轉大致如下:
retried是每一次繪制完都會合并在一個不用的Fence中進行記錄。
總結一句話,Fence的acquire狀態其實是阻塞什么時候可以被消費,什么時候可以被渲染到屏幕;而Fence的release狀態則是控制什么時候可以出隊給應用進行繪制,什么時候可以被映射到內存。
只有理解這12點,才能說你了解SF了,也不能說精通,畢竟你沒辦法盲敲出來。
等這12點全部理解通之后,會開啟Skia新的篇章,來聊聊Skia的工作原理以及源碼解析,最后我們會回歸本源,來聊聊View的繪制流程以及WMS。
這個只是一個導讀,在這12個知識點背后藏著不少的東西,希望一個總綱能讓人有一個總攬,不至于迷失在源碼中。
后話
作為每一個經常和UI交互的工程師,有必要也必須要熟悉Android 的渲染原理,只有這樣才能讓我們寫出更加優秀代碼,特別是做音視頻的哥們,更加有必要閱讀這些代碼以及看看工業級別的Android的VideoView是如何設計的。其實我在學習一些關于音視頻資料的時候,用ffmpeg編寫一個視頻播放器,發現其實那些demo還有很多地方可以優化的,可以學習flutter如何工作的,如何依托自身平臺做進一步優化,而不是應該去做一個泛用的,還過得去的東西。
我會隨著進度不斷修改本文,本文不是最終版本,會不斷的添加不少設計示意圖以及UML圖。