一、基本概念
Android系統(tǒng)每隔16ms發(fā)出VSYNC信號(hào)令漂,觸發(fā)對(duì)UI進(jìn)行渲染膝昆,如果每次渲染都成功,這樣就能夠達(dá)到流暢的畫面所需要的60fps叠必,這也意味著程序的大多數(shù)操作都必須在16ms內(nèi)完成荚孵。如果無法完成,則發(fā)生丟幀纬朝,上一幀畫面被重復(fù)顯示收叶,造成卡頓的視覺。
從整個(gè)視圖渲染流程看:
Surfaceflinger由init啟動(dòng)的獨(dú)立進(jìn)程共苛,提供合成視圖的系統(tǒng)服務(wù)判没。如果Surfaceflinger掛掉,會(huì)重啟zygote隅茎。
在Surfaceflinger的init方法中澄峰,實(shí)例化了HWComposer和兩個(gè)EventThread。
HWComposer
:負(fù)責(zé)輸出硬件產(chǎn)生或軟件模擬的Vsync信號(hào)辟犀。
EventThread
:負(fù)責(zé)分發(fā)vsync到Choreographer和SurfaceFlinger俏竞。其中mEventThread對(duì)應(yīng)Choreographer;而mSFEventThread:對(duì)應(yīng)SurfaceFlinger堂竟。
VSYNC信號(hào)主要的兩個(gè)訂閱者:SurfaceFlinger 和 Choreographer魂毁。
SurfaceFlinger
:接收信號(hào)執(zhí)行合成Layer流程。
Choreographer
:接收信號(hào)來控制同步處理輸入(Input)跃捣、動(dòng)畫(Animation)漱牵、繪制(Draw)三個(gè)UI操作夺蛇。
Choreographer通知應(yīng)用層繪制疚漆、SurfaceFlinger負(fù)責(zé)合成視圖、兩者之前加上了一定的offset,這樣能保證兩者步調(diào)一致娶聘。
在這個(gè)過程中闻镶,CPU負(fù)責(zé)把視圖加工為多邊形和紋理。GPU負(fù)責(zé)把多邊形和紋理做柵格化處理丸升,成為送顯的像素?cái)?shù)據(jù)铆农。
二、造成卡頓的原因
應(yīng)用層面:
1 視圖層面的問題
包括layout層級(jí)太深View太多狡耻、View太復(fù)雜墩剖、重復(fù)繪制、ListView沒優(yōu)化夷狰、動(dòng)畫設(shè)計(jì)不合理等等岭皂。
這是遇到卡頓問題首先需要排查的,部分問題可以通過開發(fā)階段的coding規(guī)范來避免的沼头。
1)layout層級(jí)太深View太多:可以通過Lint來檢測(cè)爷绘,優(yōu)化:通過合理容器的使用,優(yōu)先減少層級(jí)进倍,其次減少View數(shù)目土至,能重用的盡量重用。
2)View太復(fù)雜:如果是自定義View猾昆,那還是從視圖太深陶因、View太多兩個(gè)層面來考慮優(yōu)化。如果是成熟的View:比如WebView垂蜗、VideoView這種重量級(jí)的View坑赡,盡量復(fù)用和管理好生命周期。
3)重復(fù)繪制:通過Settings中打開GPU過度繪制 & GPU呈現(xiàn)模式可以了解當(dāng)前視圖層級(jí)關(guān)系么抗,當(dāng)然這部分與前面兩點(diǎn)也是分不開的毅否,最基本的要注意移除xml中非必須背景。
4)ListView優(yōu)化蝇刀,這部分主要是convertView的復(fù)用螟加,能減少View的創(chuàng)建;ViewHolder的使用吞琐,減少View的find和賦值捆探,加快加載速度;分頁加載:控制一次加載的數(shù)據(jù)量站粟,這樣加載速度會(huì)快黍图,內(nèi)存壓力也相對(duì)小。
5)動(dòng)畫:合理設(shè)計(jì)動(dòng)畫奴烙,能不用幀動(dòng)畫盡量不用助被,因?yàn)閳D片比較占內(nèi)存剖张,尤其是數(shù)量多的時(shí)候。另外針對(duì)屬性動(dòng)畫揩环,同一個(gè)view的一系列動(dòng)畫搔弄,可以使用Keyframe+PropertyValuesHolder組合方式達(dá)到只使用一個(gè)ObjectAnimator,多個(gè)view的動(dòng)畫用AnimatorSet進(jìn)行動(dòng)畫組合和排序丰滑。
2 消息相關(guān)耗時(shí)
我們都知道顾犹,耗時(shí)操作放到子線程做,通過handle返回主線程更新UI褒墨。但是消息本身也是會(huì)耗時(shí)的炫刷,主要分兩方面:1)消息本身執(zhí)行耗時(shí), 2)消息執(zhí)行被delay。消息本身執(zhí)行耗時(shí)那就是主線程耗時(shí)郁妈,消息執(zhí)行被delay柬唯,在messageQueue中,由于之前的Message太多或者執(zhí)行時(shí)間過長(zhǎng)圃庭,導(dǎo)致當(dāng)前需更新UI的操作得不到及時(shí)處理锄奢,尤其是16.6ms硬性標(biāo)準(zhǔn)下,一旦delay必然丟幀剧腻。
3 主線程耗時(shí)
這部分我要說的并不是在主線程做耗時(shí)操作了拘央,而是站在CPU調(diào)度的角度來看耗時(shí)問題,也就是說书在,比如主線程有500ms的耗時(shí)灰伟,要么Running了多久,是否存在Sleeping和Uninterruptible sleep等狀態(tài)儒旬,這段時(shí)間內(nèi)CPU被搶占了壓根就沒騰出功夫來執(zhí)行你這操作栏账。如果有現(xiàn)場(chǎng)的話,通過抓systrace能比較明顯看出來栈源。
4 Input事件本身耗時(shí)
在Android整個(gè)Input體系中有三個(gè)重要的成員:Eventhub挡爵,InputReader,InputDispatcher甚垦。它們分別擔(dān)負(fù)著各自不同的職責(zé)茶鹃,Eventhub負(fù)責(zé)監(jiān)聽/dev/input產(chǎn)生Input事件,InputReader負(fù)責(zé)從Eventhub讀取事件艰亮,并將讀取的事件發(fā)給InputDispatcher闭翩,InputDispatcher則根據(jù)實(shí)際的需要具體分發(fā)給當(dāng)前手機(jī)獲得焦點(diǎn)實(shí)際的Window,最終交給ActivityThread通過消息來處理迄埃。
系統(tǒng)角度:
InputDispatcher分發(fā)事件給Window這個(gè)過程是跨進(jìn)程通信疗韵,獲取對(duì)應(yīng)window本身可能存在耗時(shí)。
應(yīng)用角度:
客戶端接收事件的消息本身又可能存在耗時(shí)和delay的情況侄非,這又回到消息耗時(shí)的范疇了蕉汪。
5 持鎖耗時(shí)
這屬于業(yè)務(wù)邏輯層面的問題流译,最簡(jiǎn)單的就是主線程死鎖,亦或是主線程在等鎖肤无,然后當(dāng)前鎖被其他線程持有在做耗時(shí)操作等等先蒋。
6 頻繁GC
我們知道骇钦,執(zhí)行GC操作的時(shí)候宛渐,所有線程的任何操作都會(huì)需要暫停,等待GC操作完成之后眯搭,其他操作才能夠繼續(xù)運(yùn)行窥翩。通常來說,單個(gè)的GC并不會(huì)占用太多時(shí)間鳞仙,但是大量不停的GC操作則會(huì)顯著占用幀間隔時(shí)間(16ms)寇蚊。如果在幀間隔時(shí)間里面做了過多的GC操作,那么自然其他類似計(jì)算棍好,渲染等操作的可用時(shí)間就變得少了仗岸。
導(dǎo)致GC頻繁執(zhí)行有兩個(gè)原因:
1)內(nèi)存抖動(dòng),在memory monitor里能很明顯看出來借笙,短時(shí)間內(nèi)創(chuàng)建大量對(duì)象然后又迅速被釋放扒怖。
比如:在一個(gè)方法里for循環(huán)拼接String。會(huì)產(chǎn)生大量廢棄的String對(duì)象业稼,短時(shí)間內(nèi)又會(huì)被回收盗痒,所以容易造成抖動(dòng),可以用StringBuilder/StringBuffer來替代低散,它們實(shí)現(xiàn)是動(dòng)態(tài)數(shù)組俯邓,初始長(zhǎng)度128,不夠用了通過arraycopy來增加長(zhǎng)度熔号。對(duì)象統(tǒng)一管理稽鞭,不會(huì)短時(shí)間內(nèi)造成短時(shí)間內(nèi)大量創(chuàng)建和銷毀的問題,同時(shí)append與+相比更安全引镊。
String:適用于少量的字符串操作的情況
StringBuilder:適用于單線程下在字符緩沖區(qū)進(jìn)行大量操作的情況
StringBuffer:適用多線程下在字符緩沖區(qū)進(jìn)行大量操作的情況
2)瞬間產(chǎn)生大量的對(duì)象會(huì)嚴(yán)重占用Young Generation的內(nèi)存區(qū)域川慌,當(dāng)達(dá)到閥值,剩余空間不夠的時(shí)候祠乃,也會(huì)觸發(fā)GC梦重。即使每次分配的對(duì)象占用了很少的內(nèi)存,但是他們疊加在一起會(huì)增加Heap的壓力亮瓷,從而觸發(fā)更多其他類型的GC琴拧。這個(gè)操作有可能會(huì)影響到幀率,并使得用戶感知到性能問題嘱支。
系統(tǒng)層面:
1 內(nèi)存原因
在系統(tǒng)內(nèi)存非常低的情況下蚓胸,常規(guī)經(jīng)驗(yàn)是:MemAvailable 低于MemTotal 1/10的情況下挣饥,容易出現(xiàn)內(nèi)存引起的卡頓,原因無非就是在內(nèi)存低的情況下內(nèi)核在分配內(nèi)存時(shí)沛膳,很難從物理內(nèi)存(伙伴系統(tǒng))直接拿到合適大小的頁面扔枫,此時(shí)會(huì)觸發(fā)回收操作,如內(nèi)存整理(compact)锹安、回收匿名頁(swap)短荐、回收文件頁(dirty=回寫,clean=丟棄)等操作叹哭。這些回收操作較慢忍宋,因此耗時(shí)。這個(gè)過程主要體現(xiàn)在新啟一個(gè)應(yīng)用风罩,zygote fork進(jìn)程申請(qǐng)內(nèi)存的時(shí)候糠排。
2 系統(tǒng)服務(wù)持鎖耗時(shí)
應(yīng)用binder call請(qǐng)求系統(tǒng)服務(wù),一般來說超升,系統(tǒng)服務(wù)如AMS入宦、WMS對(duì)應(yīng)的方法,一上來先不管三七二十一,就是一把大鎖室琢,很多情況下乾闰,特定的操作會(huì)造成持鎖耗時(shí)的情況,具體問題具體分析研乒。
3 CPU調(diào)度問題
這類情況不太多見汹忠,但是也是存在的。在某個(gè)繪制周期中雹熬,CPU被搶占宽菜,無法及時(shí)開始繪制操作。這分幾部分來看竿报,首先是不是被某個(gè)進(jìn)程搶占的铅乡,比如dex2oat×揖或者看這段時(shí)間CPU使用率非常高阵幸,但是可能是大核跑滿了,但是小核相對(duì)比較閑芽世,這屬于系統(tǒng)調(diào)度有問題等等挚赊。
例如:dex2oat發(fā)生的時(shí)候,占用所有有CPU(默認(rèn)策略是有多少個(gè)核济瓢,就啟動(dòng)多少個(gè)線程)荠割,會(huì)將原文件中的dex文件抽出來,逐個(gè)指令的判斷,然后進(jìn)行翻譯蔑鹦,并生成大量的中間內(nèi)容夺克,這些在memory當(dāng)中是保存不下的,所以采用了swap機(jī)制, memory越少嚎朽,越容易發(fā)生交換铺纽,所以還可能引起IO上的瓶頸。
可以設(shè)置系統(tǒng)屬性:dalvik.vm.bg-dex2oat-threads 和 dalvik.vm.dex2oat-threads 哟忍,這兩個(gè)系統(tǒng)屬性是分別設(shè)置在前后臺(tái)執(zhí)行dex2oat限制的線程數(shù)狡门,對(duì)應(yīng)8核CPU來說,比如設(shè)置前后臺(tái)分別為4魁索,這樣dex2oat執(zhí)行時(shí)間會(huì)變長(zhǎng)融撞,但是卡頓會(huì)被緩解盼铁。
當(dāng)然還有一種情況是粗蔚,當(dāng)手機(jī)溫度過高,導(dǎo)致CPU降頻饶火,也會(huì)出現(xiàn)系統(tǒng)卡頓鹏控。
本文只是對(duì)卡頓分析提供一點(diǎn)不成熟的小思路。隨著學(xué)習(xí)的深入肤寝,我會(huì)持續(xù)更新当辐。
文中牽涉到的布局和重復(fù)繪制相關(guān)的內(nèi)容可以參考我的文章:布局優(yōu)化
文中牽扯到的相關(guān)性能優(yōu)化工具可以參考我的系列文章:性能優(yōu)化工具篇總結(jié)