作者:yearzhu呀酸,2011年進(jìn)入騰訊公司隆箩,從事過(guò)Web端及移動(dòng)端的測(cè)試工作该贾,喜愛(ài)新鮮事物及新技術(shù),目前在SNG開(kāi)放平臺(tái)測(cè)試組負(fù)責(zé)的移動(dòng)互聯(lián)SDK的測(cè)試工作捌臊。
現(xiàn)在越來(lái)越多的應(yīng)用開(kāi)始重視流暢度方面的測(cè)試杨蛋,了解Android應(yīng)用程序是如何在屏幕上顯示的則是基礎(chǔ)中的基礎(chǔ),就讓我們一起看看小小屏幕中大大的學(xué)問(wèn)理澎。這也是我下篇文章——《Android應(yīng)用流暢度測(cè)試分析》的基礎(chǔ)逞力。
首先,用一句話(huà)來(lái)概括一下Android應(yīng)用程序顯示的過(guò)程:Android應(yīng)用程序調(diào)用SurfaceFlinger服務(wù)把經(jīng)過(guò)測(cè)量糠爬、布局和繪制后的Surface渲染到顯示屏幕上寇荧。
名詞解釋
SurfaceFlinger:Android系統(tǒng)服務(wù),負(fù)責(zé)管理Android系統(tǒng)的幀緩沖區(qū)执隧,即顯示屏幕揩抡。
Surface:Android應(yīng)用的每個(gè)窗口對(duì)應(yīng)一個(gè)畫(huà)布(Canvas),即Surface镀琉,可以理解為Android應(yīng)用程序的一個(gè)窗口峦嗤。
Android應(yīng)用程序的顯示過(guò)程包含了兩個(gè)部分(應(yīng)用側(cè)繪制、系統(tǒng)側(cè)渲染)屋摔、兩個(gè)機(jī)制(進(jìn)程間通訊機(jī)制烁设、顯示刷新機(jī)制),接下來(lái)我們就來(lái)一一道來(lái)钓试。
應(yīng)用側(cè)
一個(gè)Android應(yīng)用程序窗口里面包含了很多UI元素署尤,這些UI元素是以樹(shù)形結(jié)構(gòu)來(lái)組織的,即它們存在著父子關(guān)系亚侠,其中曹体,子UI元素位于父UI元素里面,如下圖:
因此硝烂,在繪制一個(gè)Android應(yīng)用程序窗口的UI之前箕别,我們首先要確定它里面的各個(gè)子UI元素在父UI元素里面的大小以及位置。確定各個(gè)子UI元素在父UI元素里面的大小以及位置的過(guò)程又稱(chēng)為測(cè)量過(guò)程和布局過(guò)程。因此串稀,Android應(yīng)用程序窗口的UI渲染過(guò)程可以分為測(cè)量除抛、布局和繪制三個(gè)階段。如下圖所示:
測(cè)量:遞歸(深度優(yōu)先)確定所有視圖的大心附亍(高到忽、寬)
布局:遞歸(深度優(yōu)先)確定所有視圖的位置(左上角坐標(biāo))
繪制:在畫(huà)布canvas上繪制應(yīng)用程序窗口所有的視圖
測(cè)量、布局沒(méi)有太多要說(shuō)的清寇,這里要著重說(shuō)一下繪制喘漏。Android目前有兩種繪制模型:基于軟件的繪制模型和硬件加速的繪制模型(從Android 3.0開(kāi)始全面支持)。
在基于軟件的繪制模型下华烟,CPU主導(dǎo)繪圖翩迈,視圖按照兩個(gè)步驟繪制:
1.讓View層次結(jié)構(gòu)失效
2.繪制View層次結(jié)構(gòu)
當(dāng)應(yīng)用程序需要更新它的部分UI時(shí),都會(huì)調(diào)用內(nèi)容發(fā)生改變的View對(duì)象的invalidate()方法盔夜。無(wú)效(invalidation)消息請(qǐng)求會(huì)在View對(duì)象層次結(jié)構(gòu)中傳遞负饲,以便計(jì)算出需要重繪的屏幕區(qū)域(臟區(qū))。然后喂链,Android系統(tǒng)會(huì)在View層次結(jié)構(gòu)中繪制所有的跟臟區(qū)相交的區(qū)域返十。不幸的是,這種方法有兩個(gè)缺點(diǎn):
1.繪制了不需要重繪的視圖(與臟區(qū)域相交的區(qū)域)
2.掩蓋了一些應(yīng)用的bug(由于會(huì)重繪與臟區(qū)域相交的區(qū)域)
注意:在View對(duì)象的屬性發(fā)生變化時(shí)椭微,如背景色或TextView對(duì)象中的文本等洞坑,Android系統(tǒng)會(huì)自動(dòng)的調(diào)用該View對(duì)象的invalidate()方法。
在基于硬件加速的繪制模式下赏表,GPU主導(dǎo)繪圖检诗,繪制按照三個(gè)步驟繪制:
1.讓View層次結(jié)構(gòu)失效
2.記錄匈仗、更新顯示列表
3.繪制顯示列表
這種模式下瓢剿,Android系統(tǒng)依然會(huì)使用invalidate()方法和draw()方法來(lái)請(qǐng)求屏幕更新和展現(xiàn)View對(duì)象。但Android系統(tǒng)并不是立即執(zhí)行繪制命令悠轩,而是首先把這些View的繪制函數(shù)作為繪制指令記錄一個(gè)顯示列表中间狂,然后再讀取顯示列表中的繪制指令調(diào)用OpenGL相關(guān)函數(shù)完成實(shí)際繪制。另一個(gè)優(yōu)化是火架,Android系統(tǒng)只需要針對(duì)由invalidate()方法調(diào)用所標(biāo)記的View對(duì)象的臟區(qū)進(jìn)行記錄和更新顯示列表鉴象。沒(méi)有失效的View對(duì)象則能重放先前顯示列表記錄的繪制指令來(lái)進(jìn)行簡(jiǎn)單的重繪工作。
使用顯示列表的目的是何鸡,把視圖的各種繪制函數(shù)翻譯成繪制指令保存起來(lái)纺弊,對(duì)于沒(méi)有發(fā)生改變的視圖把原先保存的操作指令重新讀取出來(lái)重放一次就可以了,提高了視圖的顯示速度骡男。而對(duì)于需要重繪的View淆游,則更新顯示列表,以便下次重用,然后再調(diào)用OpenGL完成繪制犹菱。
硬件加速提高了Android系統(tǒng)顯示和刷新的速度拾稳,但它也不是萬(wàn)能的,它有三個(gè)缺陷:
1.兼容性(部分繪制函數(shù)不支持或不完全硬件加速腊脱,參見(jiàn)文章尾)
2.內(nèi)存消耗(OpenGL API調(diào)用就會(huì)占用8MB访得,而實(shí)際上會(huì)占用更多內(nèi)存)
3.電量消耗(GPU耗電)
系統(tǒng)側(cè)
Android應(yīng)用程序在圖形緩沖區(qū)中繪制好View層次結(jié)構(gòu)后,這個(gè)圖形緩沖區(qū)會(huì)被交給SurfaceFlinger服務(wù)陕凹,而SurfaceFlinger服務(wù)再使用OpenGL圖形庫(kù)API來(lái)將這個(gè)圖形緩沖區(qū)渲染到硬件幀緩沖區(qū)中悍抑。
由于Android應(yīng)用程序很少能涉及到Android系統(tǒng)底層,所以SurfaceFlinger服務(wù)的執(zhí)行過(guò)程不做過(guò)多的介紹捆姜。
進(jìn)程間通訊機(jī)制
Android應(yīng)用程序?yàn)榱四軌驅(qū)⒆约旱腢I繪制在系統(tǒng)的幀緩沖區(qū)上传趾,它們就必須要與SurfaceFlinger服務(wù)進(jìn)行通信,如圖所示:
Android應(yīng)用程序與SurfaceFlinger服務(wù)是運(yùn)行在不同的進(jìn)程中的泥技,因此浆兰,它們采用某種進(jìn)程間通信機(jī)制來(lái)進(jìn)行通信。由于Android應(yīng)用程序在通知SurfaceFlinger服務(wù)來(lái)繪制自己的UI的時(shí)候珊豹,需要將UI數(shù)據(jù)傳遞給SurfaceFlinger服務(wù)簸呈,例如,要繪制UI的區(qū)域店茶、位置等信息蜕便。一個(gè)Android應(yīng)用程序可能會(huì)有很多個(gè)窗口,而每一個(gè)窗口都有自己的UI數(shù)據(jù)贩幻,因此轿腺,Android系統(tǒng)的匿名共享內(nèi)存機(jī)制就派上用場(chǎng)了。
每一個(gè)Android應(yīng)用程序與SurfaceFlinger服務(wù)之間丛楚,都會(huì)通過(guò)一塊匿名共享內(nèi)存來(lái)傳遞UI數(shù)據(jù)族壳,如下所示:
但是單純的匿名共享內(nèi)存在傳遞多個(gè)窗口數(shù)據(jù)時(shí)缺乏有效的管理,所以匿名共享內(nèi)存就被抽象為一個(gè)更上流的數(shù)據(jù)結(jié)構(gòu)SharedClient趣些,如下圖所示:
在每個(gè)SharedClient中仿荆,最多有31個(gè)SharedBufferStack,每個(gè)SharedBufferStack都對(duì)應(yīng)一個(gè)Surface坏平,即一個(gè)窗口拢操。這樣,我們就可以知道為什么每一個(gè)SharedClient里面包含的是一系列SharedBufferStack而不是單個(gè)SharedBufferStack:一個(gè)SharedClient對(duì)應(yīng)一個(gè)Android應(yīng)用程序舶替,而一個(gè)Android應(yīng)用程序可能包含有多個(gè)窗口令境,即Surface。從這里也可以看出顾瞪,一個(gè)Android應(yīng)用程序至多可以包含31個(gè)窗口舔庶。
每個(gè)SharedBufferStack中又包含了N個(gè)緩沖區(qū)(<4.1 N=2; >=4.1 N=3),即顯示刷新機(jī)制中即將提到的雙緩沖和三重緩沖技術(shù)返劲。
顯示刷新機(jī)制
一般我們?cè)诶L制UI的時(shí)候,都會(huì)采用一種稱(chēng)為“雙緩沖”的技術(shù)栖茉。雙緩沖意味著要使用兩個(gè)緩沖區(qū)(SharedBufferStack中)篮绿,其中一個(gè)稱(chēng)為Front Buffer,另外一個(gè)稱(chēng)為Back Buffer吕漂。UI總是先在Back Buffer中繪制亲配,然后再和Front Buffer交換,渲染到顯示設(shè)備中惶凝。理想情況下吼虎,這樣一個(gè)刷新會(huì)在16ms內(nèi)完成(60FPS),下圖就是描述的這樣一個(gè)刷新過(guò)程(Display處理前Front Buffer苍鲜,CPU思灰、GPU處理Back Buffer。
但現(xiàn)實(shí)情況并非這么理想混滔。
1.時(shí)間從0開(kāi)始洒疚,進(jìn)入第一個(gè)16ms:Display顯示第0幀,CPU處理完第一幀后坯屿,GPU緊接其后處理繼續(xù)第一幀油湖。三者互不干擾,一切正常领跛。
2.時(shí)間進(jìn)入第二個(gè)16ms:因?yàn)樵缭谏弦粋€(gè)16ms時(shí)間內(nèi)乏德,第1幀已經(jīng)由CPU,GPU處理完畢吠昭。故Display可以直接顯示第1幀喊括。顯示沒(méi)有問(wèn)題。但在本16ms期間矢棚,CPU和GPU卻并未及時(shí)去繪制第2幀數(shù)據(jù)(注意前面的空白區(qū))郑什,而是在本周期快結(jié)束時(shí),CPU/GPU才去處理第2幀數(shù)據(jù)幻妓。
3.時(shí)間進(jìn)入第3個(gè)16ms蹦误,此時(shí)Display應(yīng)該顯示第2幀數(shù)據(jù)劫拢,但由于CPU和GPU還沒(méi)有處理完第2幀數(shù)據(jù)肉津,故Display只能繼續(xù)顯示第一幀的數(shù)據(jù),結(jié)果使得第1幀多畫(huà)了一次(對(duì)應(yīng)時(shí)間段上標(biāo)注了一個(gè)Jank)舱沧。
通過(guò)上述分析可知妹沙,此處發(fā)生Jank的關(guān)鍵問(wèn)題在于,為何第1個(gè)16ms段內(nèi)熟吏,CPU/GPU沒(méi)有及時(shí)處理第2幀數(shù)據(jù)距糖?原因很簡(jiǎn)單玄窝,CPU可能是在忙別的事情,不知道該到處理UI繪制的時(shí)間了悍引《髦可CPU一旦想起來(lái)要去處理第2幀數(shù)據(jù),時(shí)間又錯(cuò)過(guò)了趣斤!
為解決這個(gè)問(wèn)題俩块,Android 4.1中引入了VSYNC,這類(lèi)似于時(shí)鐘中斷浓领。結(jié)果如下圖所示:
由上圖可知玉凯,每收到VSYNC中斷,CPU就開(kāi)始處理各幀數(shù)據(jù)联贩。整個(gè)過(guò)程非常完美漫仆。
不過(guò),仔細(xì)琢磨后卻會(huì)發(fā)現(xiàn)一個(gè)新問(wèn)題:上圖中泪幌,CPU和GPU處理數(shù)據(jù)的速度似乎都能在16ms內(nèi)完成盲厌,而且還有時(shí)間空余,也就是說(shuō)祸泪,CPU/GPU的FPS(幀率狸眼,F(xiàn)rames Per Second)要高于Display的FPS。確實(shí)如此浴滴。由于CPU/GPU只在收到VSYNC時(shí)才開(kāi)始數(shù)據(jù)處理拓萌,故它們的FPS被拉低到與Display的FPS相同。但這種處理并沒(méi)有什么問(wèn)題升略,因?yàn)锳ndroid設(shè)備的Display FPS一般是60微王,其對(duì)應(yīng)的顯示效果非常平滑。
如果CPU/GPU的FPS小于Display的FPS品嚣,會(huì)是什么情況呢炕倘?請(qǐng)看下圖:
由上圖可知:
1.在第二個(gè)16ms時(shí)間段,Display本應(yīng)顯示B幀翰撑,但卻因?yàn)镚PU還在處理B幀罩旋,導(dǎo)致A幀被重復(fù)顯示。
2.同理眶诈,在第二個(gè)16ms時(shí)間段內(nèi)涨醋,CPU無(wú)所事事,因?yàn)锳 Buffer被Display在使用逝撬。B Buffer被GPU在使用浴骂。注意,一旦過(guò)了VSYNC時(shí)間點(diǎn)宪潮,CPU就不能被觸發(fā)以處理繪制工作了溯警。
為什么CPU不能在第二個(gè)16ms處開(kāi)始繪制工作呢趣苏?原因就是只有兩個(gè)Buffer(Android 4.1之前)。如果有第三個(gè)Buffer的存在梯轻,CPU就能直接使用它食磕,而不至于空閑。出于這一思路就引出了三重緩沖區(qū)(Android 4.1)喳挑。結(jié)果如下圖所示:
由上圖可知:
第二個(gè)16ms時(shí)間段芬为,CPU使用C Buffer繪圖。雖然還是會(huì)多顯示A幀一次蟀悦,但后續(xù)顯示就比較順暢了媚朦。
是不是Buffer越多越好呢?回答是否定的日戈。由上圖可知询张,在第二個(gè)時(shí)間段內(nèi),CPU繪制的第C幀數(shù)據(jù)要到第四個(gè)16ms才能顯示浙炼,這比雙Buffer情況多了16ms延遲份氧。所以,Buffer最好還是兩個(gè)弯屈,三個(gè)足矣蜗帜。
到這里,Android系統(tǒng)的顯示原理就介紹完了资厉。那么在了解這些原理后對(duì)我們的流暢度測(cè)試有哪些幫助呢厅缺,請(qǐng)看我的下篇文章《Android應(yīng)用流暢度測(cè)試分析》。
附:不同的API Level下宴偿,繪制函數(shù)對(duì)硬件加速模式的支持情況