Android 性能優(yōu)化(UI渲染)

注意事項(xiàng):

  • 布局優(yōu)化;盡量使用include展融、merge蜗搔、ViewStub標(biāo)簽,盡量不存在冗余嵌套及過于復(fù)雜布局(譬如10層就會(huì)直接異常)畦贸,盡量使用GONE替換INVISIBLE陨闹,使用weight后盡量將width和heigh設(shè)置為0dp減少運(yùn)算,Item存在非常復(fù)雜的嵌套時(shí)考慮使用自定義Item View來取代薄坏,減少measure與layout次數(shù)等趋厉。

  • 列表及Adapter優(yōu)化;盡量復(fù)用getView方法中的相關(guān)View胶坠,不重復(fù)獲取實(shí)例導(dǎo)致卡頓君账,列表盡量在滑動(dòng)過程中不進(jìn)行UI元素刷新等。

  • 背景和圖片等內(nèi)存分配優(yōu)化沈善;盡量減少不必要的背景設(shè)置乡数,圖片盡量壓縮處理顯示,盡量避免頻繁內(nèi)存抖動(dòng)等問題出現(xiàn)矮瘟。

  • 自定義View等繪圖與布局優(yōu)化瞳脓;盡量避免在draw、measure澈侠、layout中做過于耗時(shí)及耗內(nèi)存操作劫侧,尤其是draw方法中,盡量減少draw哨啃、measure烧栋、layout等執(zhí)行次數(shù)。

  • 避免ANR拳球,不要在UI線程中做耗時(shí)操作审姓,遵守ANR規(guī)避守則,譬如多次數(shù)據(jù)庫操作等祝峻。
    這里

設(shè)計(jì)師魔吐,開發(fā)人員扎筒,需求研究和測(cè)試都會(huì)影響到一個(gè)app最后的UI展示,所有人都很樂于去建議app應(yīng)該怎么去展示UI酬姆。UI也是app和用戶打交道的部分嗜桌,直接對(duì)用戶形成品牌意識(shí),需要仔細(xì)的設(shè)計(jì)辞色。無論你的app UI是簡(jiǎn)單還是復(fù)雜骨宠,重要的是性能一定要好。
UI性能測(cè)試
性能優(yōu)化都需要有一個(gè)目標(biāo)相满,UI的性能優(yōu)化也是一樣层亿。你可能會(huì)覺得“我的app加載很快”很重要,但我們還需要了解終端用戶的期望立美,是否可以去量化這些期望呢匿又?我們可以從人機(jī)交互心理學(xué)的角度來考慮這個(gè)問題。研究表明建蹄,0-100毫秒以內(nèi)的延遲對(duì)人來說是瞬時(shí)的琳省,100-300毫秒則會(huì)感覺明顯卡頓,300-1000毫秒會(huì)讓用戶覺得“手機(jī)卡死了”躲撰,超過1000ms就會(huì)讓用戶想去干別等事情了针贬。
這是人類心理學(xué)最基礎(chǔ)的理論,我們可以從這個(gè)角度去優(yōu)化頁面/view/app的加載時(shí)間拢蛋。 Ilya Grigorik 有一個(gè)很棒的演講桦他,是關(guān)于搭建1000毫秒內(nèi)加載完成移動(dòng)網(wǎng)站的。如果你的網(wǎng)頁能在1秒內(nèi)加載好谆棱,就超過了人類感知的預(yù)期快压,你的用戶一定會(huì)感覺很滿意。還有研究表明垃瞧,如果網(wǎng)頁在3-4秒內(nè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è)诤竺娴奈恼略倭摹?br> 卡頓(Jank)
內(nèi)容的快速加載很重要奕污,渲染的流暢性也很重要萎羔。android團(tuán)隊(duì)把滯緩,不流暢的動(dòng)畫定義為jank碳默,一般是由于丟幀引起的贾陷。安卓設(shè)備的屏幕刷新率一般是60幀每秒(1/60fps=16.6ms每幀)缘眶,所以你想要渲染的內(nèi)容能在16ms內(nèi)完成十分關(guān)鍵。每丟一幀髓废,用戶就會(huì)感覺的動(dòng)畫在跳動(dòng)磅崭,會(huì)出現(xiàn)違和感。為了保證動(dòng)畫的流暢性瓦哎,我們接下來看下從哪些方面優(yōu)化可以讓內(nèi)容在16ms內(nèi)渲染完成,同時(shí)分析一些常見的導(dǎo)致UI卡頓的問題柔逼。
android設(shè)備的UI渲染性能
早期android用戶抱怨最多的就是UI蒋譬,尤其是觸碰反饋和動(dòng)畫流暢度,感覺都很卡愉适。后來隨著android系統(tǒng)逐漸成熟犯助,開發(fā)人員也投入了大量的時(shí)間和精力讓交互變的流暢起來。下面列舉一些不同系統(tǒng)版本所帶來的提升:
在Gingerbread或者更早的設(shè)備上维咸,屏幕完全是由軟件繪制(CPU繪制)的(不需要GPU的參與)剂买。后來隨著屏幕尺寸變大和像素的提升,純粹靠軟件繪制遇到了瓶頸癌蓖。

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)打開的结胀。將軟件繪制(CPU)的壓力大部分轉(zhuǎn)移到了GPU上。

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

我們從普通開發(fā)者的角度歹垫,來逐一看下這些提升和相關(guān)的測(cè)量工具耸别。我們的目標(biāo)很明顯:
屏幕繪制低延遲
保證流程穩(wěn)定的幀率來避免卡頓

當(dāng)android開發(fā)團(tuán)隊(duì)引入這些UI流暢性的提升時(shí),他們需要能量化這些提升的工具县钥。經(jīng)由他們的努力秀姐,這些工具都打包進(jìn)了SDK以方便開發(fā)者們來檢測(cè)UI相關(guān)的性能問題。接下來我們就使用這些工具來優(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í)候伸头,一定要留意屏幕右上角的組件樹(Component Tree)。套嵌的子view越深舷蟀,組件樹就越復(fù)雜恤磷,渲染起來也就越費(fèi)時(shí)間。
圖4-1


對(duì)于app里的每一個(gè)view野宜,android系統(tǒng)都會(huì)經(jīng)過三部曲來渲染:measure扫步,layout,draw匈子『犹ィ可以在腦中回想下你搭建的view的xml布局文件結(jié)構(gòu),measure從最頂部的節(jié)點(diǎn)開始虎敦,順著layout樹形結(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)提供自己的尺寸來決定展示的位置晌区,遇到?jīng)_突的時(shí)候,父節(jié)點(diǎn)可以強(qiáng)制子節(jié)點(diǎn)重新measure(由此可能導(dǎo)致measure的時(shí)間消耗為原來的2-3倍)通贞。這就是為什么扁平的view結(jié)構(gòu)會(huì)性能更好朗若。節(jié)點(diǎn)所處位置越深昌罩,套嵌帶來的measure越多哭懈,計(jì)算就會(huì)越費(fèi)時(shí)。我們來看一些具體的例子茎用,看measure是怎么影響渲染性能的遣总。
Remeasureing Views(重新測(cè)量views)
并不是只有發(fā)生錯(cuò)誤的時(shí)候才會(huì)觸發(fā)remeasure。RelativeLayouts經(jīng)常需要measure所有子節(jié)點(diǎn)兩次才能把子節(jié)點(diǎn)合理的布局轨功。如果子節(jié)點(diǎn)設(shè)置了weights屬性旭斥,LinearLayouts也需要measure這些節(jié)點(diǎn)兩次,才能獲得精確的展示尺寸古涧。如果LinearLayouts或者RelativeLayouts被套嵌使用垂券,measure所費(fèi)時(shí)間可能會(huì)呈指數(shù)級(jí)增長(zhǎng)(兩個(gè)套嵌的views會(huì)有四次measure,三個(gè)套嵌的views會(huì)有8次的measure)羡滑」阶Γ可以看下面圖4-9里面一個(gè)夸張點(diǎn)的例子算芯。
一旦view開始被measure,該view所有的子view都會(huì)被重新layout凳宙,再把該view傳遞給它的父view熙揍,如此重復(fù)一直到最頂部的根view。layout完成之后氏涩,所有的view都被渲染到屏幕上届囚。需要特別注意到是,并不是只有用戶看得見的view才會(huì)被渲染是尖,所有的view都會(huì)意系。后面我們會(huì)看下“屏幕重復(fù)繪制”的問題。app擁有的views越多析砸,measure,layout爆袍,draw所花費(fèi)的時(shí)間就越久首繁。要縮短這個(gè)時(shí)間,關(guān)鍵是保持view的樹形結(jié)構(gòu)盡量扁平陨囊,而且要移除所有不需要渲染的view弦疮。移除這些view會(huì)對(duì)加速屏幕渲染產(chǎn)生明顯的效果。理想情況下蜘醋,總共的measure胁塞,layout,draw時(shí)間應(yīng)該被很好的控制在16ms以內(nèi)压语,以保證滑動(dòng)屏幕時(shí)UI的流暢啸罢。
雖然可以通過xml文件查看所有的view,但不一定能輕易的查出哪些view是多余的胎食。要找到那些多余的view(增加渲染延遲的view)扰才,可以用android studio monitor里的Hierarachy 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)行在帶有開發(fā)者版本的android系統(tǒng)的設(shè)備上。后續(xù)所有的view和屏幕截圖都來自一款三星的Note II設(shè)備递雀,系統(tǒng)版本是Jelly Bean柄延。在老的設(shè)備(處理器慢)上測(cè)試渲染性能,更容易發(fā)現(xiàn)問題缀程。
如圖4-2所示拦焚,打開Hierarchy Viewer之后蜡坊,會(huì)看到幾個(gè)窗口:左邊的窗口列出了連上你電腦的android設(shè)備和設(shè)備上所有運(yùn)行的進(jìn)程∈臧埽活躍的進(jìn)程是粗體展示的秕衙。第二個(gè)tab某一個(gè)編譯版本的詳情(后面細(xì)說)。中間的部分是可縮放的view的樹形圖僵刮。點(diǎn)擊某一個(gè)view能看到在設(shè)備上展示的樣子和一些額外的數(shù)據(jù)据忘。右邊有兩個(gè)view:樹形結(jié)構(gòu)總覽和布局view。樹形結(jié)構(gòu)總覽顯示了整個(gè)view的樹形結(jié)構(gòu)搞糕,里面有一個(gè)方塊顯示了中間窗口在整個(gè)樹形結(jié)構(gòu)當(dāng)中所處的位置勇吊。布局view當(dāng)中深紅色高亮的區(qū)域表示所選中的view被繪制的部分(淺紅色展示的是父view)。
圖4-2

在中間的這個(gè)窗口窍仰,你可以點(diǎn)擊任何一個(gè)view來查看該view在android設(shè)備屏幕上的展示汉规。點(diǎn)擊樹形圖工具欄里紅綠紫三色的維恩圖圖標(biāo),還能展示子view的數(shù)量驹吮,和measure针史,layout,draw三部曲所花費(fèi)的時(shí)間碟狞。這個(gè)時(shí)間是被選擇的view及其所有子節(jié)點(diǎn)所花費(fèi)時(shí)間的總和啄枕。(圖4-3中,我選擇了最頂部的view來獲取整個(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的樹形結(jié)構(gòu)圖預(yù)覽,看看所有的view是怎么拼湊到一起的盖溺。從樹形結(jié)構(gòu)圖上可以看出屏幕里有非常多的view窟绷,樹的結(jié)構(gòu)比較扁平。前面說過咐柜,扁平的結(jié)構(gòu)性能好兼蜈,樹的深度對(duì)渲染的性能會(huì)產(chǎn)生很大的影響。我們的結(jié)構(gòu)雖然是扁平的拙友,卻依然花費(fèi)了26ms的時(shí)間來渲染为狸,說明扁平的結(jié)構(gòu)也有可能會(huì)卡頓,也需要去考慮怎么優(yōu)化遗契。
圖4-4

排查一個(gè)新聞?lì)恆pp的樹形結(jié)構(gòu)辐棒,大致可以看三個(gè)區(qū)域:頭部(底部藍(lán)色的方框),文章列表(兩個(gè)橙色的方框表示兩個(gè)不同的tab),單篇文章的view是用紅色方框來標(biāo)注的漾根。內(nèi)部標(biāo)題view的結(jié)構(gòu)重復(fù)出現(xiàn)了九次泰涂,5個(gè)在上面橙色的方框內(nèi),4個(gè)在下面的方框內(nèi)辐怕。最后逼蒙,我們可以看到從邊上拉出來的導(dǎo)航欄是用底部綠色的方框標(biāo)出來的。頭部用了22個(gè)view寄疏,兩個(gè)文章列表個(gè)用了67和44個(gè)view(每個(gè)標(biāo)題部分使用了13個(gè)view)是牢,導(dǎo)航抽屜使用了20個(gè)。這樣我們還剩下18個(gè)view沒有計(jì)算在內(nèi)陕截。剩下的這些view其實(shí)是在滑動(dòng)手勢(shì)動(dòng)畫過程當(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來measure,0.077ms來layout驯妄,2.737ms來draw荷并。第五層是通過第四層的兩個(gè)RelativeLayouts來連接的(藍(lán)色高亮)合砂,這些又是通過第三層的另一個(gè)RelativeLayout來連接的(綠色高亮)青扔。如果我們把第四第五層的view都移到第三層來,我們可以少渲染一整層翩伪。而且我之前解釋過微猖,RelativeLayout里的measure都會(huì)發(fā)生兩次,套嵌的view會(huì)導(dǎo)致measure時(shí)間的增加缘屹。
現(xiàn)在凛剥,你可能已經(jīng)注意到了每個(gè)view里紅色,黃色和綠色的圓圈轻姿。它們表示該view在那一層樹形結(jié)構(gòu)里measure犁珠,layout和draw所花費(fèi)的相對(duì)時(shí)間(從左到右)。綠色表示最快的前50%互亮,黃色表示最慢的前50%犁享,紅色表示那一層里面最慢的view。顯然豹休,紅色的部分是我們優(yōu)先優(yōu)化的對(duì)象炊昆。
再看下文章標(biāo)題的樹形結(jié)構(gòu),繪制最慢的view是右上角的ImageView。順著ImageView一直找到文章父view凤巨,父view是通過兩個(gè)RelativeLayouts來連接的(這里增加了measure的時(shí)間)视乐,然后是3個(gè)沒有子節(jié)點(diǎn)的view(在最底部)。這3個(gè)view可以優(yōu)化合并成一個(gè)view敢茁,這樣能減少兩個(gè)layer的渲染佑淀。
我們?cè)倏戳硪粋€(gè)新聞?lì)恆pp是怎么來減少標(biāo)題view里面的子view數(shù)量的。從圖4-6里能看到一個(gè)和圖4-5類似的樹形結(jié)構(gòu)圖卷要。
圖4-6

圖4-6里的標(biāo)題view也有RelativeLayouts(綠色的部分)的問題渣聚,一共消耗了1.275ms的measure時(shí)間,layout用了0.066ms僧叉,draw 3.24ms(總共是4.6ms)奕枝。在這些數(shù)據(jù)基礎(chǔ)上,我們?cè)僮鲆恍┱{(diào)整瓶堕,加入一個(gè)更大的圖片展示和分享按鈕隘道,但是整個(gè)樹形結(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的渲染性能了。我們分幾步來進(jìn)行優(yōu)化任连,每一步改變都可以通過Hierarchy View可視化的查看蚤吹。每換一種layout方式,xml渲染的性能要么變好随抠,要么變差裁着。我們先從性能差的布局方式開始。先快速的掃一眼圖4-8里的Hierarchy View拱她。
圖4-8

這個(gè)簡(jiǎn)單的app里有59個(gè)view二驰。但是和圖4-4里的app不同,這個(gè)app的樹形結(jié)構(gòu)更扁平椭懊,水平方向的view更多一些诸蚕。疊加的view越多步势,渲染就會(huì)越費(fèi)時(shí),減少view樹形結(jié)構(gòu)的深度背犯,app每一幀的渲染就會(huì)變快坏瘩。
藍(lán)色方框里面的view是action bar。橘色方框里的是屏幕頂部的text box漠魏,紫色方框里展示的是山羊的詳細(xì)信息(有6個(gè)這種view)倔矾。紅色方框標(biāo)示了7個(gè)view,每個(gè)都增加了樹形結(jié)構(gòu)的深度柱锹。我們仔細(xì)看些這7個(gè)view其中三個(gè)的remeasure數(shù)據(jù)(圖4-9)哪自。
圖4-9
4-9
4-9

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

我們可以繼續(xù)看下山羊信息到row展示部分,來繼續(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唯一的作用就是加深了樹機(jī)構(gòu)的深度镐侯。LinearLayout連接了RelativeLayout侦讨,但并沒有展示其他什么內(nèi)容。
圖4-11

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

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


每一行減少大約1ms的時(shí)間汗洒,我們一共可以節(jié)省6ms的渲染時(shí)間。如果你的app有卡頓父款,或者你通過工具檢測(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只是用來承載子view的玫坛,那這個(gè)view只不過是增加了view結(jié)構(gòu)的深度,這種情況下包晰,我們可以移除這個(gè)view湿镀,用一個(gè)merge標(biāo)簽來代替。這種方式可以移除樹形結(jié)構(gòu)里多余的層伐憾。
大家可以從github上下載這個(gè)山羊app練習(xí)下勉痴,改變里面xml文件的布局方式,再用Hierarchy View工具看下渲染時(shí)間的變化树肃。
Hierarchy Viewer(不止是樹形結(jié)構(gòu)圖)
Hierarchy Viewer還有一個(gè)功能蒸矛,可以幫助開發(fā)者發(fā)現(xiàn)overdraw(重復(fù)的繪制)。從左到右看下樹形結(jié)構(gòu)窗口的選項(xiàng)胸嘴,可以發(fā)現(xiàn)這些功能:
把view的樹形結(jié)構(gòu)圖保存為png圖片雏掠。
導(dǎo)出為photoshop的格式。
重新加載一個(gè)view(第二個(gè)紫色樹形按鈕)劣像。
在另一個(gè)窗口里打開較大的view結(jié)構(gòu)圖乡话,還可以設(shè)置背景色來發(fā)現(xiàn)重復(fù)繪制。
讓一個(gè)view的繪制失效(有條紅線的按鈕)耳奕。
讓view重新layout绑青。
讓view生出draw命令到logcat(紫色樹形按鈕到第三個(gè)用處)诬像。這樣可以查看繪制到底觸發(fā)了哪些opengl行為。這個(gè)功能對(duì)opengl的專家做深度優(yōu)化比較有用闸婴。

Hierarchy Viewer對(duì)于優(yōu)化app view的樹形結(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è)備而異)。主要是通過資源上色的方式來進(jìn)行縮減姐刁。比如只加載一個(gè)資源芥牌,然后在運(yùn)行的時(shí)候通過ColorFilter進(jìn)行上色。我們看下下面的例子是怎么個(gè)一個(gè)drawable上色的聂使。


這樣一個(gè)資源文件就可以表示幾種不同的狀態(tài)了(加星或者不加星壁拉,在線或者離線等等)。
屏幕的重復(fù)繪制
每過幾年柏靶,就會(huì)有傳聞?wù)f某個(gè)博物館在用x光掃描一副無價(jià)的名畫之后弃理,發(fā)現(xiàn)畫作的作者其實(shí)重用了老的畫布,在名畫的底下還藏著另一副沒有被發(fā)現(xiàn)的畫作屎蜓。有時(shí)候痘昌,博物館還能用高級(jí)的圖像技術(shù)來還原畫布上的原作。android里面的view的繪制就是類似的情況炬转。當(dāng)android系統(tǒng)繪制屏幕的時(shí)候辆苔,先畫父view,然后子view扼劈,再是更深的子view等等驻啤。這會(huì)導(dǎo)致所有的view都被繪制到了屏幕上,就像畫家的畫布一樣荐吵,這些view都被他們的子view覆蓋住了骑冗。
文藝復(fù)興時(shí)期,有很多偉大的畫家要等畫干了以后才能重用畫布捍靠。但在我們的高科技觸摸屏上沐旨,屏幕重畫的速度要快幾個(gè)數(shù)量級(jí)森逮,但是多次的重新繪制屏幕會(huì)使得繪制延遲變大榨婆,最終導(dǎo)致布局的卡頓。重新繪制屏幕的行為叫做overdraw褒侧,下面我們會(huì)看下怎么檢測(cè)overdraw良风。
overdraw還帶來的另一個(gè)問題谊迄,當(dāng)view內(nèi)容有更新的時(shí)候,之前繪制的view就失效了烟央,view的每一個(gè)像素都需要重繪统诺。android設(shè)備沒法判斷哪個(gè)view是可見的,所以只能繪制每個(gè)view的相關(guān)像素疑俭。類比上面畫家的例子粮呢,畫家只能把老畫一幅幅還原出來,再一層層畫到畫布上钞艇,最后再畫上最新的畫啄寡。你的app如果有很多層,每一層的相關(guān)像素都需要繪制一遍哩照。如果不小心挺物,這些繪制就會(huì)帶來性能問題。
檢測(cè)overdraw
android提供了一些很好的工具來檢測(cè)overdraw飘弧。Jelly Bean 4.2里识藤,開發(fā)者選項(xiàng)菜單里增加了Debug GPU Overdraw的選項(xiàng)。如果你用的是Jelly Bean 4.3 或者 KitKat 設(shè)備次伶,在屏幕的左下角會(huì)有一個(gè)計(jì)數(shù)展示屏幕overdraw的程度痴昧。我親身試過這個(gè)工具對(duì)檢測(cè)overdraw十分有效。雖然有時(shí)候這個(gè)會(huì)多提示6-7次overdraw(發(fā)生的概率還不泄谕酢)剪个。
圖4-14中的截圖還是來自上面的山羊app。左下方可以看到overdraw的計(jì)數(shù)版确。屏幕中可以看到3個(gè)overdraw的計(jì)數(shù)扣囊,其中開發(fā)者能控制的是主窗口的計(jì)數(shù)。overdraw的計(jì)數(shù)是在左下方绒疗。沒優(yōu)化過的app overdraw的次數(shù)是8.43侵歇,我們優(yōu)化過后可以降到1.38。導(dǎo)航欄overdraw的次數(shù)是1.2(菜單按鈕是2.4)吓蘑,也就是說文字和圖標(biāo)的overdraw貢獻(xiàn)了額外的20%惕虑。overdraw計(jì)數(shù)可以在不影響用戶體驗(yàn)的前提下,快速便捷的比較不同app的overdraw磨镶,但沒辦法定位overdraw是哪里產(chǎn)生的溃蔫。
圖4-14

另一種查看overdraw的方式是在Debug GPU overdraw菜單里選擇“Show Overdraw areas”選項(xiàng)。選擇之后琳猫,會(huì)在app的不同區(qū)域覆蓋不同的顏色來表示overdraw的次數(shù)雁比。比較屏幕上這些不同的顏色叛买,可以快速方便的定位overdraw問題:
白色:沒有overdraw 藍(lán)色:1x overdraw(屏幕繪制了2次) 綠色:2x overdraw 淺紅色:3x overdraw 深紅色:4x或者更多overdraw
在圖4-15中,可以看到山羊app優(yōu)化前后overdraw區(qū)域的變化析命。app的菜單欄優(yōu)化前后都沒有顏色(沒有overdraw),但android圖標(biāo)和菜單按鈕圖標(biāo)都是綠色的(2x overdraw)。山羊圖片等列表在優(yōu)化之前是深紅色的(4x以上的overdraw)。優(yōu)化app 之后,只有checkbox和圖片區(qū)域是藍(lán)色(1x)的了暗膜,說明至少3層overdraw被消滅掉了!text和空白區(qū)域都沒有overdraw了鞭衩。
圖4-15

通過減少view的數(shù)量(或者去移除重復(fù)繪制的view)学搜,app的渲染會(huì)更快。通過比較父view在優(yōu)化前后的繪制時(shí)間论衍,可以發(fā)現(xiàn)優(yōu)化后帶來50%性能的提升恒水,由13.5ms降到6.8ms。
Hierarchy Viewer當(dāng)中的overdraw
另一種查看app當(dāng)中overdraw的方式是把Hierarchy Viewer中的view的樹形結(jié)構(gòu)保存成photoshop識(shí)別的文檔(樹形view里的第二個(gè)選項(xiàng))饲齐。如果你沒有安裝photoshop钉凌,有幾個(gè)其他的免費(fèi)軟件也可以打開這個(gè)文檔。打開文檔查看view捂人,可以清楚看到不同layer里的overdraw御雕。對(duì)于大部分的線上app,在一個(gè)白色背景上放上另一個(gè)白色背景很常見滥搭。聽起來還好酸纲,但這里其實(shí)有一次繪制是多余的,完全可以避免的瑟匆。我們?cè)倏聪律窖騛pp闽坡,所有overdraw圖片區(qū)域都放在了一張?bào)H子的背景圖片上(替換了之前的白色背景)。之前的驢子看不到愁溜,是因?yàn)楸话咨尘皥D擋住了疾嗅。移除掉之后就可以看到下面的驢子了,這樣我們就可以快速的定位哪里出現(xiàn)了overdraw冕象。用GIMP打開文檔之后代承,app里所有可見的view的左邊都有一個(gè)小眼睛圖標(biāo)。在圖4-16中渐扮,可以看到我從最上面開始把view一個(gè)個(gè)隱藏起來了论悴。在右邊的layout視圖中,可以看到一些其他的全屏layout(都顯示了驢子的圖片)墓律。
圖4-16

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

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)用到上面的例子匕坯,也就是說驢子那張大背景圖就不會(huì)去繪制了束昵。這很明顯會(huì)極大的提高設(shè)備的繪制性能。但開發(fā)者還是要盡可能的避免額外的overdraw(為了更好的性能葛峻,也為了能兼容Jelly Bean及更老的設(shè)備)锹雏。
Overdraw Avoidance和相關(guān)開發(fā)者工具
當(dāng)用上面提到的overdraw檢測(cè)工具時(shí),KitKat的overdraw avoidance功能會(huì)被禁止术奖,這只是為了方便你查看view的布局礁遵,和在設(shè)備上真正運(yùn)行的情況并不一樣。
分析卡頓(測(cè)量GPU的渲染性能)
在我們優(yōu)化過view的樹形結(jié)構(gòu)和overdraw之后采记,你可能還是感覺自己的app有卡頓和丟幀佣耐,或者滑動(dòng)慢:卡頓還是存在∵罅洌可能高端機(jī)器上感覺不到卡頓兼砖,但低端機(jī)上還是可能會(huì)出現(xiàn)卡頓。為了能獲取更全面的卡頓檢測(cè)信息既棺,android在Jelly Bean及更新的系統(tǒng)里加入了一個(gè)GPU繪制開發(fā)者選項(xiàng)讽挟。能夠測(cè)出每一幀的繪制用了多少時(shí)間。你可以把測(cè)量出來的數(shù)據(jù)保存到一個(gè)logfile(adb shell dumpsys gfxinfo)丸冕,或者在設(shè)備的屏幕上實(shí)時(shí)查看這些信息(只支持android 4.2+)戏挡。
我們快速來看下怎么分析,我比較喜歡在屏幕上直接展示GPU的渲染數(shù)據(jù)晨仑,這樣感覺更直觀全面(logfile里面的數(shù)據(jù)很適合離線的詳細(xì)分析)褐墅。我們最好在不同的設(shè)備上都試一下。圖4-18展示的是Nexus 6運(yùn)行Lollipop(左邊)和Moto G運(yùn)行 KitKat(右邊)同時(shí)跑山羊app的GPU渲染數(shù)據(jù)洪己。重點(diǎn)看下GPU測(cè)量圖表底部的水平綠條妥凳。它是設(shè)備16ms繪制一幀的分割線,如果你有很多幀都超過了這條綠線答捕,那就表示有卡頓了逝钥。在下圖里可以看到Nexus6上有偶爾的卡頓。出現(xiàn)在滑動(dòng)到頁面底部的時(shí)候拱镐,播放里一個(gè)反彈的動(dòng)畫艘款。用戶體驗(yàn)不算太糟持际。每一次屏幕繪制(豎線)被分成四種顏色來表示額外的測(cè)量數(shù)據(jù):draw(藍(lán)色),prepare(紫色)哗咆,process(紅色)蜘欲,執(zhí)行(黃色)。在KitKat和更早的版本里晌柬,prepare的數(shù)據(jù)沒有獨(dú)立出來姥份,包含在其他項(xiàng)里面(因此只有看到3種顏色)。
圖4-18

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

一般來說,GPU Profiler可以幫你發(fā)現(xiàn)問題幕与。在山羊app里挑势,如果我打開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等等盛险。程序里一般用來表示遞歸,這里我用了最低效的方式來生成Fibonacci序列。

生成這些數(shù)字的計(jì)算次數(shù)呈指數(shù)級(jí)增長(zhǎng)苦掘。這樣做的目的是在渲染的時(shí)候增加CPU的壓力换帜,這樣渲染事件就無法得到及時(shí)處理,出現(xiàn)延遲鹤啡。計(jì)算n=40就把a(bǔ)pp變得很慢了(低端機(jī)上會(huì)crash)惯驼。這個(gè)例子雖然有點(diǎn)牽強(qiáng),但我們定位卡頓是由Fibonacci產(chǎn)生的過程會(huì)很有意義揉忘。
Android Marshmallow里的GPU渲染
在android marshmallow里跳座,運(yùn)行adb shell dumpsys gfxinfo . 可以發(fā)現(xiàn)一些檢測(cè)卡頓的新功能端铛。首先泣矛,數(shù)據(jù)報(bào)告開頭部分能看到每一幀渲染的信息了。


從app的啟動(dòng)開始禾蚕,我們可以看到一共渲染了多少幀您朽,其中多少幀的渲染時(shí)間是控制在理想值的90%以內(nèi),還能看到渲染比較慢的幀(90%换淆,95%哗总,99%)。最后五行列出的是沒有在16ms內(nèi)渲染完成的原因倍试。注意讯屈,這里不止有卡頓的問題,幀率還收到了其他因素的影響县习。
android marshmallow在gfxinfo庫里增加了另一個(gè)好用的測(cè)試工具涮母,adb shell dumpsys gfxinfo framestats。它能夠輸出每一幀里發(fā)生的某些事件耗時(shí)躁愿,格式是逗號(hào)分隔的一張大表叛本。列名沒有給出,但在Android Developer網(wǎng)站里有解釋彤钟。為了算出渲染里每一步的費(fèi)時(shí)来候,我們要計(jì)算出報(bào)告里不同framestats的差異。下面是一些繪制事件:

VSYNC-Intended_VSYC(告訴你是否丟幀里逸雹,也就是卡頓)
處理輸入事件的時(shí)間(一般要小于2ms)
動(dòng)畫計(jì)算(一般小于2ms)
layout和measure
view.draw()耗時(shí)
Sync耗時(shí)(如果大于0.4ms营搅,表示很多bitmap正在發(fā)送到GPU)
GPU耗時(shí)(overdraw的時(shí)間會(huì)在這里面)
繪制一幀的總時(shí)間

有時(shí)候即使出現(xiàn)了超過16ms的繪制,但由于有vsync buffer的存在梆砸,也不會(huì)出現(xiàn)丟幀转质。對(duì)于沒有額外buffer的低端設(shè)備,就可能會(huì)出現(xiàn)卡頓了辫樱。
不只是卡頓(丟幀)
有時(shí)候GPU Profile里看不到超過16ms的數(shù)據(jù)峭拘,但你從屏幕上看到明顯的卡頓或跳動(dòng)。出現(xiàn)這種情況可能是由于CPU在做別的事情被堵住了,從而導(dǎo)致里丟幀鸡挠。在Monitor或者Android Studio中辉饱,可以查看DDMS里的logfiles。通過過濾log更容易查看app的運(yùn)行情況拣展∨碚樱可以重點(diǎn)看下類似下圖中的log。


我們?cè)诤竺娴奈恼吕飼?huì)講訴CPU導(dǎo)致的丟幀是怎么產(chǎn)生的备埃。
Systrace
在上面的這些優(yōu)化之后姓惑,如果你的界面還有卡頓,我們還有辦法按脚。Systrace工具也可以測(cè)量你app的性能于毙。甚至可以幫助你定位問題產(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è)卡頓問題介蛉。
Systrace和之前的工具不同的是,它記錄的是整個(gè)android系統(tǒng)的狀態(tài)溶褪,并不是針對(duì)某一個(gè)app 的币旧。所以最好是用運(yùn)行app比較少的設(shè)備來做檢測(cè),這樣就不會(huì)受到其他app的干擾了猿妈。Systrace圖標(biāo)是綠色和粉紅色組成的(下圖紅色的橢圓里)吹菱。點(diǎn)擊下,會(huì)彈出一個(gè)帶幾個(gè)選項(xiàng)的窗口于游。
圖4-22

trace數(shù)據(jù)記錄在一個(gè)html文件里毁葱,可以用瀏覽器打開。這里主要研究屏幕的交互數(shù)據(jù)贰剥,主要收集CPU倾剿,graphics和view數(shù)據(jù)(如圖4-22所示)。duration留空(默認(rèn)是5秒)蚌成。點(diǎn)擊OK之后前痘,Systrace會(huì)馬上開始采集設(shè)備上的數(shù)據(jù)(最好馬上開始操作)。因?yàn)椴杉臄?shù)據(jù)非常之多担忧,所以最好一次只針對(duì)一個(gè)問題芹缔。
traces里面的數(shù)據(jù)看著有點(diǎn)嚇人(我們只是勾選里4個(gè)選項(xiàng)!)瓶盛。鼠標(biāo)可以控制滑動(dòng)最欠,WASD可以用來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-23

Systrace進(jìn)化史
就像android生態(tài)圈一樣左刽,Systrace在不同的系統(tǒng)版本里有不同的界面,展示摄乒,和輸出結(jié)果悠反。
在Jelly Bean設(shè)備残黑,在設(shè)置的開發(fā)者選項(xiàng)里可以打開tracing馍佑。必須要同時(shí)打開電腦和手機(jī)上的該功能。
隨著android系統(tǒng)版本的升級(jí)梨水,trace生成的數(shù)據(jù)也更加詳細(xì)拭荤,布局也有一些改變。
我建議通過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í)可能有問題的繪制网持。只要刷新率和繪制都正常,屏幕的渲染應(yīng)該就是流暢的长踊。但只要一個(gè)出問題功舀,就有可能會(huì)導(dǎo)致頁面渲染的卡頓。
Systrace Screen Painting
我們通過圖4-24來看下屏幕繪制的步驟身弊。最頂部一行的trace(藍(lán)色高亮)時(shí)VSYNC辟汰,由一些均勻分布的藍(lán)綠色寬條組成列敲。VSYNC是操作系統(tǒng)發(fā)來的信號(hào),表示此時(shí)該刷新屏幕了帖汞。每個(gè)寬條表示16ms(寬條之間的空白也是16ms)酿炸。當(dāng)VSYNC事件發(fā)生的時(shí)候(在藍(lán)綠色寬條的任意一側(cè)),surface flinger(紅色高亮方框包含幾種顏色的長(zhǎng)條)會(huì)從view buffer(沒展示出來)里選一個(gè)view涨冀,然后繪制到屏幕上填硕。理想情況下,surfaceflinger事件之間相距16ms(沒有卡頓)鹿鳖,因此如果出現(xiàn)長(zhǎng)條空缺則表示surfaceflinger丟掉了一次VSYNC更新事件扁眯,屏幕就沒有及時(shí)的刷新(此時(shí)就會(huì)有卡頓)。在trace文件2/3的位置可以看到這樣的空缺(綠色高亮方框)翅帜。
圖4-24


圖4-24底部展示的是app的詳情姻檀。第二行數(shù)據(jù)(綠色和紫色的線條)表示的app正在創(chuàng)建view,然后是底部的數(shù)據(jù)(綠色涝滴,藍(lán)色绣版,和一些紫色的條狀),表示的是RenderThread歼疮,view的渲染和發(fā)送到buffer(圖中沒有畫出來)都是在這個(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沒有卡頓的時(shí)候Systrace輸出又是什么樣的空民。一旦弄明白了Systrace是怎么工作的刃唐,查找問題就方便多了。在圖4-25中袭景,我把a(bǔ)pp正常運(yùn)行時(shí)Systrace紀(jì)錄的相關(guān)線條放到了一起唁桩。我們從屏幕左邊的droid.yahoo.com看起。我描述的時(shí)候在trace文件里會(huì)來回跳動(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ù)量。剛開始米奸,只有一個(gè)昼接,當(dāng)新的view加入到buffer中之后,高度就變成了2倍悴晰。
綠色方框:VSYNC-sf 提示surface flinger有16ms的時(shí)間來渲染屏幕慢睡。里面棕色的條狀表示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一開始,droid.yahoo.att就不停的重復(fù)這個(gè)過程哈扮,measure view,發(fā)送給RenderThread等等纬纪,不停的循環(huán)。

圖4-25


再回過頭想一下設(shè)備能這么短的時(shí)間內(nèi)流暢的渲染屏幕滑肉,確實(shí)是件很神奇的事情包各。了解了渲染的過程,我們來找下卡頓的原因赦邻。
圖4-26中髓棋,我們看下OS層的行為。我增加了一些箭頭來表示16ms的間隔惶洲,紅色的方框表示surfaceflinger的丟幀。
圖4-26

為什么會(huì)出現(xiàn)這種情況膳犹?箭頭上方的一行是view buffer恬吕,行的高度表示有多少幀緩存在了buffer里面。trace開始的時(shí)候须床,buffer里緩存的數(shù)量是1到2交替出現(xiàn)铐料。surfaceflinger每抓取一個(gè)view(buffer里的數(shù)量減一),又會(huì)馬上從app里生成一個(gè)新的view來填充豺旬。但是當(dāng)surfaceflinger完成第三個(gè)動(dòng)作之后钠惩,buffer被清空了,但是沒有從app里及時(shí)填充新的view族阅。所以篓跛,我們從app層面來檢查下這期間發(fā)生了什么。
在圖4-27中坦刀,我們可以看到開始的時(shí)候RenderThread發(fā)送了一個(gè)view到buffer(紅色方框)愧沟。橘色方框表示app新建了另一個(gè)view蔬咬,渲染,然后交給buffer(droid.yahoo.att measure,layout所有的view沐寺,RenderThread負(fù)責(zé)繪制)林艘。不幸的是,app沒來得及創(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)建這一幀開始的時(shí)候,buffer里只有一幀的數(shù)據(jù)泳猬,但是設(shè)備需要兩幀批钠。buffer沒有被填滿,所以屏幕繪制出現(xiàn)了卡頓得封。
圖4-27

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

從總覽圖上可以清晰的看到有很多幀都丟掉了(trace開始的時(shí)候surfacelinger部分有很多的空缺)梗逮。而且頂部那一行(view buffer)里的buffer經(jīng)常是空的(導(dǎo)致里卡頓)项秉,buffer里同時(shí)有兩個(gè)view的情況非常少。對(duì)于一個(gè)GPU性能比較差的設(shè)備來說慷彤,app能夠像Nexus 6一樣趕上填滿buffer的概率比較小娄蔼。
小貼示: 其實(shí)你可以偶爾渲染一幀超過16ms,因?yàn)閎uffer里面一般都有1到2幀準(zhǔn)備好的view備用底哗。但是如果超過2-3幀渲染很慢岁诉,用戶就會(huì)感覺到卡頓了。
上面的trace是在運(yùn)行Jelly Bean的手機(jī)上跑的跋选,RenderThread的數(shù)據(jù)歸到了droid.yahoo.att那一行(Lollipop之前measure,draw,layout都是和在一起的)涕癣。把每一行數(shù)據(jù)合在一起之后豎條變寬。每一次調(diào)用之間的細(xì)條空白說明手機(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-29中菱农,可以看到performtraversals(父view的draw命令)平均用了13.8ms,大概有5ms的波動(dòng)柿估。16ms的卡頓閾值在波動(dòng)的范圍之內(nèi)循未,所以很有可能設(shè)備上會(huì)有卡頓。
圖4-29

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

那到底是什么觸發(fā)了deliverInputEvents呢?這其實(shí)是用戶在點(diǎn)擊屏幕镊讼,強(qiáng)制ListView重繪所有的view宽涌。這部分影響是CPU,我們接下來簡(jiǎn)單看下這時(shí)候CPU都在干啥狠毯。
Systrace和CPU對(duì)渲染的影響
如果你頻繁的感覺到卡頓护糖,但是在繪制或者surfaceflinger部分看不到什么明顯的異常,這時(shí)候可以嘗試看下CPU在處理什么事情嚼松,在Systrace的頂部可以看到這部分的數(shù)據(jù)。如果你能大概猜到是哪部分的邏輯影響了繪制锰扶,可以先把這部分代碼注釋掉試試献酗。山羊app里有個(gè)選項(xiàng)可以開啟Fibonacci延遲。打開之后坷牛,app在每一行數(shù)據(jù)渲染的時(shí)候都會(huì)計(jì)算一個(gè)很大的Fibonacci值罕偎。用膝蓋想都知道這時(shí)CPU會(huì)變得很忙。由于計(jì)算是在主線程做的京闰,會(huì)妨礙的view的渲染颜及,理所當(dāng)然就導(dǎo)致里丟幀甩苛,滑動(dòng)也會(huì)變的很卡。圖4-21里顯示的log就能看到這種情況下的丟幀俏站。我們?cè)偕钔谝稽c(diǎn)看能不能通過Systrace定位到計(jì)算Fibonacci數(shù)的代碼讯蒲。
我們?cè)僦仡^看下trace數(shù)據(jù),圖4-31里是沒有優(yōu)化過的山羊app在Nexus 6上跑的數(shù)據(jù)肄扎。
圖4-31

展示做了一些修改墨林,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è)行為都沒有超過2-4ms而姐。
當(dāng)我們加入費(fèi)時(shí)的Fibonacci計(jì)算之后腊凶,Systrace的結(jié)果看起來就很不一樣了。(圖4-32)
圖4-32

從Systrace里能看到很多卡頓拴念,在相同的100ms時(shí)間范圍內(nèi)钧萍,surfaceflinger就畫了三幀(上面不卡頓的情況畫了7幀)≌螅可以看到RenderThread繪制view還是很快的(從圖中可以看出风瘦,藍(lán)色的RenderThread是在CPU0上運(yùn)行的)。但是公般,measure view的時(shí)候万搔,F(xiàn)ibonacci的遞歸計(jì)算就導(dǎo)致了問題。山羊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里,我把每一幀的更新都高亮出來了哪廓。在新版本的systrace(圖4-33)里狗唉,每一幀都是由一個(gè)帶F的小圓圈標(biāo)示的。正常渲染的幀會(huì)有綠點(diǎn)撩独,慢幀則是黃色或者紅色敞曹。選擇一個(gè)點(diǎn),然后按下m就可以高亮某一幀综膀,分析起來更方便澳迫。
圖4-33

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

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

這些新功能讓Systrace診斷UI問題更加簡(jiǎn)單了染突。
第三方工具
每個(gè)大的芯片廠商都有自己的GPU評(píng)測(cè)工具捻爷,可以幫助發(fā)現(xiàn)更多渲染時(shí)遇到瓶頸的信息。這些工具對(duì)一些特定的芯片更有針對(duì)性份企,信息也更多也榄。可以幫你針對(duì)不同的GPU做更深度的優(yōu)化司志。Qualcomm甜紫,NVIDIA和Intel都提供了這些開發(fā)者工具,有興趣的可以自己試下骂远。
感知優(yōu)化
上面的內(nèi)容都是在討論怎么通過測(cè)試棵介,調(diào)試,優(yōu)化布局來讓UI的體驗(yàn)更快吧史。其實(shí)還有另外一個(gè)辦法讓你的app UI更快:讓用戶感覺更快冰蘑。當(dāng)然作為開發(fā)者要盡可能優(yōu)化自己的代碼,view永乌,overdraw和其它所有可能會(huì)影響渲染性能的地方橙弱,上面這些都做了之后,再考慮下面這些能讓用戶覺得你的app更快的方法钞脂。
人類大腦工作的方式很有意思揣云,通過改變大腦對(duì)等待的感知,可以讓你的用戶感覺延遲變短了冰啃。雜貨店的老板都會(huì)在走廊上放一些沒用的雜志邓夕,就是為了讓客戶有東西可以看,感覺等待的時(shí)間就會(huì)短一些阎毅。如果在向用戶展示內(nèi)容的時(shí)候增加一些過渡效果焚刚,見效明顯。這就像一個(gè)小魔術(shù)一樣讓用戶感覺體驗(yàn)變的更快了扇调,歸根結(jié)底重要的是用戶覺得你的app有多快矿咕。這個(gè)技巧實(shí)現(xiàn)起來也有點(diǎn)取巧,有時(shí)候這種感知的優(yōu)化甚至?xí)玫较喾吹男Ч桥ィ鯝/B test來確保你的優(yōu)化對(duì)用戶來說真的有效碳柱。
loading菊花:優(yōu)缺點(diǎn)
loading菊花,進(jìn)度條熬芜,沙漏圖標(biāo)莲镣,和其它所有表示等待的方式都存在很久了。這些都可以讓app的內(nèi)容過渡變得更快涎拉。比如在app里加一個(gè)進(jìn)度條瑞侮,加載的時(shí)候播放一個(gè)進(jìn)度的動(dòng)畫來讓用戶等待。研究表明使用一個(gè)帶有動(dòng)畫的滑動(dòng)條的時(shí)候用戶會(huì)感覺更舒服曼库∏冢快速旋轉(zhuǎn)的loading菊花也讓用戶感覺等待的時(shí)間更短。
但是毁枯,有延遲的時(shí)候慈缔,加個(gè)菊花并不總是有效的。iOS app Polar的開發(fā)者發(fā)現(xiàn)他們的app渲染一個(gè)view的時(shí)候有一點(diǎn)延遲种玛。他們第一反應(yīng)是在頁面里加了一個(gè)菊花告訴用戶頁面正在渲染內(nèi)容藐鹤,但效果不如預(yù)期。用戶開始反饋app變慢了赂韵,等待頁面加載的時(shí)間變長(zhǎng)了(其實(shí)app沒有變慢娱节,不過是加了一個(gè)菊花)。加了個(gè)等待的標(biāo)識(shí)之后讓用戶明顯的感覺到他們?cè)诘燃朗尽H∠栈ㄖ笠蘼脩舾杏Xapp又變快了(開發(fā)者僅僅是改變了菊花)。通過改變用戶對(duì)等待的感知,可以讓用戶覺得app變快了稠歉。Facebook也遇到過類似的問題:使用自己定制的菊花讓用戶感覺更慢掰担,用默認(rèn)菊花感覺更快。
增加菊花最好讓用戶測(cè)試下他們的真實(shí)感受怒炸。一般來說带饱,當(dāng)?shù)却臅r(shí)間稍微有點(diǎn)長(zhǎng)的時(shí)候,增加菊花是可以接受的:比如打開一個(gè)新頁面或者從網(wǎng)上下載一張圖片阅羹。如果延遲很短(一般來說小于一秒)勺疼,就應(yīng)該考慮去掉菊花了。這種情況下應(yīng)該讓用戶覺得他們并沒有在等捏鱼。
用動(dòng)畫來抵消等待的時(shí)間
點(diǎn)擊后看到一個(gè)空白的屏幕會(huì)讓用戶感覺在等待执庐。就是這個(gè)原因讓瀏覽器在點(diǎn)擊鏈接,新頁面刷出來之前都是展示舊的頁面穷躁。在手機(jī)app里耕肩,一般來說我們不希望讓用戶停留在老的頁面上,一個(gè)快速的切換動(dòng)畫可以爭(zhēng)取到足夠的時(shí)間讓下一個(gè)頁面準(zhǔn)備就緒问潭≡持睿可以觀察下你最常用的android app,當(dāng)頁面切換的時(shí)候有多少從邊上或者底部出現(xiàn)的動(dòng)畫狡忙。
瞬時(shí)更新的小謊言
如果你的用戶在頁面上做了更新數(shù)據(jù)的操作梳虽,即使數(shù)據(jù)還沒抵達(dá)服務(wù)器,可以馬上把用戶看到的數(shù)據(jù)更新掉(當(dāng)然開發(fā)者要保證這些數(shù)據(jù)能100%抵達(dá)服務(wù)器)灾茁。比如說窜觉,你在Instagram上點(diǎn)了贊,頁面上馬上就更新了贊的狀態(tài)北专,其實(shí)贊的狀態(tài)甚至可能還沒有更新到服務(wù)器禀挫。Instangram的開發(fā)者管這叫“行為最優(yōu)化”,狀態(tài)的更新要幾秒后才能到服務(wù)器并對(duì)網(wǎng)站的用戶可見(網(wǎng)速不好的時(shí)候可能要幾分鐘)拓颓,但是更新最后都會(huì)成功语婴,等待服務(wù)器返回成功其實(shí)是沒必要的。移動(dòng)端用戶一般都不希望在等待驶睦,只要最后能成功就好砰左。
瞬時(shí)更新的另一個(gè)好處是,用戶會(huì)感覺你的app在網(wǎng)速或者信號(hào)不好(火車經(jīng)過隧道)的時(shí)候也能正常工作场航。FlipBoard就做過一個(gè)離線發(fā)送網(wǎng)絡(luò)請(qǐng)求的框架缠导,可以很方便的應(yīng)用到更新UI。
另一個(gè)優(yōu)化的小技巧是提前上傳溉痢。對(duì)于像Instagram這種app來說僻造,上傳大量的圖片會(huì)增加主線程的延遲憋他,提前開始上傳這些圖片會(huì)是個(gè)好辦法。Instagram發(fā)現(xiàn)發(fā)一個(gè)新post是慢在上傳圖片這一步嫡意,所以Instagram就在用戶在圖片上添加文字的間隙開始上傳圖片了举瑰,圖片被真正發(fā)布到服務(wù)器之前就已經(jīng)傳好了。用戶只要一點(diǎn)擊Post按鈕蔬螟,就只需要上傳文本和創(chuàng)建post的命令了,這樣就會(huì)讓用戶感知非称耄快旧巾。Instagram在遇到“是否要添加菊花”這個(gè)問題時(shí),他們的答案是通過改變架構(gòu)的方式永遠(yuǎn)的杜絕菊花忍些。
提升感知體驗(yàn)的小提示
當(dāng)app的速度通過優(yōu)化代碼或者view的優(yōu)化提升之后,你可以用秒表來測(cè)試下結(jié)果罢坝。有些感知是可以用秒表測(cè)量的(Instagram的例子),有些則不能(菊花的例子)嘁酿。當(dāng)常規(guī)的分析或者測(cè)量工具不可靠的時(shí)候,需要讓用戶來真正的體驗(yàn)這些優(yōu)化效果闹司。可以做一些可用性測(cè)試牲迫,增加測(cè)試的范圍借卧,A/B測(cè)試,這些才能真正的讓你確認(rèn)你的優(yōu)化是讓用戶更開心還是更沮喪铐刘。
總結(jié)
Android app的用戶體驗(yàn)直接跟屏幕上展現(xiàn)的內(nèi)容相關(guān)陪每。如果app的內(nèi)容加載很慢或者滑動(dòng)不夠流暢,用戶的感知就是負(fù)面的滨达。在這篇文章奶稠,我們講了如何優(yōu)化view樹形結(jié)構(gòu),看是否扁平或者簡(jiǎn)化view等等捡遍。我們還講了怎么檢測(cè)解決overdraw的問題锌订。還有一些需要深度分析的優(yōu)化(像CPU導(dǎo)致的問題),systrace很適合發(fā)現(xiàn)和解決這種卡頓問題画株。最后是一些讓你的app感覺更快的小技巧辆飘,比如把CPU或者網(wǎng)絡(luò)相關(guān)的任務(wù)延后處理啦辐,不要影響繪制渲染。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蜈项,一起剝皮案震驚了整個(gè)濱河市芹关,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌紧卒,老刑警劉巖侥衬,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異跑芳,居然都是意外死亡轴总,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門博个,熙熙樓的掌柜王于貴愁眉苦臉地迎上來怀樟,“玉大人,你說我怎么就攤上這事盆佣。” “怎么了共耍?”我有些...
    開封第一講書人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵瘩缆,是天一觀的道長(zhǎng)庸娱。 經(jīng)常有香客問我熟尉,道長(zhǎng)斤儿,這世上最難降的妖魔是什么往果? 我笑而不...
    開封第一講書人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任陕贮,我火速辦了婚禮肮之,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘眶明。我一直安慰自己搜囱,他們只是感情好犬辰,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開白布灸促。 她就那樣靜靜地躺著浴栽,像睡著了一般典鸡。 火紅的嫁衣襯著肌膚如雪萝玷。 梳的紋絲不亂的頭發(fā)上球碉,一...
    開封第一講書人閱讀 48,970評(píng)論 1 284
  • 那天睁冬,我揣著相機(jī)與錄音豆拨,去河邊找鬼施禾。 笑死拾积,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的一死。 我是一名探鬼主播投慈,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼伪煤,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼抱既!你這毒婦竟也來了防泵?” 一聲冷哼從身側(cè)響起捷泞,我...
    開封第一講書人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤锁右,失蹤者是張志新(化名)和其女友劉穎咏瑟,沒想到半個(gè)月后响蕴,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體浦夷,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡劈狐,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了续膳。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡谒兄,死狀恐怖承疲,靈堂內(nèi)的尸體忽然破棺而出燕鸽,到底是詐尸還是另有隱情啊研,我是刑警寧澤悲伶,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站舆声,受9級(jí)特大地震影響媳握,放射性物質(zhì)發(fā)生泄漏蛾找。R本人自食惡果不足惜打毛,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一幻枉、第九天 我趴在偏房一處隱蔽的房頂上張望熬甫。 院中可真熱鬧椿肩,春花似錦郑象、人聲如沸扣唱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽局义。三九已至萄唇,卻和暖如春另萤,著一層夾襖步出監(jiān)牢的瞬間四敞,已是汗流浹背忿危。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來泰國(guó)打工铺厨, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留努释,地道東北人伐蒂。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像在扰,于是被迫代替她去往敵國(guó)和親芒珠。 傳聞我的和親對(duì)象是個(gè)殘疾皇子皱卓,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

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