[轉(zhuǎn)]Android UI性能優(yōu)化詳解

設(shè)計(jì)師,開(kāi)發(fā)人員,需求研究和測(cè)試都會(huì)影響到一個(gè)app最后的UI展示,所有人都很樂(lè)于去建議app應(yīng)該怎么去展示UI。UI也是app和用戶打交道的部分,直接對(duì)用戶形成品牌意識(shí),需要仔細(xì)的設(shè)計(jì)。無(wú)論你的app UI是簡(jiǎn)單還是復(fù)雜,重要的是性能一定要好。


UI性能測(cè)試

性能優(yōu)化都需要有一個(gè)目標(biāo),UI的性能優(yōu)化也是一樣。你可能會(huì)覺(jué)得“我的app加載很快”很重要,但我們還需要了解終端用戶的期望,是否可以去量化這些期望呢?我們可以從人機(jī)交互心理學(xué)的角度來(lái)考慮這個(gè)問(wèn)題。研究表明,0-100毫秒以?xún)?nèi)的延遲對(duì)人來(lái)說(shuō)是瞬時(shí)的,100-300毫秒則會(huì)感覺(jué)明顯卡頓,300-1000毫秒會(huì)讓用戶覺(jué)得“手機(jī)卡死了”,超過(guò)1000ms就會(huì)讓用戶想去干別等事情了。

這是人類(lèi)心理學(xué)最基礎(chǔ)的理論,我們可以從這個(gè)角度去優(yōu)化頁(yè)面/view/app的加載時(shí)間。 Ilya Grigorik 有一個(gè)很棒的演講,是關(guān)于搭建1000毫秒內(nèi)加載完成移動(dòng)網(wǎng)站的。如果你的網(wǎng)頁(yè)能在1秒內(nèi)加載好,就超過(guò)了人類(lèi)感知的預(yù)期,你的用戶一定會(huì)感覺(jué)很滿意。還有研究表明,如果網(wǎng)頁(yè)在3-4秒內(nèi)還沒(méi)加載出任何內(nèi)容,用戶就會(huì)放棄了。把這些數(shù)據(jù)應(yīng)用到app的加載,不難明白加載時(shí)間是越短越好。這篇文章主要關(guān)注UI的加載時(shí)間。當(dāng)然UI性能優(yōu)化還會(huì)涉及到其他方面,比如必需在后臺(tái)運(yùn)行到任務(wù),要從服務(wù)器下載一個(gè)文件等等,這些我們?cè)诤竺娴奈恼略倭摹?/p>


卡頓(Jank)

內(nèi)容的快速加載很重要,渲染的流暢性也很重要。android團(tuán)隊(duì)把滯緩,不流暢的動(dòng)畫(huà)定義為jank,一般是由于丟幀引起的。安卓設(shè)備的屏幕刷新率一般是60幀每秒(1/60fps=16.6ms每幀),所以你想要渲染的內(nèi)容能在16ms內(nèi)完成十分關(guān)鍵。每丟一幀,用戶就會(huì)感覺(jué)的動(dòng)畫(huà)在跳動(dòng),會(huì)出現(xiàn)違和感。為了保證動(dòng)畫(huà)的流暢性,我們接下來(lái)看下從哪些方面優(yōu)化可以讓內(nèi)容在16ms內(nèi)渲染完成,同時(shí)分析一些常見(jiàn)的導(dǎo)致UI卡頓的問(wèn)題。


android設(shè)備的UI渲染性能

早期android用戶抱怨最多的就是UI,尤其是觸碰反饋和動(dòng)畫(huà)流暢度,感覺(jué)都很卡。后來(lái)隨著android系統(tǒng)逐漸成熟,開(kāi)發(fā)人員也投入了大量的時(shí)間和精力讓交互變的流暢起來(lái)。下面列舉一些不同系統(tǒng)版本所帶來(lái)的提升:

在Gingerbread或者更早的設(shè)備上,屏幕完全是由軟件繪制(CPU繪制)的(不需要GPU的參與)。后來(lái)隨著屏幕尺寸變大和像素的提升,純粹靠軟件繪制遇到了瓶頸。

Honeycomb加入了平板設(shè)備,進(jìn)一步增加了屏幕尺寸。同時(shí)出于性能考慮,加入了GPU芯片,app在渲染內(nèi)容的時(shí)候多了一個(gè)GPU硬件加速的選項(xiàng)。

對(duì)于針對(duì)Ice Cream Sandwich或者更高系統(tǒng)的設(shè)備,GPU硬件加速是默認(rèn)打開(kāi)的。將軟件繪制(CPU)的壓力大部分轉(zhuǎn)移到了GPU上。

Jelly Bean 4.1 (and 4.2) “Project Butter” 做了近一步的提升來(lái)避免卡頓,通過(guò)引入VSYNC機(jī)制和增加額外的frame buffer(vsync和frame buffer的解釋可以參考這篇文章),運(yùn)行 Jelly Bean的設(shè)備丟幀的概率變的更小。引入這些機(jī)制的同時(shí),android開(kāi)發(fā)團(tuán)隊(duì)還加入了一些優(yōu)秀的工具來(lái)測(cè)量屏幕的繪制,開(kāi)發(fā)者可以使用這些工具來(lái)檢測(cè)VSYNC buffering和卡頓。

我們從普通開(kāi)發(fā)者的角度,來(lái)逐一看下這些提升和相關(guān)的測(cè)量工具。我們的目標(biāo)很明顯:

屏幕繪制低延遲

保證流程穩(wěn)定的幀率來(lái)避免卡頓

當(dāng)android開(kāi)發(fā)團(tuán)隊(duì)引入這些UI流暢性的提升時(shí),他們需要能量化這些提升的工具。經(jīng)由他們的努力,這些工具都打包進(jìn)了SDK以方便開(kāi)發(fā)者們來(lái)檢測(cè)UI相關(guān)的性能問(wèn)題。接下來(lái)我們就使用這些工具來(lái)優(yōu)化幾個(gè)demo程序。


搭建Views

大家應(yīng)該都對(duì)android studio里xml布局編輯器很熟悉了,知道怎么在android studio(Eclipse)中搭建和檢測(cè)View結(jié)構(gòu)。下圖是一個(gè)簡(jiǎn)單的app view,包含一些套嵌的子view。搭建這些view的時(shí)候,一定要留意屏幕右上角的組件樹(shù)(Component Tree)。套嵌的子view越深,組件樹(shù)就越復(fù)雜,渲染起來(lái)也就越費(fèi)時(shí)間。



圖4-1

對(duì)于app里的每一個(gè)view,android系統(tǒng)都會(huì)經(jīng)過(guò)三部曲來(lái)渲染:measure,layout,draw。可以在腦中回想下你搭建的view的xml布局文件結(jié)構(gòu),measure從最頂部的節(jié)點(diǎn)開(kāi)始,順著layout樹(shù)形結(jié)構(gòu)依次往下:測(cè)量每個(gè)view需要在屏幕當(dāng)中展示的尺寸大小(上圖當(dāng)中:LinearLayout,RelativeLayout,LinearLayout;然后是textView0和LinearLayout Row1點(diǎn)分支,該分支又有另外3個(gè)子節(jié)點(diǎn))。每個(gè)子節(jié)點(diǎn)都需要向自己的父節(jié)點(diǎn)提供自己的尺寸來(lái)決定展示的位置,遇到?jīng)_突的時(shí)候,父節(jié)點(diǎn)可以強(qiáng)制子節(jié)點(diǎn)重新measure(由此可能導(dǎo)致measure的時(shí)間消耗為原來(lái)的2-3倍)。這就是為什么扁平的view結(jié)構(gòu)會(huì)性能更好。節(jié)點(diǎn)所處位置越深,套嵌帶來(lái)的measure越多,計(jì)算就會(huì)越費(fèi)時(shí)。我們來(lái)看一些具體的例子,看measure是怎么影響渲染性能的。


Remeasureing Views(重新測(cè)量views)

并不是只有發(fā)生錯(cuò)誤的時(shí)候才會(huì)觸發(fā)remeasure。RelativeLayout經(jīng)常需要measure所有子節(jié)點(diǎn)兩次才能把子節(jié)點(diǎn)合理的布局。如果子節(jié)點(diǎn)設(shè)置了weight屬性,LinearLayout也需要measure這些節(jié)點(diǎn)兩次,才能獲得精確的展示尺寸。如果LinearLayout或者RelativeLayout被套嵌使用,measure所費(fèi)時(shí)間可能會(huì)呈指數(shù)級(jí)增長(zhǎng)(兩個(gè)套嵌的views會(huì)有四次measure,三個(gè)套嵌的views會(huì)有8次的measure)。可以看下面圖4-9里面一個(gè)夸張點(diǎn)的例子。

一旦view開(kāi)始被measure,該view所有的子view都會(huì)被重新layout,再把該view傳遞給它的父view,如此重復(fù)一直到最頂部的根view。layout完成之后,所有的view都被渲染到屏幕上。需要特別注意到是,并不是只有用戶看得見(jiàn)的view才會(huì)被渲染,所有的view都會(huì)。后面我們會(huì)看下“屏幕重復(fù)繪制”的問(wèn)題。app擁有的views越多,measure,layout,draw所花費(fèi)的時(shí)間就越久。要縮短這個(gè)時(shí)間,關(guān)鍵是保持view的樹(shù)形結(jié)構(gòu)盡量扁平,而且要移除所有不需要渲染的view。移除這些view會(huì)對(duì)加速屏幕渲染產(chǎn)生明顯的效果。理想情況下,總共的measure,layout,draw時(shí)間應(yīng)該被很好的控制在16ms以?xún)?nèi),以保證滑動(dòng)屏幕時(shí)UI的流暢。

雖然可以通過(guò)xml文件查看所有的view,但不一定能輕易的查出哪些view是多余的。要找到那些多余的view(增加渲染延遲的view),可以用Android studio monitor里的Hierarchy Viewer工具,可視化的查看所有的view。(monitor是個(gè)獨(dú)立的app,下載android studio的時(shí)候會(huì)同時(shí)下載)


Hierarchy Viewer

Hierarchy Viewer可以很方便可視化的查看屏幕上套嵌view結(jié)構(gòu),是查看你的view結(jié)構(gòu)的實(shí)用工具。這個(gè)工具包含在android studio monitor當(dāng)中,需要運(yùn)行在帶有開(kāi)發(fā)者版本的android系統(tǒng)的設(shè)備上。后續(xù)所有的view和屏幕截圖都來(lái)自一款三星的Note II設(shè)備,系統(tǒng)版本是Jelly Bean。在老的設(shè)備(處理器慢)上測(cè)試渲染性能,更容易發(fā)現(xiàn)問(wèn)題。

如圖4-2所示,打開(kāi)Hierarchy Viewer之后,會(huì)看到幾個(gè)窗口:左邊的窗口列出了連上你電腦的android設(shè)備和設(shè)備上所有運(yùn)行的進(jìn)程。活躍的進(jìn)程是粗體展示的。第二個(gè)tab某一個(gè)編譯版本的詳情(后面細(xì)說(shuō))。中間的部分是可縮放的view的樹(shù)形圖。點(diǎn)擊某一個(gè)view能看到在設(shè)備上展示的樣子和一些額外的數(shù)據(jù)。右邊有兩個(gè)view:樹(shù)形結(jié)構(gòu)總覽和布局view。樹(shù)形結(jié)構(gòu)總覽顯示了整個(gè)view的樹(shù)形結(jié)構(gòu),里面有一個(gè)方塊顯示了中間窗口在整個(gè)樹(shù)形結(jié)構(gòu)當(dāng)中所處的位置。布局view當(dāng)中深紅色高亮的區(qū)域表示所選中的view被繪制的部分(淺紅色展示的是父view)。

圖4-2

在中間的這個(gè)窗口,你可以點(diǎn)擊任何一個(gè)view來(lái)查看該view在android設(shè)備屏幕上的展示。點(diǎn)擊樹(shù)形圖工具欄里紅綠紫三色的維恩圖圖標(biāo),還能展示子view的數(shù)量,和measure,layout,draw三部曲所花費(fèi)的時(shí)間。這個(gè)時(shí)間是被選擇的view及其所有子節(jié)點(diǎn)所花費(fèi)時(shí)間的總和。(圖4-3中,我選擇了最頂部的view來(lái)獲取整個(gè)view結(jié)構(gòu)的時(shí)間)

圖4-3

最頂部的view總共包含181個(gè)view,measure的總時(shí)間為3.6ms,layout是7ms,draw花了14.5ms(總共大約25ms)。要縮短渲染這些view的總時(shí)間,我們先看下app的樹(shù)形結(jié)構(gòu)圖預(yù)覽,看看所有的view是怎么拼湊到一起的。從樹(shù)形結(jié)構(gòu)圖上可以看出屏幕里有非常多的view,樹(shù)的結(jié)構(gòu)比較扁平。前面說(shuō)過(guò),扁平的結(jié)構(gòu)性能好,樹(shù)的深度對(duì)渲染的性能會(huì)產(chǎn)生很大的影響。我們的結(jié)構(gòu)雖然是扁平的,卻依然花費(fèi)了26ms的時(shí)間來(lái)渲染,說(shuō)明扁平的結(jié)構(gòu)也有可能會(huì)卡頓,也需要去考慮怎么優(yōu)化。

圖4-4

排查一個(gè)新聞?lì)恆pp的樹(shù)形結(jié)構(gòu),大致可以看三個(gè)區(qū)域:頭部(底部藍(lán)色的方框),文章列表(兩個(gè)橙色的方框表示兩個(gè)不同的tab),單篇文章的view是用紅色方框來(lái)標(biāo)注的。內(nèi)部標(biāo)題view的結(jié)構(gòu)重復(fù)出現(xiàn)了九次,5個(gè)在上面橙色的方框內(nèi),4個(gè)在下面的方框內(nèi)。最后,我們可以看到從邊上拉出來(lái)的導(dǎo)航欄是用底部綠色的方框標(biāo)出來(lái)的。頭部用了22個(gè)view,兩個(gè)文章列表個(gè)用了67和44個(gè)view(每個(gè)標(biāo)題部分使用了13個(gè)view),導(dǎo)航抽屜使用了20個(gè)。這樣我們還剩下18個(gè)view沒(méi)有計(jì)算在內(nèi)。剩下的這些view其實(shí)是在滑動(dòng)手勢(shì)動(dòng)畫(huà)過(guò)程當(dāng)中生成的。很顯然,view的數(shù)量很多,要做到不卡頓要讓view的繪制非常高效才行。

圖4-5

仔細(xì)看下標(biāo)題部分,一個(gè)標(biāo)題是由13個(gè)view組成的。每個(gè)標(biāo)題的結(jié)構(gòu)有5層之深,一共花費(fèi)0.456ms來(lái)measure,0.077ms來(lái)layout,2.737ms來(lái)draw。第五層是通過(guò)第四層的兩個(gè)RelativeLayouts來(lái)連接的(藍(lán)色高亮),這些又是通過(guò)第三層的另一個(gè)RelativeLayout來(lái)連接的(綠色高亮)。如果我們把第四第五層的view都移到第三層來(lái),我們可以少渲染一整層。而且我之前解釋過(guò),RelativeLayout里的measure都會(huì)發(fā)生兩次,套嵌的view會(huì)導(dǎo)致measure時(shí)間的增加。

現(xiàn)在,你可能已經(jīng)注意到了每個(gè)view里紅色,黃色和綠色的圓圈。它們表示該view在那一層樹(shù)形結(jié)構(gòu)里measure,layout和draw所花費(fèi)的相對(duì)時(shí)間(從左到右)。綠色表示最快的前50%,黃色表示最慢的前50%,紅色表示那一層里面最慢的view。顯然,紅色的部分是我們優(yōu)先優(yōu)化的對(duì)象。

再看下文章標(biāo)題的樹(shù)形結(jié)構(gòu),繪制最慢的view是右上角的ImageView。順著ImageView一直找到文章父view,父view是通過(guò)兩個(gè)RelativeLayouts來(lái)連接的(這里增加了measure的時(shí)間),然后是3個(gè)沒(méi)有子節(jié)點(diǎn)的view(在最底部)。這3個(gè)view可以?xún)?yōu)化合并成一個(gè)view,這樣能減少兩個(gè)layer的渲染。

我們?cè)倏戳硪粋€(gè)新聞?lì)恆pp是怎么來(lái)減少標(biāo)題view里面的子view數(shù)量的。從圖4-6里能看到一個(gè)和圖4-5類(lèi)似的樹(shù)形結(jié)構(gòu)圖。

圖4-6

圖4-6里的標(biāo)題view也有RelativeLayouts(綠色的部分)的問(wèn)題,一共消耗了1.275ms的measure時(shí)間,layout用了0.066ms,draw 3.24ms(總共是4.6ms)。在這些數(shù)據(jù)基礎(chǔ)上,我們?cè)僮鲆恍┱{(diào)整,加入一個(gè)更大的圖片展示和分享按鈕,但是整個(gè)樹(shù)形結(jié)構(gòu)變得扁平一點(diǎn)(如圖4-7所示)。

圖4-7

再看下標(biāo)題view的渲染時(shí)間(三層的結(jié)構(gòu)),只用了4.2ms!雖然展示了更大的內(nèi)容,但節(jié)省了400ms!

為了更好的了解這部分的優(yōu)化,我們?cè)倏戳硪粋€(gè)例子app。這個(gè)例子會(huì)展示一個(gè)山羊圖片等列表。界面使用了幾種不同的layout方式,性能差的和性能好的都有。仔細(xì)的查看這些布局,然后一步步優(yōu)化它們,我們就能清楚的理解怎么去優(yōu)化一個(gè)app的渲染性能了。我們分幾步來(lái)進(jìn)行優(yōu)化,每一步改變都可以通過(guò)Hierarchy View可視化的查看。每換一種layout方式,xml渲染的性能要么變好,要么變差。我們先從性能差的布局方式開(kāi)始。先快速的掃一眼圖4-8里的Hierarchy View。

圖4-8

這個(gè)簡(jiǎn)單的app里有59個(gè)view。但是和圖4-4里的app不同,這個(gè)app的樹(shù)形結(jié)構(gòu)更扁平,水平方向的view更多一些。疊加的view越多,渲染就會(huì)越費(fèi)時(shí),減少view樹(shù)形結(jié)構(gòu)的深度,app每一幀的渲染就會(huì)變快。

藍(lán)色方框里面的view是action bar。橘色方框里的是屏幕頂部的text box,紫色方框里展示的是山羊的詳細(xì)信息(有6個(gè)這種view)。紅色方框標(biāo)示了7個(gè)view,每個(gè)都增加了樹(shù)形結(jié)構(gòu)的深度。我們仔細(xì)看些這7個(gè)view其中三個(gè)的remeasure數(shù)據(jù)(圖4-9)。

圖4-9

當(dāng)設(shè)備開(kāi)始measure views的時(shí)候,先從右邊的子views開(kāi)始,然后到左邊的父views。右邊ListView包含6行數(shù)據(jù),一共37個(gè)view,花了0.012ms來(lái)measure。把這個(gè)ListView加到中間的LinearLayout之后,變成38個(gè)views。有意思的是,measure的時(shí)間由于remeasure被觸發(fā),瞬間跳到了18.109ms,是原來(lái)的三個(gè)數(shù)量級(jí)。LinearLayout左邊的RelativeLayout使得measure的時(shí)間再次翻倍到33.739ms。再依次往左繼續(xù)觀察(圖4-8里紅色方框部分),measure的時(shí)間疊加到了68ms。但是只要移除上面的一個(gè)LinearLayout,measure的時(shí)間瞬間降到了1ms。我們可以移除更多的層讓樹(shù)形結(jié)構(gòu)更扁平一些,這樣我們可以得到圖4-10里的結(jié)果,層數(shù)減少到了3層。


圖4-10

我們可以繼續(xù)看下山羊信息到row展示部分,來(lái)繼續(xù)減少view結(jié)構(gòu)的深度。每一行山羊信息有6個(gè)view,一個(gè)有6行數(shù)據(jù)在屏幕中展示(圖4-8中有一行數(shù)據(jù)是用紫色方框高亮的)。我們用Hierarchy View看下一行view的結(jié)構(gòu)是怎么樣的(圖4-11),先看下左邊兩個(gè)view(一個(gè)LinearLayout,一個(gè)RelativeLayout),這兩個(gè)view唯一的作用就是加深了樹(shù)機(jī)構(gòu)的深度。LinearLayout連接了RelativeLayout,但并沒(méi)有展示其他什么內(nèi)容。


圖4-11

因?yàn)镽elativeLayout會(huì)measure兩次(我們現(xiàn)在關(guān)注優(yōu)化measure的時(shí)間),我們先移除RelativeLayout(圖4-12)。這樣樹(shù)形結(jié)構(gòu)的深度從4減到了3,渲染立馬快了一些。


圖4-12

但效果還并不理想。我們繼續(xù)移除LinearLayout,同時(shí)調(diào)整下RelativeLayout來(lái)展示整個(gè)row的信息(圖4-13),這樣深度近一步減少到了2。渲染又快了0.1ms。這樣看來(lái)優(yōu)化的途徑有很多種,多嘗試總是有好處的(看下表格4-1里的結(jié)果)。


圖4-13


圖4-14

每一行減少大約1ms的時(shí)間,我們一共可以節(jié)省6ms的渲染時(shí)間。如果你的app有卡頓,或者你通過(guò)工具檢測(cè)到每次渲染接近16ms了,減少6ms的時(shí)間當(dāng)然會(huì)讓你的app更快一點(diǎn)。



View的重用

如果一個(gè)程序員面向?qū)ο缶幊探?jīng)驗(yàn)豐富,他就會(huì)盡可能重用創(chuàng)建的view(而不是每次都創(chuàng)建)。拿上面山羊app作為例子,其實(shí)每一行展示的layout都是重用的。如果xml文件里最外層的view只是用來(lái)承載子view的,那這個(gè)view只不過(guò)是增加了view結(jié)構(gòu)的深度,這種情況下,我們可以移除這個(gè)view,用一個(gè)merge標(biāo)簽來(lái)代替。這種方式可以移除樹(shù)形結(jié)構(gòu)里多余的層。

大家可以從github上下載這個(gè)山羊app練習(xí)下,改變里面xml文件的布局方式,再用Hierarchy View工具看下渲染時(shí)間的變化。

Hierarchy Viewer(不止是樹(shù)形結(jié)構(gòu)圖)

Hierarchy Viewer還有一個(gè)功能,可以幫助開(kāi)發(fā)者發(fā)現(xiàn)overdraw(重復(fù)的繪制)。從左到右看下樹(shù)形結(jié)構(gòu)窗口的選項(xiàng),可以發(fā)現(xiàn)這些功能:

把view的樹(shù)形結(jié)構(gòu)圖保存為png圖片。

導(dǎo)出為photoshop的格式。

重新加載一個(gè)view(第二個(gè)紫色樹(shù)形按鈕)。

在另一個(gè)窗口里打開(kāi)較大的view結(jié)構(gòu)圖,還可以設(shè)置背景色來(lái)發(fā)現(xiàn)重復(fù)繪制。

讓一個(gè)view的繪制失效(有條紅線的按鈕)。

讓view重新layout。

讓view生出draw命令到logcat(紫色樹(shù)形按鈕到第三個(gè)用處)。這樣可以查看繪制到底觸發(fā)了哪些opengl行為。這個(gè)功能對(duì)opengl的專(zhuān)家做深度優(yōu)化比較有用。

Hierarchy Viewer對(duì)于優(yōu)化app view的樹(shù)形結(jié)構(gòu)重要性不言而喻了,很可能會(huì)幫你節(jié)省幾十毫秒的繪制時(shí)間。


資源縮減

在我們把a(bǔ)pp的view結(jié)構(gòu)變扁平,view的總數(shù)量減少之后,我們還可以嘗試減少每個(gè)view里面使用的資源數(shù)量。2014年的時(shí)候,Instagram把標(biāo)題欄里的資源數(shù)量從29減少到了8個(gè)。他們測(cè)量后發(fā)現(xiàn)app的啟動(dòng)時(shí)間增加了10%-20%(因設(shè)設(shè)備而異)。主要是通過(guò)資源上色的方式來(lái)進(jìn)行縮減。比如只加載一個(gè)資源,然后在運(yùn)行的時(shí)候通過(guò)ColorFilter進(jìn)行上色。我們看下下面的例子是怎么個(gè)一個(gè)drawable上色的。

圖4-15

這樣一個(gè)資源文件就可以表示幾種不同的狀態(tài)了(加星或者不加星,在線或者離線等等)。


屏幕的重復(fù)繪制

每過(guò)幾年,就會(huì)有傳聞?wù)f某個(gè)博物館在用x光掃描一副無(wú)價(jià)的名畫(huà)之后,發(fā)現(xiàn)畫(huà)作的作者其實(shí)重用了老的畫(huà)布,在名畫(huà)的底下還藏著另一副沒(méi)有被發(fā)現(xiàn)的畫(huà)作。有時(shí)候,博物館還能用高級(jí)的圖像技術(shù)來(lái)還原畫(huà)布上的原作。android里面的view的繪制就是類(lèi)似的情況。當(dāng)android系統(tǒng)繪制屏幕的時(shí)候,先畫(huà)父view,然后子view,再是更深的子view等等。這會(huì)導(dǎo)致所有的view都被繪制到了屏幕上,就像畫(huà)家的畫(huà)布一樣,這些view都被他們的子view覆蓋住了。

文藝復(fù)興時(shí)期,有很多偉大的畫(huà)家要等畫(huà)干了以后才能重用畫(huà)布。但在我們的高科技觸摸屏上,屏幕重畫(huà)的速度要快幾個(gè)數(shù)量級(jí),但是多次的重新繪制屏幕會(huì)使得繪制延遲變大,最終導(dǎo)致布局的卡頓。重新繪制屏幕的行為叫做overdraw,下面我們會(huì)看下怎么檢測(cè)overdraw。

overdraw還帶來(lái)的另一個(gè)問(wèn)題,當(dāng)view內(nèi)容有更新的時(shí)候,之前繪制的view就失效了,view的每一個(gè)像素都需要重繪。android設(shè)備沒(méi)法判斷哪個(gè)view是可見(jiàn)的,所以只能繪制每個(gè)view的相關(guān)像素。類(lèi)比上面畫(huà)家的例子,畫(huà)家只能把老畫(huà)一幅幅還原出來(lái),再一層層畫(huà)到畫(huà)布上,最后再畫(huà)上最新的畫(huà)。你的app如果有很多層,每一層的相關(guān)像素都需要繪制一遍。如果不小心,這些繪制就會(huì)帶來(lái)性能問(wèn)題。


檢測(cè)overdraw

android提供了一些很好的工具來(lái)檢測(cè)overdraw。Jelly Bean 4.2里,開(kāi)發(fā)者選項(xiàng)菜單里增加了Debug GPU Overdraw的選項(xiàng)。如果你用的是Jelly Bean 4.3 或者 KitKat 設(shè)備,在屏幕的左下角會(huì)有一個(gè)計(jì)數(shù)展示屏幕overdraw的程度。我親身試過(guò)這個(gè)工具對(duì)檢測(cè)overdraw十分有效。雖然有時(shí)候這個(gè)會(huì)多提示6-7次overdraw(發(fā)生的概率還不小)。

圖4-16中的截圖還是來(lái)自上面的山羊app。左下方可以看到overdraw的計(jì)數(shù)。屏幕中可以看到3個(gè)overdraw的計(jì)數(shù),其中開(kāi)發(fā)者能控制的是主窗口的計(jì)數(shù)。overdraw的計(jì)數(shù)是在左下方。沒(méi)優(yōu)化過(guò)的app overdraw的次數(shù)是8.43,我們優(yōu)化過(guò)后可以降到1.38。導(dǎo)航欄overdraw的次數(shù)是1.2(菜單按鈕是2.4),也就是說(shuō)文字和圖標(biāo)的overdraw貢獻(xiàn)了額外的20%。overdraw計(jì)數(shù)可以在不影響用戶體驗(yàn)的前提下,快速便捷的比較不同app的overdraw,但沒(méi)辦法定位overdraw是哪里產(chǎn)生的。

圖4-16


另一種查看overdraw的方式是在Debug GPU overdraw菜單里選擇“Show Overdraw areas”選項(xiàng)。選擇之后,會(huì)在app的不同區(qū)域覆蓋不同的顏色來(lái)表示overdraw的次數(shù)。比較屏幕上這些不同的顏色,可以快速方便的定位overdraw問(wèn)題:

白色:沒(méi)有overdraw 藍(lán)色:1x overdraw(屏幕繪制了2次) 綠色:2x overdraw 淺紅色:3x overdraw 深紅色:4x或者更多overdraw

在圖4-17中,可以看到山羊app優(yōu)化前后overdraw區(qū)域的變化。app的菜單欄優(yōu)化前后都沒(méi)有顏色(沒(méi)有overdraw),但android圖標(biāo)和菜單按鈕圖標(biāo)都是綠色的(2x overdraw)。山羊圖片等列表在優(yōu)化之前是深紅色的(4x以上的overdraw)。優(yōu)化app 之后,只有checkbox和圖片區(qū)域是藍(lán)色(1x)的了,說(shuō)明至少3層overdraw被消滅掉了!text和空白區(qū)域都沒(méi)有overdraw了。

圖4-17


通過(guò)減少view的數(shù)量(或者去移除重復(fù)繪制的view),app的渲染會(huì)更快。通過(guò)比較父view在優(yōu)化前后的繪制時(shí)間,可以發(fā)現(xiàn)優(yōu)化后帶來(lái)50%性能的提升,由13.5ms降到6.8ms。

Hierarchy Viewer當(dāng)中的overdraw

另一種查看app當(dāng)中overdraw的方式是把Hierarchy Viewer中的view的樹(shù)形結(jié)構(gòu)保存成photoshop識(shí)別的文檔(樹(shù)形view里的第二個(gè)選項(xiàng))。如果你沒(méi)有安裝photoshop,有幾個(gè)其他的免費(fèi)軟件也可以打開(kāi)這個(gè)文檔。打開(kāi)文檔查看view,可以清楚看到不同layer里的overdraw。對(duì)于大部分的線上app,在一個(gè)白色背景上放上另一個(gè)白色背景很常見(jiàn)。聽(tīng)起來(lái)還好,但這里其實(shí)有一次繪制是多余的,完全可以避免的。我們?cè)倏聪律窖騛pp,所有overdraw圖片區(qū)域都放在了一張?bào)H子的背景圖片上(替換了之前的白色背景)。之前的驢子看不到,是因?yàn)楸话咨尘皥D擋住了。移除掉之后就可以看到下面的驢子了,這樣我們就可以快速的定位哪里出現(xiàn)了overdraw。用GIMP打開(kāi)文檔之后,app里所有可見(jiàn)的view的左邊都有一個(gè)小眼睛圖標(biāo)。在圖4-18中,可以看到我從最上面開(kāi)始把view一個(gè)個(gè)隱藏起來(lái)了。在右邊的layout視圖中,可以看到一些其他的全屏layout(都顯示了驢子的圖片)。

圖4-18

在圖4-19中可以看到另一個(gè)逐步隱藏view的辦法。從最左邊的全屏圖片開(kāi)始,到中間的圖片,可以看到我們隱藏了兩行山羊的圖片展示,每一行下面的出現(xiàn)了一張拉伸的驢子的圖片。在這些驢子圖片的下面是一張白色的背景圖(從最右邊的圖片可以看出)。再移除這張白色背景可以看到一張大的驢子的圖片,在左下角。再往下是另一張白色的全屏背景圖。

圖4-19


KitKat里的overdraw

在KitKat或者更新的設(shè)備里,overdraw被大幅度的削減了。這項(xiàng)技術(shù)叫overdraw avoidance,系統(tǒng)可以檢測(cè)發(fā)現(xiàn)簡(jiǎn)單的overdraw場(chǎng)景(比如一個(gè)view完全蓋住了另一個(gè)view),然后自動(dòng)移除額外的繪制,應(yīng)用到上面的例子,也就是說(shuō)驢子那張大背景圖就不會(huì)去繪制了。這很明顯會(huì)極大的提高設(shè)備的繪制性能。但開(kāi)發(fā)者還是要盡可能的避免額外的overdraw(為了更好的性能,也為了能兼容Jelly Bean及更老的設(shè)備)。

Overdraw Avoidance和相關(guān)開(kāi)發(fā)者工具

當(dāng)用上面提到的overdraw檢測(cè)工具時(shí),KitKat的overdraw avoidance功能會(huì)被禁止,這只是為了方便你查看view的布局,和在設(shè)備上真正運(yùn)行的情況并不一樣。

分析卡頓(測(cè)量GPU的渲染性能)

在我們優(yōu)化過(guò)view的樹(shù)形結(jié)構(gòu)和overdraw之后,你可能還是感覺(jué)自己的app有卡頓和丟幀,或者滑動(dòng)慢:卡頓還是存在。可能高端機(jī)器上感覺(jué)不到卡頓,但低端機(jī)上還是可能會(huì)出現(xiàn)卡頓。為了能獲取更全面的卡頓檢測(cè)信息,android在Jelly Bean及更新的系統(tǒng)里加入了一個(gè)GPU繪制開(kāi)發(fā)者選項(xiàng)。能夠測(cè)出每一幀的繪制用了多少時(shí)間。你可以把測(cè)量出來(lái)的數(shù)據(jù)保存到一個(gè)logfile(adb shell dumpsys gfxinfo),或者在設(shè)備的屏幕上實(shí)時(shí)查看這些信息(只支持android 4.2+)。

我們快速來(lái)看下怎么分析,我比較喜歡在屏幕上直接展示GPU的渲染數(shù)據(jù),這樣感覺(jué)更直觀全面(logfile里面的數(shù)據(jù)很適合離線的詳細(xì)分析)。我們最好在不同的設(shè)備上都試一下。圖4-20展示的是Nexus 6運(yùn)行Lollipop(左邊)和Moto G運(yùn)行 KitKat(右邊)同時(shí)跑山羊app的GPU渲染數(shù)據(jù)。重點(diǎn)看下GPU測(cè)量圖表底部的水平綠條。它是設(shè)備16ms繪制一幀的分割線,如果你有很多幀都超過(guò)了這條綠線,那就表示有卡頓了。在下圖里可以看到Nexus6上有偶爾的卡頓。出現(xiàn)在滑動(dòng)到頁(yè)面底部的時(shí)候,播放里一個(gè)反彈的動(dòng)畫(huà)。用戶體驗(yàn)不算太糟。每一次屏幕繪制(豎線)被分成四種顏色來(lái)表示額外的測(cè)量數(shù)據(jù):draw(藍(lán)色),prepare(紫色),process(紅色),執(zhí)行(黃色)。在KitKat和更早的版本里,prepare的數(shù)據(jù)沒(méi)有獨(dú)立出來(lái),包含在其他項(xiàng)里面(因此只有看到3種顏色)。

圖4-20


對(duì)比下Nexus 6和Moto G的GPU數(shù)據(jù)可以看出真機(jī)測(cè)試的重要性。圖4-20中,沒(méi)有優(yōu)化過(guò)的山羊app精確的表示Moto G繪制的時(shí)間是Nexus 6的兩倍(比較兩圖中綠線的高度)。這一點(diǎn)可以通過(guò)數(shù)據(jù)采集(adb shell dumpsys gfxinfo)進(jìn)一步說(shuō)明。下一個(gè)例子當(dāng)中,優(yōu)化過(guò)的view繪制在Moto G上用了兩倍多時(shí)間。對(duì)于兩臺(tái)設(shè)備來(lái)說(shuō),draw,prepare,process這幾步都花了差不多的時(shí)間(少于4ms)。差別出現(xiàn)在execute階段(紫色),Moto G比Nexus 6多用了差不多4ms。說(shuō)明GPU渲染測(cè)試最好是在低端機(jī)器上來(lái)做,比較容易發(fā)現(xiàn)卡頓問(wèn)題。

圖4-21


一般來(lái)說(shuō),GPU Profiler可以幫你發(fā)現(xiàn)問(wèn)題。在山羊app里,如果我打開(kāi)Fibonacci延遲(在創(chuàng)建view多時(shí)候進(jìn)行耗時(shí)的遞歸計(jì)算),GPU profiler看不出任何卡頓,因?yàn)橛?jì)算都發(fā)生在主線程而且完全阻止了渲染(在低端機(jī)上,可能會(huì)出現(xiàn)ANR消息)。

Fibonacci算法

Fibonacci序列是這樣一組數(shù)的集合:每個(gè)數(shù)字都是它前面兩個(gè)數(shù)字的和。比如0,1,1,2,3,5,8等等。程序里一般用來(lái)表示遞歸,這里我用了最低效的方式來(lái)生成Fibonacci序列。

圖4-22

生成這些數(shù)字的計(jì)算次數(shù)呈指數(shù)級(jí)增長(zhǎng)。這樣做的目的是在渲染的時(shí)候增加CPU的壓力,這樣渲染事件就無(wú)法得到及時(shí)處理,出現(xiàn)延遲。計(jì)算n=40就把a(bǔ)pp變得很慢了(低端機(jī)上會(huì)crash)。這個(gè)例子雖然有點(diǎn)牽強(qiáng),但我們定位卡頓是由Fibonacci產(chǎn)生的過(guò)程會(huì)很有意義。


Android Marshmallow里的GPU渲染

在android marshmallow里,運(yùn)行adb shell dumpsys gfxinfo?. 可以發(fā)現(xiàn)一些檢測(cè)卡頓的新功能。首先,數(shù)據(jù)報(bào)告開(kāi)頭部分能看到每一幀渲染的信息了。

圖4-23

從app的啟動(dòng)開(kāi)始,我們可以看到一共渲染了多少幀,其中多少幀的渲染時(shí)間是控制在理想值的90%以?xún)?nèi),還能看到渲染比較慢的幀(90%,95%,99%)。最后五行列出的是沒(méi)有在16ms內(nèi)渲染完成的原因。注意,這里不止有卡頓的問(wèn)題,幀率還收到了其他因素的影響。

android marshmallow在gfxinfo庫(kù)里增加了另一個(gè)好用的測(cè)試工具,adb shell dumpsys gfxinfo?framestats。它能夠輸出每一幀里發(fā)生的某些事件耗時(shí),格式是逗號(hào)分隔的一張大表。列名沒(méi)有給出,但在Android Developer網(wǎng)站里有解釋。為了算出渲染里每一步的費(fèi)時(shí),我們要計(jì)算出報(bào)告里不同framestats的差異。下面是一些繪制事件:

VSYNC-Intended_VSYC(告訴你是否丟幀里,也就是卡頓)

處理輸入事件的時(shí)間(一般要小于2ms)

動(dòng)畫(huà)計(jì)算(一般小于2ms)

layout和measure

view.draw()耗時(shí)

Sync耗時(shí)(如果大于0.4ms,表示很多bitmap正在發(fā)送到GPU)

GPU耗時(shí)(overdraw的時(shí)間會(huì)在這里面)

繪制一幀的總時(shí)間

有時(shí)候即使出現(xiàn)了超過(guò)16ms的繪制,但由于有vsync buffer的存在,也不會(huì)出現(xiàn)丟幀。對(duì)于沒(méi)有額外buffer的低端設(shè)備,就可能會(huì)出現(xiàn)卡頓了。


不只是卡頓(丟幀)

有時(shí)候GPU Profile里看不到超過(guò)16ms的數(shù)據(jù),但你從屏幕上看到明顯的卡頓或跳動(dòng)。出現(xiàn)這種情況可能是由于CPU在做別的事情被堵住了,從而導(dǎo)致里丟幀。在Monitor或者Android Studio中,可以查看DDMS里的logfiles。通過(guò)過(guò)濾log更容易查看app的運(yùn)行情況。可以重點(diǎn)看下類(lèi)似下圖中的log。

圖4-24

我們?cè)诤竺娴奈恼吕飼?huì)講訴CPU導(dǎo)致的丟幀是怎么產(chǎn)生的。

Systrace

在上面的這些優(yōu)化之后,如果你的界面還有卡頓,我們還有辦法。Systrace工具也可以測(cè)量你app的性能。甚至可以幫助你定位問(wèn)題產(chǎn)生的位置。這個(gè)工具是作為“Project Butter”一部分同Jelly Bean一同發(fā)布的,它能夠從內(nèi)核級(jí)檢測(cè)你設(shè)備的運(yùn)行狀態(tài)。Systrace可配置的參數(shù)很多。我們這里重點(diǎn)關(guān)注UI是怎么渲染的,用systrace檢測(cè)卡頓問(wèn)題。

Systrace和之前的工具不同的是,它記錄的是整個(gè)android系統(tǒng)的狀態(tài),并不是針對(duì)某一個(gè)app 的。所以最好是用運(yùn)行app比較少的設(shè)備來(lái)做檢測(cè),這樣就不會(huì)受到其他app的干擾了。Systrace圖標(biāo)是綠色和粉紅色組成的(下圖紅色的橢圓里)。點(diǎn)擊下,會(huì)彈出一個(gè)帶幾個(gè)選項(xiàng)的窗口。

圖4-25

trace數(shù)據(jù)記錄在一個(gè)html文件里,可以用瀏覽器打開(kāi)。這里主要研究屏幕的交互數(shù)據(jù),主要收集CPU,graphics和view數(shù)據(jù)(如圖4-25所示)。duration留空(默認(rèn)是5秒)。點(diǎn)擊OK之后,Systrace會(huì)馬上開(kāi)始采集設(shè)備上的數(shù)據(jù)(最好馬上開(kāi)始操作)。因?yàn)椴杉臄?shù)據(jù)非常之多,所以最好一次只針對(duì)一個(gè)問(wèn)題。

traces里面的數(shù)據(jù)看著有點(diǎn)嚇人(我們只是勾選里4個(gè)選項(xiàng)!)。鼠標(biāo)可以控制滑動(dòng),WASD可以用來(lái)zoom in/out(W,S)和左右滑動(dòng)(A,D)。在剛跑的trace數(shù)據(jù)最上面,能看到CPU的詳細(xì)數(shù)據(jù),CPU數(shù)據(jù)的下面是幾個(gè)可折疊的區(qū)域,分別表示不同的活躍進(jìn)程。每一個(gè)色條表示系統(tǒng)的一個(gè)行為,色條的長(zhǎng)度表示該行為的耗時(shí)(放大可以看到更多細(xì)節(jié))。選中屏幕底部的一個(gè)色條,第一眼看到的總覽有點(diǎn)嚇人,我們一條條分析看下這些數(shù)據(jù)。

圖4-26



Systrace進(jìn)化史

就像android生態(tài)圈一樣,Systrace在不同的系統(tǒng)版本里有不同的界面,展示,和輸出結(jié)果。

在Jelly Bean設(shè)備,在設(shè)置的開(kāi)發(fā)者選項(xiàng)里可以打開(kāi)tracing。必須要同時(shí)打開(kāi)電腦和手機(jī)上的該功能。

隨著android系統(tǒng)版本的升級(jí),trace生成的數(shù)據(jù)也更加詳細(xì),布局也有一些改變。

我建議通過(guò)Jelly Bean查看Systraaces,然后喝Lollipop上的數(shù)據(jù)對(duì)比,收集到的數(shù)據(jù)會(huì)不一樣。

在2015年的google io大會(huì)上,google發(fā)布了新版本的Systrace,新版本增加了一些新特性,下面會(huì)有更詳細(xì)的介紹。

我們繼續(xù)滑動(dòng)Systrace的輸出結(jié)果,運(yùn)行期間每個(gè)進(jìn)程的數(shù)據(jù)都可以看到。我們主要研究卡頓相關(guān)信息,查看屏幕刷新時(shí)可能有問(wèn)題的繪制。只要刷新率和繪制都正常,屏幕的渲染應(yīng)該就是流暢的。但只要一個(gè)出問(wèn)題,就有可能會(huì)導(dǎo)致頁(yè)面渲染的卡頓。


Systrace Screen Painting

我們通過(guò)圖4-24來(lái)看下屏幕繪制的步驟。最頂部一行的trace(藍(lán)色高亮)時(shí)VSYNC,由一些均勻分布的藍(lán)綠色寬條組成。VSYNC是操作系統(tǒng)發(fā)來(lái)的信號(hào),表示此時(shí)該刷新屏幕了。每個(gè)寬條表示16ms(寬條之間的空白也是16ms)。當(dāng)VSYNC事件發(fā)生的時(shí)候(在藍(lán)綠色寬條的任意一側(cè)),surface flinger(紅色高亮方框包含幾種顏色的長(zhǎng)條)會(huì)從view buffer(沒(méi)展示出來(lái))里選一個(gè)view,然后繪制到屏幕上。理想情況下,surfaceflinger事件之間相距16ms(沒(méi)有卡頓),因此如果出現(xiàn)長(zhǎng)條空缺則表示surfaceflinger丟掉了一次VSYNC更新事件,屏幕就沒(méi)有及時(shí)的刷新(此時(shí)就會(huì)有卡頓)。在trace文件2/3的位置可以看到這樣的空缺(綠色高亮方框)。

圖4-27

圖4-27底部展示的是app的詳情。第二行數(shù)據(jù)(綠色和紫色的線條)表示的app正在創(chuàng)建view,然后是底部的數(shù)據(jù)(綠色,藍(lán)色,和一些紫色的條狀),表示的是RenderThread,view的渲染和發(fā)送到buffer(圖中沒(méi)有畫(huà)出來(lái))都是在這個(gè)線程里做的。注意看可以發(fā)現(xiàn)大概1/3的位置,這些條狀在該區(qū)域集中變粗了,表示app此時(shí)由于某種原因發(fā)生了卡頓。不同app情況不一樣,發(fā)生卡頓的原因也不同,但是我們可以根據(jù)一些共同的現(xiàn)象推測(cè)卡頓的發(fā)生。

這種總覽很適合查找卡頓,但要調(diào)查清楚原因需要放大仔細(xì)看下。要明白Systrace都記錄了什么數(shù)據(jù),最好搞明白Systrace到底是怎么進(jìn)行測(cè)量的,app沒(méi)有卡頓的時(shí)候Systrace輸出又是什么樣的。一旦弄明白了Systrace是怎么工作的,查找問(wèn)題就方便多了。在圖4-28中,我把a(bǔ)pp正常運(yùn)行時(shí)Systrace紀(jì)錄的相關(guān)線條放到了一起。我們從屏幕左邊的droid.yahoo.com看起。我描述的時(shí)候在trace文件里會(huì)來(lái)回跳動(dòng)到不同的位置。當(dāng)繪制發(fā)生的時(shí)候:

紅色方框:droid.yahoo.com完成了所有view的measure,然后把結(jié)果發(fā)送給RenderThread。

橘色方框:RenderThread,這里app會(huì):

????????????????繪制frame(淺綠色)

????????????????顯示buffer里的內(nèi)容(灰色)?

????????????????清空buffer(紫色)

????????????????發(fā)送給緩存的view列表。

黃色方框:com.yahoo.mobile.client.andr…

buffer里面有一些view,線條的高度表示了buffer當(dāng)中view的數(shù)量。剛開(kāi)始,只有一個(gè),當(dāng)新的view加入到buffer中之后,高度就變成了2倍。

綠色方框:VSYNC-sf 提示surface flinger有16ms的時(shí)間來(lái)渲染屏幕。里面棕色的條狀表示16ms的長(zhǎng)度。

藍(lán)色方框:surfaceflinger從隊(duì)列里抓取一個(gè)view(注意黃色方框里的buffer中view數(shù)量從2變?yōu)?)。完成之后,view被發(fā)送給GPU,屏幕就繪制被繪制了。

紫色方框:VSYNC-app告訴app去渲染新的view(這里有個(gè)16ms的timer)。

當(dāng)VSYNC一開(kāi)始,droid.yahoo.att就不停的重復(fù)這個(gè)過(guò)程,measure view,發(fā)送給RenderThread等等,不停的循環(huán)。

圖4-28

再回過(guò)頭想一下設(shè)備能這么短的時(shí)間內(nèi)流暢的渲染屏幕,確實(shí)是件很神奇的事情。了解了渲染的過(guò)程,我們來(lái)找下卡頓的原因。

圖4-29中,我們看下OS層的行為。我增加了一些箭頭來(lái)表示16ms的間隔,紅色的方框表示surfaceflinger的丟幀。

圖4-29

為什么會(huì)出現(xiàn)這種情況?箭頭上方的一行是view buffer,行的高度表示有多少幀緩存在了buffer里面。trace開(kāi)始的時(shí)候,buffer里緩存的數(shù)量是1到2交替出現(xiàn)。surfaceflinger每抓取一個(gè)view(buffer里的數(shù)量減一),又會(huì)馬上從app里生成一個(gè)新的view來(lái)填充。但是當(dāng)surfaceflinger完成第三個(gè)動(dòng)作之后,buffer被清空了,但是沒(méi)有從app里及時(shí)填充新的view。所以,我們從app層面來(lái)檢查下這期間發(fā)生了什么。

在圖4-30中,我們可以看到開(kāi)始的時(shí)候RenderThread發(fā)送了一個(gè)view到buffer(紅色方框)。橘色方框表示app新建了另一個(gè)view,渲染,然后交給buffer(droid.yahoo.att measure,layout所有的view,RenderThread負(fù)責(zé)繪制)。不幸的是,app沒(méi)來(lái)得及創(chuàng)建新view就被掛起了(黃色方框內(nèi))。為了創(chuàng)建下一個(gè)view,droid.yahoo.att app在運(yùn)行暗綠色的“performTraversals”(3ms)之前,要先運(yùn)行“obtainView” 7ms,“setupListItem” 8.7ms。app然后把數(shù)據(jù)交給RenderThread,這一步也比較慢(12ms)。創(chuàng)建這一幀總共用了近31ms(上一個(gè)只用了6ms)。當(dāng)創(chuàng)建這一幀開(kāi)始的時(shí)候,buffer里只有一幀的數(shù)據(jù),但是設(shè)備需要兩幀。buffer沒(méi)有被填滿,所以屏幕繪制出現(xiàn)了卡頓。

圖4-30

有意思的是app后面馬上就速度追了上來(lái)。黃色方框內(nèi)延遲遞交的view創(chuàng)建并交給buffer之后,后續(xù)的兩幀緊接著創(chuàng)建好了(綠色和藍(lán)色的方框)。通過(guò)快速的填充新的幀,app就只丟了一幀。這個(gè)trace結(jié)果是在Nexus 6上運(yùn)行的(處理器比較快,能快速的跟上)。在三星S4 Mini,Jelly Bean 4.2.2上運(yùn)行同樣的結(jié)果得到圖4-31.

圖4-31

從總覽圖上可以清晰的看到有很多幀都丟掉了(trace開(kāi)始的時(shí)候surfacelinger部分有很多的空缺)。而且頂部那一行(view buffer)里的buffer經(jīng)常是空的(導(dǎo)致里卡頓),buffer里同時(shí)有兩個(gè)view的情況非常少。對(duì)于一個(gè)GPU性能比較差的設(shè)備來(lái)說(shuō),app能夠像Nexus 6一樣趕上填滿buffer的概率比較小。

小貼示: 其實(shí)你可以偶爾渲染一幀超過(guò)16ms,因?yàn)閎uffer里面一般都有1到2幀準(zhǔn)備好的view備用。但是如果超過(guò)2-3幀渲染很慢,用戶就會(huì)感覺(jué)到卡頓了。

上面的trace是在運(yùn)行Jelly Bean的手機(jī)上跑的,RenderThread的數(shù)據(jù)歸到了droid.yahoo.att那一行(Lollipop之前measure,draw,layout都是和在一起的)。把每一行數(shù)據(jù)合在一起之后豎條變寬。每一次調(diào)用之間的細(xì)條空白說(shuō)明手機(jī)在每幀的繪制之后,只剩下很少的時(shí)間處理其它任務(wù)。手機(jī)上的app只能稍稍領(lǐng)先surfacelinger填滿buffer的速度。如果app能夠減小所繪制view的復(fù)雜度,也就是加快view的渲染,細(xì)條的空白就會(huì)變的寬一點(diǎn),buffer填滿的概率就更大,也就給低端設(shè)備在繪制之外更多的空間去處理其它任務(wù)。

把這塊區(qū)域加高亮之后,Systrace會(huì)把所有條狀所占的時(shí)間計(jì)算出一個(gè)總和,用鼠標(biāo)在上面依次移動(dòng)就能看到基本的數(shù)據(jù)了。圖4-32中,可以看到performtraversals(父view的draw命令)平均用了13.8ms,大概有5ms的波動(dòng)。16ms的卡頓閾值在波動(dòng)的范圍之內(nèi),所以很有可能設(shè)備上會(huì)有卡頓。

圖4-32

把這塊放大能看到更多的細(xì)節(jié)(圖4-33)。每個(gè)垂直的紅線表示16ms。從圖中可以看出,大概有5,6次SurfaceFlinger錯(cuò)過(guò)了紅線標(biāo)記。綠色的“performtraversals”線條都幾乎有16ms長(zhǎng)(這一步是必須做的,有卡頓)。還有兩個(gè)藍(lán)綠色的 deliverInputEvents(每個(gè)都超過(guò)了16ms)也阻礙了app的屏幕繪制。

圖4-33

那到底是什么觸發(fā)了deliverInputEvents呢?這其實(shí)是用戶在點(diǎn)擊屏幕,強(qiáng)制ListView重繪所有的view。這部分影響是CPU,我們接下來(lái)簡(jiǎn)單看下這時(shí)候CPU都在干啥。


Systrace和CPU對(duì)渲染的影響

如果你頻繁的感覺(jué)到卡頓,但是在繪制或者surfaceflinger部分看不到什么明顯的異常,這時(shí)候可以嘗試看下CPU在處理什么事情,在Systrace的頂部可以看到這部分的數(shù)據(jù)。如果你能大概猜到是哪部分的邏輯影響了繪制,可以先把這部分代碼注釋掉試試。山羊app里有個(gè)選項(xiàng)可以開(kāi)啟Fibonacci延遲。打開(kāi)之后,app在每一行數(shù)據(jù)渲染的時(shí)候都會(huì)計(jì)算一個(gè)很大的Fibonacci值。用膝蓋想都知道這時(shí)CPU會(huì)變得很忙。由于計(jì)算是在主線程做的,會(huì)妨礙的view的渲染,理所當(dāng)然就導(dǎo)致里丟幀,滑動(dòng)也會(huì)變的很卡。圖4-24里顯示的log就能看到這種情況下的丟幀。我們?cè)偕钔谝稽c(diǎn)看能不能通過(guò)Systrace定位到計(jì)算Fibonacci數(shù)的代碼。

我們?cè)僦仡^看下trace數(shù)據(jù),圖4-34里是沒(méi)有優(yōu)化過(guò)的山羊app在Nexus 6上跑的數(shù)據(jù)。

圖4-34

展示做了一些修改,CPU和surfaceflinger之間的一些線被去掉了。這個(gè)trace里看不到卡頓,surfaceflingers每16ms的間隔很均勻。RenderThread和每一行view填滿buffer的表現(xiàn)也很正常。和CPU那一行數(shù)據(jù)對(duì)比一下,可以發(fā)現(xiàn)一個(gè)新規(guī)律。當(dāng)RenderThread在繪制layout的時(shí)候,CPU1正在運(yùn)行一個(gè)藍(lán)色的任務(wù)(注意我們看的是窄一點(diǎn)的CPU1,不是CPU1:C-State)。當(dāng)山羊app的view正在被measure的時(shí)候,CPU0有一個(gè)相應(yīng)的紫色的行為。view的layout和繪制是由兩個(gè)CPU完成的。注意X軸上的點(diǎn)擊是每隔10ms發(fā)生的,這里每個(gè)行為都沒(méi)有超過(guò)2-4ms。

當(dāng)我們加入費(fèi)時(shí)的Fibonacci計(jì)算之后,Systrace的結(jié)果看起來(lái)就很不一樣了。(圖4-35)

圖4-35

從Systrace里能看到很多卡頓,在相同的100ms時(shí)間范圍內(nèi),surfaceflinger就畫(huà)了三幀(上面不卡頓的情況畫(huà)了7幀)。可以看到RenderThread繪制view還是很快的(從圖中可以看出,藍(lán)色的RenderThread是在CPU0上運(yùn)行的)。但是,measure view的時(shí)候,F(xiàn)ibonacci的遞歸計(jì)算就導(dǎo)致了問(wèn)題。山羊app進(jìn)程那一行花了大部分的時(shí)間在obtainView的狀態(tài),而不是measure。同時(shí)可以看到CPU1上紫色對(duì)應(yīng)的山羊進(jìn)程不再是2-4ms寬了,變成了2-17ms寬。Fibonacci計(jì)算每次大概用了13-17ms,對(duì)app的繪制性能產(chǎn)生了很大的影響。


Systrace更新-I/O 2015

在2015年Google I/O大會(huì)上,google發(fā)布了新版本的systrace,上面提到的分析數(shù)據(jù)變的更簡(jiǎn)單了。在圖4-27里,我把每一幀的更新都高亮出來(lái)了。在新版本的systrace(圖4-36)里,每一幀都是由一個(gè)帶F的小圓圈標(biāo)示的。正常渲染的幀會(huì)有綠點(diǎn),慢幀則是黃色或者紅色。選擇一個(gè)點(diǎn),然后按下m就可以高亮某一幀,分析起來(lái)更方便。

圖4-36

新版本的systrace對(duì)于正在發(fā)生的行為也有更清晰的描述了。在圖4-36中,幀的渲染時(shí)間是18.181ms,是用黃色標(biāo)示的,如果有很多幀超過(guò)了16ms就會(huì)導(dǎo)致卡頓了。在trace文件下方的描述信息面板上(圖4-37),可以看到警告信息,說(shuō)我的app在重用ListView的item,而不是創(chuàng)建新的item,這樣拖慢了view inflation。

圖4-37

在systrace里可以看到其它類(lèi)似的警告,形狀像泡泡或是點(diǎn),屏幕右邊的警告面板也列出了這些信息(圖4-38)。

圖4-38

這些新功能讓Systrace診斷UI問(wèn)題更加簡(jiǎn)單了。


第三方工具

每個(gè)大的芯片廠商都有自己的GPU評(píng)測(cè)工具,可以幫助發(fā)現(xiàn)更多渲染時(shí)遇到瓶頸的信息。這些工具對(duì)一些特定的芯片更有針對(duì)性,信息也更多。可以幫你針對(duì)不同的GPU做更深度的優(yōu)化。Qualcomm,NVIDIA和Intel都提供了這些開(kāi)發(fā)者工具,有興趣的可以自己試下。

感知優(yōu)化

上面的內(nèi)容都是在討論怎么通過(guò)測(cè)試,調(diào)試,優(yōu)化布局來(lái)讓UI的體驗(yàn)更快。其實(shí)還有另外一個(gè)辦法讓你的app UI更快:讓用戶感覺(jué)更快。當(dāng)然作為開(kāi)發(fā)者要盡可能優(yōu)化自己的代碼,view,overdraw和其它所有可能會(huì)影響渲染性能的地方,上面這些都做了之后,再考慮下面這些能讓用戶覺(jué)得你的app更快的方法。

人類(lèi)大腦工作的方式很有意思,通過(guò)改變大腦對(duì)等待的感知,可以讓你的用戶感覺(jué)延遲變短了。雜貨店的老板都會(huì)在走廊上放一些沒(méi)用的雜志,就是為了讓客戶有東西可以看,感覺(jué)等待的時(shí)間就會(huì)短一些。如果在向用戶展示內(nèi)容的時(shí)候增加一些過(guò)渡效果,見(jiàn)效明顯。這就像一個(gè)小魔術(shù)一樣讓用戶感覺(jué)體驗(yàn)變的更快了,歸根結(jié)底重要的是用戶覺(jué)得你的app有多快。這個(gè)技巧實(shí)現(xiàn)起來(lái)也有點(diǎn)取巧,有時(shí)候這種感知的優(yōu)化甚至?xí)玫较喾吹男Ч鯝/B test來(lái)確保你的優(yōu)化對(duì)用戶來(lái)說(shuō)真的有效。


loading菊花:優(yōu)缺點(diǎn)

loading菊花,進(jìn)度條,沙漏圖標(biāo),和其它所有表示等待的方式都存在很久了。這些都可以讓app的內(nèi)容過(guò)渡變得更快。比如在app里加一個(gè)進(jìn)度條,加載的時(shí)候播放一個(gè)進(jìn)度的動(dòng)畫(huà)來(lái)讓用戶等待。研究表明使用一個(gè)帶有動(dòng)畫(huà)的滑動(dòng)條的時(shí)候用戶會(huì)感覺(jué)更舒服。快速旋轉(zhuǎn)的loading菊花也讓用戶感覺(jué)等待的時(shí)間更短。

但是,有延遲的時(shí)候,加個(gè)菊花并不總是有效的。iOS app Polar的開(kāi)發(fā)者發(fā)現(xiàn)他們的app渲染一個(gè)view的時(shí)候有一點(diǎn)延遲。他們第一反應(yīng)是在頁(yè)面里加了一個(gè)菊花告訴用戶頁(yè)面正在渲染內(nèi)容,但效果不如預(yù)期。用戶開(kāi)始反饋app變慢了,等待頁(yè)面加載的時(shí)間變長(zhǎng)了(其實(shí)app沒(méi)有變慢,不過(guò)是加了一個(gè)菊花)。加了個(gè)等待的標(biāo)識(shí)之后讓用戶明顯的感覺(jué)到他們?cè)诘取H∠栈ㄖ螅脩舾杏X(jué)app又變快了(開(kāi)發(fā)者僅僅是改變了菊花)。通過(guò)改變用戶對(duì)等待的感知,可以讓用戶覺(jué)得app變快了。Facebook也遇到過(guò)類(lèi)似的問(wèn)題:使用自己定制的菊花讓用戶感覺(jué)更慢,用默認(rèn)菊花感覺(jué)更快。

增加菊花最好讓用戶測(cè)試下他們的真實(shí)感受。一般來(lái)說(shuō),當(dāng)?shù)却臅r(shí)間稍微有點(diǎn)長(zhǎng)的時(shí)候,增加菊花是可以接受的:比如打開(kāi)一個(gè)新頁(yè)面或者從網(wǎng)上下載一張圖片。如果延遲很短(一般來(lái)說(shuō)小于一秒),就應(yīng)該考慮去掉菊花了。這種情況下應(yīng)該讓用戶覺(jué)得他們并沒(méi)有在等。


用動(dòng)畫(huà)來(lái)抵消等待的時(shí)間

點(diǎn)擊后看到一個(gè)空白的屏幕會(huì)讓用戶感覺(jué)在等待。就是這個(gè)原因讓瀏覽器在點(diǎn)擊鏈接,新頁(yè)面刷出來(lái)之前都是展示舊的頁(yè)面。在手機(jī)app里,一般來(lái)說(shuō)我們不希望讓用戶停留在老的頁(yè)面上,一個(gè)快速的切換動(dòng)畫(huà)可以爭(zhēng)取到足夠的時(shí)間讓下一個(gè)頁(yè)面準(zhǔn)備就緒。可以觀察下你最常用的android app,當(dāng)頁(yè)面切換的時(shí)候有多少?gòu)倪吷匣蛘叩撞砍霈F(xiàn)的動(dòng)畫(huà)。


瞬時(shí)更新的小謊言

如果你的用戶在頁(yè)面上做了更新數(shù)據(jù)的操作,即使數(shù)據(jù)還沒(méi)抵達(dá)服務(wù)器,可以馬上把用戶看到的數(shù)據(jù)更新掉(當(dāng)然開(kāi)發(fā)者要保證這些數(shù)據(jù)能100%抵達(dá)服務(wù)器)。比如說(shuō),你在Instagram上點(diǎn)了贊,頁(yè)面上馬上就更新了贊的狀態(tài),其實(shí)贊的狀態(tài)甚至可能還沒(méi)有更新到服務(wù)器。Instangram的開(kāi)發(fā)者管這叫“行為最優(yōu)化”,狀態(tài)的更新要幾秒后才能到服務(wù)器并對(duì)網(wǎng)站的用戶可見(jiàn)(網(wǎng)速不好的時(shí)候可能要幾分鐘),但是更新最后都會(huì)成功,等待服務(wù)器返回成功其實(shí)是沒(méi)必要的。移動(dòng)端用戶一般都不希望在等待,只要最后能成功就好。

瞬時(shí)更新的另一個(gè)好處是,用戶會(huì)感覺(jué)你的app在網(wǎng)速或者信號(hào)不好(火車(chē)經(jīng)過(guò)隧道)的時(shí)候也能正常工作。FlipBoard就做過(guò)一個(gè)離線發(fā)送網(wǎng)絡(luò)請(qǐng)求的框架,可以很方便的應(yīng)用到更新UI。

另一個(gè)優(yōu)化的小技巧是提前上傳。對(duì)于像Instagram這種app來(lái)說(shuō),上傳大量的圖片會(huì)增加主線程的延遲,提前開(kāi)始上傳這些圖片會(huì)是個(gè)好辦法。Instagram發(fā)現(xiàn)發(fā)一個(gè)新post是慢在上傳圖片這一步,所以Instagram就在用戶在圖片上添加文字的間隙開(kāi)始上傳圖片了,圖片被真正發(fā)布到服務(wù)器之前就已經(jīng)傳好了。用戶只要一點(diǎn)擊Post按鈕,就只需要上傳文本和創(chuàng)建post的命令了,這樣就會(huì)讓用戶感知非常快。Instagram在遇到“是否要添加菊花”這個(gè)問(wèn)題時(shí),他們的答案是通過(guò)改變架構(gòu)的方式永遠(yuǎn)的杜絕菊花。


提升感知體驗(yàn)的小提示

當(dāng)app的速度通過(guò)優(yōu)化代碼或者view的優(yōu)化提升之后,你可以用秒表來(lái)測(cè)試下結(jié)果。有些感知是可以用秒表測(cè)量的(Instagram的例子),有些則不能(菊花的例子)。當(dāng)常規(guī)的分析或者測(cè)量工具不可靠的時(shí)候,需要讓用戶來(lái)真正的體驗(yàn)這些優(yōu)化效果。可以做一些可用性測(cè)試,增加測(cè)試的范圍,A/B測(cè)試,這些才能真正的讓你確認(rèn)你的優(yōu)化是讓用戶更開(kāi)心還是更沮喪。



總結(jié)

Android app的用戶體驗(yàn)直接跟屏幕上展現(xiàn)的內(nèi)容相關(guān)。如果app的內(nèi)容加載很慢或者滑動(dòng)不夠流暢,用戶的感知就是負(fù)面的。在這篇文章,我們講了如何優(yōu)化view樹(shù)形結(jié)構(gòu),看是否扁平或者簡(jiǎn)化view等等。我們還講了怎么檢測(cè)解決overdraw的問(wèn)題。還有一些需要深度分析的優(yōu)化(像CPU導(dǎo)致的問(wèn)題),systrace很適合發(fā)現(xiàn)和解決這種卡頓問(wèn)題。最后是一些讓你的app感覺(jué)更快的小技巧,比如把CPU或者網(wǎng)絡(luò)相關(guān)的任務(wù)延后處理,不要影響繪制渲染。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容