一、 卡頓有哪些場(chǎng)景
????首先檬果,回想下在什么情況下你會(huì)覺(jué)得某個(gè)App很卡,不妨設(shè)想現(xiàn)在從手機(jī)桌面打開(kāi)一個(gè)App-簡(jiǎn)書(shū)唐断。
????1 當(dāng)App啟動(dòng)后很久才進(jìn)入主頁(yè)面选脊,你會(huì)覺(jué)得卡;
????2 當(dāng)主頁(yè)面內(nèi)容很久才展示完全脸甘,你會(huì)覺(jué)得卡恳啥;
????3 當(dāng)在列表頁(yè)面滑動(dòng)時(shí)出現(xiàn)停頓,你會(huì)覺(jué)得卡丹诀;
????4 當(dāng)點(diǎn)擊某篇文章停留很久才跳轉(zhuǎn)钝的,你會(huì)覺(jué)得卡;
????5 當(dāng)點(diǎn)擊訂閱按鈕铆遭,很久才有訂閱成功反饋硝桩,你會(huì)覺(jué)得卡;
總結(jié)提煉下枚荣,你會(huì)發(fā)現(xiàn)卡頓這種視覺(jué)感知問(wèn)題歸根到底是事件處理和UI展示的綜合消耗時(shí)間超出了用戶(hù)感官系統(tǒng)的期待時(shí)間碗脊。精確一點(diǎn)就可給出如下定義:
二、卡頓定義
????在能夠感知的視覺(jué)場(chǎng)景中橄妆,當(dāng)事件處理(思考)和UI展示(表達(dá))的綜合消耗時(shí)間超過(guò)用戶(hù)視覺(jué)系統(tǒng)的最大期待時(shí)間衙伶,我們就說(shuō)出現(xiàn)了卡頓。
????在卡頓描述中有提到一個(gè)概念用戶(hù)視覺(jué)系統(tǒng)的期待時(shí)間害碾,這個(gè)期待時(shí)間是主觀的痕支,但要小于大多數(shù)用戶(hù)的期待時(shí)間,它在一定條件下又是客觀的蛮原。比如當(dāng)點(diǎn)擊訂閱按鈕卧须,App會(huì)彈出訂閱成功或者訂閱失敗彈框,有人等待1s也沒(méi)覺(jué)得有問(wèn)題,也有人超過(guò)0.4s就感覺(jué)體驗(yàn)很不爽花嘶,但我們開(kāi)發(fā)者要關(guān)注的是所有用戶(hù)的期待時(shí)間笋籽,所以閾值一定是讓大多數(shù)用戶(hù)感覺(jué)爽的,若點(diǎn)擊App中任意按鈕椭员、視圖车海,都在0.1s內(nèi)給出反饋,這樣基本上99%的人都是感覺(jué)-哇哦~你們App反應(yīng)好快隘击。
????除了上述事件反饋時(shí)間侍芝,還有一個(gè)我們?nèi)祟?lèi)視覺(jué)系統(tǒng)硬件帶來(lái)的期待時(shí)間,那就是連續(xù)動(dòng)畫(huà)中單個(gè)畫(huà)面的渲染時(shí)間埋同。連續(xù)的動(dòng)畫(huà)中一個(gè)畫(huà)面的如果在視覺(jué)暫留時(shí)間內(nèi)沒(méi)有渲染好州叠,顯示系統(tǒng)將會(huì)展示上一幀頁(yè)面,那么對(duì)于用戶(hù)來(lái)說(shuō)就是發(fā)生了卡頓凶赁。通常用fps衡量渲染速度咧栗,fps(frame per second)是一秒鐘系統(tǒng)的渲染頁(yè)面的總次數(shù)。渲染速度越快虱肄,平均一幀渲染時(shí)間就越短致板,我們就感覺(jué)越絲滑。但由于人類(lèi)的視覺(jué)暫留時(shí)間基本都大于16.6ms咏窿,所以我們單幀渲染時(shí)間小于16.6ms(也就是幀率60fps以上)就可以使大多數(shù)人感覺(jué)非常流暢斟或。
????通過(guò)卡頓的定義,我們找到了解決卡頓的關(guān)鍵兩要素:
????1 事件數(shù)據(jù)處理時(shí)間
????2 UI渲染時(shí)間
????我們要做的是在用戶(hù)能夠感知的使用場(chǎng)景中集嵌,給出優(yōu)化事件處理時(shí)間和UI渲染時(shí)間的方案缕粹。
????結(jié)合用戶(hù)感知最多的卡頓場(chǎng)景,可以得出一個(gè)較全面的卡頓解決方案:
三纸淮、最常見(jiàn)卡頓的解決方案
3.1 App冷啟動(dòng)優(yōu)化
冷啟動(dòng)具體時(shí)間段界定
startTime:用戶(hù)點(diǎn)擊桌面圖標(biāo)開(kāi)始
點(diǎn)擊桌面圖標(biāo)->點(diǎn)擊事件回調(diào)到桌面(Launcher) App->Launcher處理點(diǎn)擊事件,收集該圖標(biāo)相關(guān)的信息亚享,發(fā)起intent調(diào)用->跨進(jìn)程調(diào)用AMS啟動(dòng)對(duì)應(yīng)的進(jìn)程
然后再ActivityStarter打印log:
2021-12-16 14:18:50.402 24772-24772/com.example.demo V/JG: launcher onClick start
2021-12-16 14:18:50.403 534-945/system_process I/ActivityTaskManager: START u0 {flg=0x10000000 cmp=com.jingang.lifechange/.SplashActivity} from uid 10164
2021-12-16 14:18:50.449 534-563/system_process I/ActivityManager: Start proc 24932:com.jingang.lifechange/u0a162 for pre-top-activity {com.jingang.lifechange/com.jingang.lifechange.SplashActivity}
//第一句log是我們模擬桌面App的點(diǎn)擊事件咽块;第二句log是AMS開(kāi)始啟動(dòng)SplashActivity,第三句log是ams發(fā)現(xiàn)該Activity所在的進(jìn)程未啟動(dòng)去啟動(dòng)進(jìn)程欺税。
從點(diǎn)擊桌面到這兩個(gè)log打印侈沪,過(guò)程有很多步驟,但這些步驟里面沒(méi)有耗時(shí)操作晚凿,一般情況下非常短暫(2ms內(nèi))亭罪,而且沒(méi)有l(wèi)og,所以一般情況下把上述第二句logSTART u0 ~*的時(shí)間當(dāng)作App冷啟動(dòng)的開(kāi)始時(shí)間歼秽。當(dāng)然一些特殊情況需要定位問(wèn)題到底在哪邊需要精確定位時(shí)間应役,我們就要想辦法去無(wú)限接近用戶(hù)點(diǎn)擊桌面圖標(biāo)的時(shí)間,這個(gè)需要去了解Android 輸入系統(tǒng)或觀察Launcher app點(diǎn)擊日志(基本沒(méi)有),以后介紹箩祥。
但只有代碼運(yùn)行到自己的App進(jìn)程之后院崇,我們才能有所作為,所以還要記錄下冷啟動(dòng)時(shí)候App最早收到回調(diào)的時(shí)間點(diǎn)-這也是優(yōu)化的起點(diǎn)時(shí)間:
public class MainApplication extends Application {
private static final String TAG = "lifeCycle:"+MainApplication.class.getName();
@Override
protected void attachBaseContext(Context base) {
Log.v(TAG,"attachBaseContext");
super.attachBaseContext(base);
}
}
endTime:第一個(gè)頁(yè)面主體內(nèi)容展示出來(lái)結(jié)束
如果有splash頁(yè)面袍祖,那就是Home頁(yè)面主體展示出來(lái)結(jié)束底瓣。
home頁(yè)面主體展示出來(lái),比較精確的就是取第一幀圖像繪制出來(lái)的時(shí)間蕉陋。
@Override
protected void onResume() {
super.onResume();
Log.v(getTag(),"onResume");
getWindow().getDecorView().getViewTreeObserver().addOnDrawListener(new ViewTreeObserver.OnDrawListener() {
@Override
public void onDraw() {
Log.v(getTag(),"onDecorView draw , draw time");
}
});
}
當(dāng)然也可以利用系統(tǒng)log捐凭,如下的第二句log
2021-12-16 15:19:35.979 28022-28022/com.jingang.lifechange V/lifeCycle:MainActivity: onDecorView draw , draw time 0
2021-12-16 15:19:36.010 534-561/system_process I/ActivityTaskManager: Displayed com.jingang.lifechange/.MainActivity: +1s863ms
2021-12-16 15:19:36.018 28022-28022/com.jingang.lifechange V/lifeCycle:MainActivity: onDecorView draw , draw time 1
2021-12-16 15:19:36.040 28022-28022/com.jingang.lifechange V/lifeCycle:MainActivity: onDecorView draw , draw time 2
冷啟啟動(dòng)時(shí)間指標(biāo)
4s以?xún)?nèi)良,8s以外差————用戶(hù)點(diǎn)擊后桌面圖標(biāo)后凳鬓, 心里開(kāi)始 數(shù)1 茁肠、2、 3 到4頁(yè)面還沒(méi)有出來(lái)村视,用戶(hù)開(kāi)始著急官套,數(shù)到7、8沒(méi)出來(lái)用戶(hù)一般就放棄了蚁孔。也就是上面我們記錄的結(jié)束時(shí)間減去開(kāi)始時(shí)間最好不好超過(guò)4s奶赔。
冷啟動(dòng)優(yōu)化方案
原則: 視覺(jué)優(yōu)化、異步杠氢、 懶加載站刑、協(xié)調(diào)加載順序
實(shí)現(xiàn):
1)用戶(hù)點(diǎn)擊桌面應(yīng)用圖標(biāo)——到正式展示出來(lái)假如只有4S,但4S內(nèi)出現(xiàn)了點(diǎn)擊無(wú)反應(yīng)鼻百、黑屏绞旅、白屏,那用戶(hù)主觀感覺(jué)也是非常糟糕温艇。所以我們把視覺(jué)優(yōu)化放到第一個(gè)要優(yōu)化的項(xiàng)目因悲,最常見(jiàn)的就是把冷啟動(dòng)的Activity增加一個(gè)帶有背景(閃屏圖)的主體(Theme)。
2)把沒(méi)有必要放在UI線程中的初始化任務(wù)放入其他線程勺爱;
3)把沒(méi)有必要在應(yīng)用啟動(dòng)時(shí)的初始化任務(wù)移動(dòng)到真正使用之前晃琳,或者利用應(yīng)用線程空余時(shí)間進(jìn)行;
4)注意很多初始化任務(wù)是有順序的琐鲁,在優(yōu)化過(guò)程中這些順序要注意保持卫旱;
如果是一個(gè)大型項(xiàng)目,app啟動(dòng)初始化任務(wù)特別多围段,有很多初始化任務(wù)之間有加載順序問(wèn)題顾翼,初始化也不一定在UI線程進(jìn)行,那我們可以大干一場(chǎng)奈泪,構(gòu)建一個(gè)App啟動(dòng)器适贸。App啟動(dòng)器是一個(gè)任務(wù)調(diào)度工具類(lèi)灸芳,可以把不同的初始化任務(wù)按照順序在不同線程中執(zhí)行,從而使App啟動(dòng)正確而高效取逾,做好之后也可以在不同項(xiàng)目中復(fù)用耗绿。這個(gè)以后再探討如何設(shè)計(jì)和實(shí)現(xiàn)。
3.2 頁(yè)面跳轉(zhuǎn)卡頓優(yōu)化
頁(yè)面跳轉(zhuǎn)時(shí)間段界定
startTime:發(fā)起頁(yè)面用戶(hù)事件
endTime:打開(kāi)頁(yè)面首幀加載完成
頁(yè)面跳轉(zhuǎn)時(shí)間指標(biāo)
1s內(nèi)- 頁(yè)面秒開(kāi)砾隅,無(wú)他頁(yè)面秒開(kāi)基本稱(chēng)為一個(gè)用戶(hù)在App內(nèi)操作的一個(gè)潛在標(biāo)準(zhǔn)误阻,當(dāng)然跟用戶(hù)主觀感受有關(guān)。
頁(yè)面跳轉(zhuǎn)優(yōu)化概覽
普通模式下:Activity A跳轉(zhuǎn)Activity B生命周期
A onPause()->B onCreate()-> B onStart()->B onResume()->A onStop()
B頁(yè)面的首幀加載是在B onResume()回調(diào)后進(jìn)行View的測(cè)量晴埂、布局究反、繪制,同本文冷啟動(dòng)結(jié)束的時(shí)間節(jié)點(diǎn)儒洛,可以參考上面進(jìn)行精耐。
由頁(yè)面跳轉(zhuǎn)的計(jì)算開(kāi)始結(jié)束時(shí)間點(diǎn),可得如下具體點(diǎn)優(yōu)化點(diǎn):
1)頁(yè)面跳轉(zhuǎn)發(fā)起頁(yè)面Activity A琅锻,onPause()內(nèi)盡量減少UI線程耗時(shí)操作卦停,可提升這個(gè)頁(yè)面打開(kāi)其他頁(yè)面的速度;
2)頁(yè)面跳轉(zhuǎn)發(fā)起頁(yè)面Activity A恼蓬,onStop()的UI線程耗時(shí)操作惊完,雖然不會(huì)使頁(yè)面跳轉(zhuǎn)看起來(lái)加快,但因?yàn)閛nStop是這個(gè)用戶(hù)操作最后一個(gè)環(huán)節(jié)处硬,所以減少耗時(shí)操作可以減少出現(xiàn)ANR的概率小槐;
3)被打開(kāi)頁(yè)面Activity B 在onCreate()\onStart()\onResume()方法中盡量減少UI線程的耗時(shí)操作,提升這個(gè)頁(yè)面被打開(kāi)的速度 荷辕。比如一些耗時(shí)操作移動(dòng)到idleHandler中凿跳;
4)對(duì)Activity B 的View層級(jí)、布局疮方、繪制等進(jìn)行優(yōu)化也可以加快頁(yè)面B的打開(kāi)速度控嗜,這個(gè)會(huì)在接下來(lái)章節(jié)繼續(xù)展開(kāi)。
原則
根據(jù)不同模式下Activity啟動(dòng)骡显,涉及到的生命周期變化進(jìn)行跟蹤優(yōu)化疆栏。
3.3 頁(yè)面滑動(dòng),屬性動(dòng)畫(huà)蟆盐、幀動(dòng)畫(huà)等動(dòng)畫(huà)
動(dòng)畫(huà)的本質(zhì)是什么?
動(dòng)畫(huà)和視頻的本質(zhì)都是按一定順序快速展示的一組圖片遭殉,因?yàn)槿搜鄣囊曈X(jué)暫留原理就形成了連續(xù)移動(dòng)的感覺(jué)石挂。
圖片的本質(zhì)是什么?
一張圖片的本質(zhì)是一組像素點(diǎn)
圖片的這一組像素點(diǎn)如何得來(lái)
1 現(xiàn)成圖片:各種圖片格式险污,雖然可能有壓縮痹愚,但一張jpg富岳,png等格式的圖片本質(zhì)就是一組像素點(diǎn)。
????顯示的時(shí)候就是把這組像素點(diǎn)從網(wǎng)絡(luò)拯腮、硬盤(pán)等地方讀入到內(nèi)存窖式,由內(nèi)存完成一些校驗(yàn)工作,并緩存起來(lái)动壤,等待特定時(shí)機(jī)(接收到vsync信號(hào)時(shí))寫(xiě)入到顯示器緩存區(qū)-從而顯示出來(lái)萝喘。
2 由數(shù)據(jù)生成
????操作系統(tǒng)都提供一套用戶(hù)定義圖像api--比如Android的顯示系統(tǒng)中View就是提供給用戶(hù)自定義圖像的api,用戶(hù)按照一定規(guī)范調(diào)用api描述自己想要的圖像琼懊,系統(tǒng)在特定時(shí)機(jī)(接收到vsync信號(hào)時(shí))就把這種用戶(hù)規(guī)范的描述轉(zhuǎn)換成一組像素點(diǎn)阁簸,寫(xiě)入顯示器緩存,從而顯示出來(lái)哼丈。
????我們上面談到的頁(yè)面滑動(dòng)启妹、屬性動(dòng)畫(huà)都屬于第二種情況——圖片是由操作系統(tǒng)根據(jù)用戶(hù)的描述一步步生成,所以接下來(lái)的重點(diǎn)是討論這種情況如何顯示和優(yōu)化醉旦。
????計(jì)算機(jī)顯示一張圖片饶米,其實(shí)跟我們自己找人畫(huà)一張畫(huà)像然后送給朋友非常相似,都是由我們把想畫(huà)的場(chǎng)景告訴畫(huà)家车胡,畫(huà)家取紙張繪制檬输,最后我們拿到畫(huà)作,去展示給心愛(ài)的人吨拍。
????在android系統(tǒng)上我們稍微深入一點(diǎn)褪猛,得到一個(gè)更詳細(xì)的流程。
????從上述一張圖繪制流程圖可以看到羹饰,系統(tǒng)繪制一張圖比較耗時(shí)間的點(diǎn)在于1) 讀取和轉(zhuǎn)換用戶(hù)描述 ,2)繪制(包含渲染)
????除此之外伊滋,我們大多數(shù)繪制工作是要在ui線程進(jìn)行,雖然android系統(tǒng)有消息屏障機(jī)制可保證繪制任務(wù)優(yōu)先級(jí)很高队秩,但是ui線程并不能把當(dāng)前正在執(zhí)行的任務(wù)終止笑旺,所以在進(jìn)行繪制流程時(shí)候,遇到UI線程中有耗時(shí)很多的任務(wù)馍资,也會(huì)導(dǎo)致繪制被推遲筒主,從而造成卡頓、丟幀等鸟蟹。
????所以我們優(yōu)化從以下兩個(gè)方面進(jìn)行:
????1乌妙、布局優(yōu)化
????布局優(yōu)化就是指是采用盡量節(jié)約的方式指達(dá)到同樣的顯示效果。比如減少 View 層級(jí)建钥,這樣會(huì)加快 View 的循環(huán)遍歷過(guò)程藤韵,比如view層級(jí)優(yōu)化可以減少view;去除不必要的背景(背景是單獨(dú)繪制)熊经,可以使繪制內(nèi)容減少泽艘;減少View 的過(guò)度繪制欲险;提前把xml轉(zhuǎn)換成java代碼,減少解析時(shí)間等匹涮;
????2天试、 減少UI線程中耗時(shí)任務(wù)和異常阻塞
????前面有講到過(guò)UI線程會(huì)處理完當(dāng)前任務(wù),才進(jìn)行繪制然低,如果在我們申請(qǐng)繪制的時(shí)候有耗時(shí)任務(wù)在執(zhí)行喜每,那勢(shì)必會(huì)影響正常顯示,android系統(tǒng)三緩沖機(jī)制脚翘,所以原則上一個(gè)ui線程任務(wù)超過(guò)16*3=48ms的時(shí)候就會(huì)對(duì)繪制任務(wù)造成影響灼卢,所以我們要減少這些任務(wù)。在full GC的時(shí)候也停止ui線程来农,影響繪制造成卡頓鞋真。
如何檢測(cè)
1 布局優(yōu)化檢測(cè)
Hierarchy Viewer、開(kāi)發(fā)者模式過(guò)度繪制開(kāi)關(guān)等
2 ui耗時(shí)任務(wù)檢測(cè)
通過(guò)類(lèi)似下面的計(jì)算出ui線程每個(gè)任務(wù)執(zhí)行的時(shí)間沃于,找到耗時(shí)比較長(zhǎng)的時(shí)間進(jìn)行優(yōu)化涩咖。
getMainLooper().setMessageLogging(new LogPrinter(Log.INFO,"uiThread"));
可以通過(guò)hook等方法,插入定位問(wèn)題所需要的信息繁莹。
五 思考題目
系統(tǒng)有哪些監(jiān)控措施是處理卡頓的檩互?
答:ANR 、strictmode
本文分析的可全面咨演?
答:并沒(méi)有闸昨, 我們只是找了啟動(dòng)流程中最可能有問(wèn)題的地方拿出來(lái)分析講解,不代表其他地方?jīng)]有問(wèn)題薄风,比如跨進(jìn)程通信 饵较、AMS狀態(tài)、當(dāng)前系統(tǒng)cpu和內(nèi)存使用狀態(tài)等遭赂。
關(guān)于流暢度未來(lái)
1 卡頓預(yù)測(cè)
基于對(duì)用戶(hù)行為的洞察循诉,可以預(yù)測(cè)到接下來(lái)會(huì)該顯示哪些內(nèi)容,提前進(jìn)行數(shù)據(jù)初始化撇他,甚至提前走完所有繪制步驟茄猫。
2 分工與異步
對(duì)繪制過(guò)程再進(jìn)行重新解讀,將當(dāng)前順序執(zhí)行的再次進(jìn)行分工困肩,充分利用當(dāng)前多cpu和gpu架構(gòu)划纽。
3 顯示效果與功耗更加平衡
動(dòng)態(tài)多線程,動(dòng)態(tài)開(kāi)啟gpu
4 感官優(yōu)化
用戶(hù)覺(jué)得慢锌畸,這個(gè)問(wèn)題是“用戶(hù)覺(jué)得慢”勇劣,不一定是真的慢。