前言
卡頓場(chǎng)景可分為以下四類(lèi):
- UI繪制:繪制呜笑、刷新
- 應(yīng)用啟動(dòng):安裝啟動(dòng)、冷啟動(dòng)叫胁、熱啟動(dòng)
- 頁(yè)面跳轉(zhuǎn):頁(yè)面間切換驼鹅、前后臺(tái)切換
- 事件響應(yīng):按鍵、系統(tǒng)事件输钩、滑動(dòng)
這四種卡頓場(chǎng)景的根本原因又可以分為兩大類(lèi):
- 界面繪制:主要原因是繪制的層級(jí)深张足、頁(yè)面復(fù)雜、刷新不合理哼绑。
- 數(shù)據(jù)處理:導(dǎo)致這種卡頓場(chǎng)景的原因是數(shù)據(jù)處理量太大碉咆,一般分為三種情況:
- 一是數(shù)據(jù)處理在UI線(xiàn)程(這種應(yīng)該避免)。
- 二是數(shù)據(jù)處理占用CPU高茂浮,導(dǎo)致主線(xiàn)程拿不到時(shí)間片次和。
- 三是內(nèi)存增加導(dǎo)致GC頻繁,從而引起卡頓幌羞。
Android系統(tǒng)顯示原理
Android的顯示過(guò)程可以簡(jiǎn)單概括為:Android應(yīng)用程序把經(jīng)過(guò)測(cè)量、布局熊痴、繪制后的surface緩存數(shù)據(jù)聂宾,通過(guò)SurfaceFlinger把數(shù)據(jù)渲染到屏幕上,通過(guò)Android的刷新機(jī)制來(lái)刷新數(shù)據(jù)巾陕。
繪制原理
應(yīng)用層
在Android的每個(gè)view繪制中又三個(gè)核心步驟:Mesasure纪他、Layout、Draw。通過(guò)Measure和Layout來(lái)確定當(dāng)前需要繪制的view所在的大小和位置混聊,通過(guò)繪制(Draw)到surface句喜。
Measure和Layout都是遞歸來(lái)獲取view的大小和位置,并且以深度作為優(yōu)先級(jí)植康,因此層級(jí)越深展懈,元素越多,耗時(shí)也就越長(zhǎng)冻记。
系統(tǒng)層
應(yīng)用層和系統(tǒng)層是兩個(gè)不同進(jìn)程来惧,在Android的顯示系統(tǒng),使用匿名共享內(nèi)存:SharedClient隅居,每個(gè)應(yīng)用和SurfaceFlinger之間都會(huì)創(chuàng)建一個(gè)SharedClient。在每個(gè)SharedClient中棉钧,最多可以創(chuàng)建31個(gè)ShardBufferStack,每個(gè)Surface都對(duì)應(yīng)一個(gè)ShardBufferStack乒融,也就是一個(gè)window。 一個(gè)SharedClient對(duì)應(yīng)一個(gè)Android應(yīng)用程序愧捕,意味著一個(gè)Android應(yīng)用程序最多可以包含31個(gè)窗口申钩。
顯示整體流程分為三個(gè)模塊:應(yīng)用層繪制到緩存區(qū),SurfaceFlinger把緩存區(qū)數(shù)據(jù)渲染到屏幕邮偎,由于是兩個(gè)不同的進(jìn)程义黎,所以使用Android的匿名共享內(nèi)存SharedClient緩存需要顯示的數(shù)據(jù)來(lái)達(dá)到目的。
知道繪制原理后泻云,那么繪制一個(gè)單元多長(zhǎng)時(shí)間才是合理的狐蜕?
——在理想情況下层释,60FPS(Frames Per Second 每秒傳遞的幀數(shù))就感覺(jué)不到卡,這意味著每個(gè)繪制時(shí)長(zhǎng)應(yīng)該在16ms以?xún)?nèi)贡羔。
Android系統(tǒng)每隔16ms發(fā)出VSYNC信號(hào)治力,觸發(fā)對(duì)UI進(jìn)行渲染。若每次都成功就能達(dá)到流暢畫(huà)面的60FPS晕讲。若某個(gè)操作耗時(shí)較久,系統(tǒng)在得到VSYNC信號(hào)時(shí)就無(wú)法正常渲染瓢省,這樣就會(huì)發(fā)生丟幀現(xiàn)象勤婚。
==卡頓的根本原因==
影響繪制的根本原因有以下兩方面:
- ==繪制任務(wù)太重==,繪制一幀內(nèi)容耗時(shí)太長(zhǎng)馒胆。
- ==主線(xiàn)程太忙==祝迂,導(dǎo)致VSync信號(hào)來(lái)時(shí)還沒(méi)有準(zhǔn)備好數(shù)據(jù)導(dǎo)致丟幀。
性能分析工具
性能問(wèn)題不容易復(fù)現(xiàn)当凡,在分析性能問(wèn)題時(shí)需要借助相應(yīng)的調(diào)試工具纠俭,比如查看Layout層次的Hierarchy View、Android系統(tǒng)自帶的 Profile GPU卡頓檢測(cè)工具和靜態(tài)代碼檢查工具Lint朴则,以及性能分析常用的TraceView和SysTrace等钓简。
卡頓檢測(cè)工具
Profile GPU Rendering是Android4.1系統(tǒng)開(kāi)始提供的開(kāi)發(fā)輔助工具,可在開(kāi)發(fā)者選項(xiàng)中打開(kāi)(華為手機(jī)是:GPU呈現(xiàn)模式分析按鈕)
特點(diǎn):
- 是一個(gè)圖形檢測(cè)工具,實(shí)時(shí)反應(yīng)當(dāng)前繪制的耗時(shí)欧宜。
-
提供一個(gè)標(biāo)準(zhǔn)耗時(shí)坐榆,高于標(biāo)準(zhǔn)耗時(shí),表示當(dāng)前一幀丟失冗茸。
各種顏色含義
技巧:
在實(shí)際開(kāi)發(fā)中席镀,圖形不便做數(shù)據(jù)分析,可通過(guò):adb shell dumpsys gfxinfo com.##.##(包名)把具體的耗時(shí)輸出到日志中來(lái)分析夏漱。
對(duì)大部分應(yīng)用來(lái)說(shuō)丟失幾幀影響不大豪诲,只需保證大部分在警戒線(xiàn)下即可挂绰。通過(guò)Profile GPU Rendering發(fā)現(xiàn)有問(wèn)題對(duì)頁(yè)面后屎篱,可通過(guò)另一個(gè)工具Hierarchy Viewer來(lái)查看布局層次和每個(gè)view所花時(shí)間具體定位。
TraceView
TraceView是AndroidSDK自帶的工具,用來(lái)分析函數(shù)調(diào)用過(guò)程交播,可以分析到應(yīng)用具體每一個(gè)方法的執(zhí)行時(shí)間重虑。
- 使用方法
在使用TraceView分析問(wèn)題之前需要得到一個(gè)*.trace的文件,然后通過(guò)TraceView來(lái)分析秦士。trace文件的獲取方法有兩種:
- 通過(guò)Android Studio的Android Device Monitor缺厉,單擊Start Method Profile按鈕開(kāi)始監(jiān)控,操作要監(jiān)控的界面隧土,完成后stop就會(huì)跳到TraceView視圖提针。
- 代碼中加入調(diào)試語(yǔ)句保存trace文件:android.os.Debug類(lèi)中提供類(lèi)相應(yīng)的方法,調(diào)用代碼如下:
//在開(kāi)始監(jiān)控的地方,保存在"/sdcard/trace_name.trace"
Debug.startMethodTracing("trace_name");
//...
//stop trace
Debug.stopMethodTracing();
- TraceView 視圖說(shuō)明
TraceView視圖分兩個(gè)部分曹傀,上半部分為時(shí)間片面板辐脖,下半部分為分析面板。
- 時(shí)間片面板:X軸表示時(shí)間消耗卖毁,Y表示各個(gè)線(xiàn)程揖曾,每個(gè)線(xiàn)程中的不同方法用不同顏色表示,顏色越寬表示該方法占用CPU時(shí)間越長(zhǎng)亥啦。
- 分析面板:主要關(guān)注Calls + Recur Calls/Total(該方法調(diào)用次數(shù)+遞歸次數(shù))和Cpu Time / Call(該方法耗時(shí))這兩個(gè)值炭剪,也就是關(guān)注調(diào)用次數(shù)多和耗時(shí)久的方法,然后優(yōu)化這些方法的邏輯翔脱。
SysTrace UI性能分析
Systrace是Android4.1以上版本提供的性能數(shù)據(jù)采樣和分析工具奴拦。功能包括跟蹤系統(tǒng)的I/O操作、內(nèi)核工作隊(duì)列届吁、CPU負(fù)載等错妖。能直觀查看CPU周期消耗的具體時(shí)間,用不同顏色來(lái)突出問(wèn)題嚴(yán)重性疚沐,并提供解決建議暂氯。
注意:由于Systrace從系統(tǒng)角度返回一些信息,并不能定位到具體耗時(shí)的方法亮蛔,要具體分析原因要借助TraceView
- Systrace 使用方法
- 在DDMS上使用:
(1)打開(kāi)Android Device Monitor痴施,連接手機(jī)準(zhǔn)備好需抓取界面;
(2)單擊Systrace進(jìn)入抓取前的設(shè)置究流,選擇需跟蹤內(nèi)容辣吃;
(3)手機(jī)操作需跟蹤過(guò)程;
(4)到設(shè)定時(shí)間后芬探,生成Trace文件神得,使用Chrome打開(kāi)文件。 - 使用命令行
cd android-sdk/platform-tools/systrace
python systrace.py --time=10 -o mytrace.html sched gfx view wm
具體命令查看官方文檔
- 應(yīng)用中獲韧捣隆:在應(yīng)用中加入Trace跟蹤需要注意兩點(diǎn):
(1)Trace嵌套時(shí)哩簿,endSection()方法只會(huì)結(jié)束離它最近的一個(gè)beginSection()宵蕉。所以要保證endSection和beginSection調(diào)用次數(shù)匹配。
(2)Trace的begin和end必須在同一線(xiàn)程中執(zhí)行卡骂。
public void ProcessPeople{
Trace.beginSection("ProcessPeople");
try{
Trace.beginSection("Process One");
try{
//code
}finally{
Trace.endSection(); //end Process One
}
Trace.beginSection("Process Two");
try{
//code
}finally{
Trace.endSection(); //end Process Two
}
}finally{
Trace.endSection(); //end ProcessPeople
}
}
- 分析Systrace報(bào)告
通過(guò)前面方法獲取到的trace.html文件国裳,需要用Chrome打開(kāi),其中和UI繪制關(guān)系緊密的是Alerts和Frame兩個(gè)數(shù)據(jù)全跨。
- Alerts:標(biāo)記了性能有問(wèn)題的點(diǎn)缝左,可以看到問(wèn)題的詳細(xì)描述
- Frame:每個(gè)應(yīng)用都有一行專(zhuān)門(mén)顯示frame,它將任何它認(rèn)為性能有問(wèn)題的東西都高亮警告并提升怎么優(yōu)化浓若。
布局優(yōu)化
布局是否合理主要影響的是頁(yè)面測(cè)量時(shí)間的多少渺杉,如果層級(jí)太深,每增加一層則會(huì)增加更多的頁(yè)面顯示時(shí)間挪钓。
常用布局優(yōu)化工具
1. Hierarchy View
Hierarchy View是Android SDK自帶的調(diào)試工具是越,用來(lái)檢查L(zhǎng)ayout嵌套及繪制時(shí)間,以可視化的布局角度獲取Layout布局設(shè)計(jì)和各種屬性信息碌上。
使用:在Android Studio中打開(kāi)Android Device Monitor菜單倚评,直接打開(kāi)Hierarchy View
- 查看層級(jí)圖:在window窗口頁(yè),選擇需要查看的組件馏予,雙擊或單擊Load View Hierarchy按鈕即可天梧。
- 查看某個(gè)view的耗時(shí):在快捷鍵工具欄單擊Obtain layout times for tree rooted at selected node。
一個(gè)應(yīng)用界面非常多霞丧,如果一個(gè)個(gè)用Hierarchy View分析效率低呢岗,可以用另一個(gè)工具Lint,用于檢查所有頁(yè)面的層級(jí)蛹尝,并把深度高于N的界面輸出后豫,然后在用Hierarchy View仔細(xì)分析。
2. 布局層級(jí)檢查
Android Lint是ADT 16之后引入的代碼檢查工具突那,通過(guò)代碼靜態(tài)檢查挫酿,可以發(fā)現(xiàn)潛在的代碼問(wèn)題,并給出優(yōu)化建議愕难。
使用前可在File -> Setting -> Inspections -> Android Lint中配置掃描規(guī)則和缺陷級(jí)別饭豹。
在Android studio中啟動(dòng)Lint:從菜單欄選擇Analyze -> Inspect Code,進(jìn)去后選擇掃描范圍掃描务漩。
布局優(yōu)化方法
通過(guò)減少Layout層級(jí),減少測(cè)量它褪、繪制時(shí)間饵骨,提高復(fù)用性三方方面來(lái)優(yōu)化布局,茫打,優(yōu)化的目的是減少層級(jí)居触,讓布局扁平化妖混,以提高繪制的時(shí)間,提高布局的復(fù)用性轮洋。
1. 減少層級(jí)
減少層級(jí)的兩個(gè)常用方案:
- 合理使用RelativeLayout和LinearLayout
- 合理使用Merge
合理使用RelativeLayout和LinearLayout:
RelativeLayout相對(duì)LinearLayout能夠減少布局層級(jí)制市,但也存在性能低的問(wèn)題,原因是RelativeLayout會(huì)對(duì)子view做兩次測(cè)量弊予,因?yàn)橐蕾?lài)關(guān)系可能和布局中view順序不同祥楣,在確定子view位置時(shí),需先給所有子view做一次排序汉柒。
布局原則:
- 盡量使用RelativeLayout和LinearLayout
- 在層級(jí)相同時(shí)误褪,使用LinearLayout
- 如果用LinearLayout會(huì)使層級(jí)變多,則應(yīng)該用RelativeLayout碾褂。
合理使用Merge
Merge是合并的意思兽间,可以有效優(yōu)化某些符合條件的多余層級(jí)。使用場(chǎng)景如下:
- 在自定義view中使用正塌,父元素盡量是FrameLayout或者LinearLayout嘀略。
- 在Activity中整體布局,根元素需要是FrameLayout乓诽。
Merge使用要求:
- Merge只能用在布局XML文件的根元素帜羊。
- 使用Merge加載布局時(shí),必須指定一個(gè)ViewGroup作為其父元素问裕,并且設(shè)置加載的attachToRoot參數(shù)為true逮壁。
- 不能在ViewStub中使用Merge元素。(原因是ViewStub的inflate方法中根本沒(méi)有attachToRoot的設(shè)置)
2. 提高顯示速度
有時(shí)需要某個(gè)布局在一開(kāi)始不顯示粮宛,在某個(gè)條件下才顯示窥淆,可以通過(guò)visable屬性來(lái)控制,但這樣效率非常低巍杈,因?yàn)殡m然布局隱藏來(lái)忧饭,但還在布局中,仍會(huì)解析這些布局筷畦〈士悖可以使用ViewStub控件來(lái)解決這個(gè)場(chǎng)景并提高效率。
ViewStub是一個(gè)輕量級(jí)的View鳖宾,它是一個(gè)看不見(jiàn)的吼砂,并不占布局位置,占用資源非常小的視圖對(duì)象鼎文。
使用ViewStub注意的點(diǎn):
- ViewStub只能加載一次渔肩,之后ViewStub對(duì)象會(huì)被置空。也就是布局被加載后就不能再用ViewStub來(lái)控制它的顯示隱藏拇惋。
- ViewStub只能用來(lái)加載一個(gè)布局文件周偎,而不是某個(gè)具體的View抹剩。
- ViewStub不能嵌套Merge標(biāo)簽。
ViewStub主要使用場(chǎng)景:
- 在程序運(yùn)行期間蓉坎,某個(gè)布局被加載后澳眷,狀態(tài)就不會(huì)有變化。
- 想要控制一個(gè)布局文件的隱藏/顯示蛉艾,而不是某個(gè)view
3. 布局復(fù)用
開(kāi)發(fā)過(guò)程中可以將一些公共的布局抽離出來(lái)作為一個(gè)布局文件钳踊,然后在需要使用的地方通過(guò)<include/>標(biāo)簽來(lái)實(shí)現(xiàn)引入。
對(duì)布局優(yōu)化的總結(jié)
- 盡量使用RelativeLayout和LinearLayout
- 盡可能少用wrap_content伺通,會(huì)增加布局Measure時(shí)的計(jì)算成本箍土,已知道寬高固定值時(shí)不用wrap_content。
- 將復(fù)用組件抽取出來(lái)并通過(guò)<include/>標(biāo)簽使用
- 使用<ViewStub/>標(biāo)簽加載按需顯示的布局
- 使用<Merge/>標(biāo)簽減少布局嵌套層級(jí)
- 刪除控件中無(wú)用屬性
避免過(guò)度繪制
過(guò)度繪制的主要原因
- XML布局:控件有重疊且都有設(shè)置背景罐监。
- View自繪:View.OnDraw里面同一個(gè)區(qū)域被繪制多次吴藻。
過(guò)度繪制檢測(cè)工具
通過(guò)手機(jī)設(shè)置中開(kāi)發(fā)者選項(xiàng),打開(kāi)Show GPU Overdraw選項(xiàng)(調(diào)試 GPU 過(guò)度繪制)弓柱,打開(kāi)后會(huì)有不同的顏色區(qū)域表示不同的過(guò)度繪制次數(shù)沟堡。不同顏色含義如下:
- 無(wú)色:沒(méi)有過(guò)度繪制,每個(gè)像素只繪制來(lái)1次
- 藍(lán)色:過(guò)度繪制 1 次(大片藍(lán)是可以接受的)
- 綠色:過(guò)度繪制 2 次
- 粉色:過(guò)度繪制 3 次(不要超過(guò)1/4)
- 紅色:過(guò)度繪制 4 次或更多次(需優(yōu)化)
我們的目標(biāo)是減少紅色Overdraw矢空,看到更多藍(lán)色或無(wú)色區(qū)域航罗。
如何避免過(guò)度繪制
- 布局上的優(yōu)化
- 移除XML中非必需的背景,或根據(jù)條件設(shè)置
- 移除Window默認(rèn)背景
- 按需顯示占位背景圖片
在Android自帶的一些主題時(shí)activity往往會(huì)設(shè)置一個(gè)默認(rèn)的背景屁药,這個(gè)背景有DecorView持有粥血。當(dāng)自定義布局有一個(gè)全屏背景時(shí),DecorView的背景此時(shí)對(duì)我們來(lái)說(shuō)是無(wú)用的酿箭,但會(huì)產(chǎn)生一次Overdraw复亏,因此可以移除
protect void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
this.getWindow().setBackgroundDrawaable(null);
}
- 自定義View優(yōu)化
自定義view能減少layout的層級(jí),但在實(shí)際繪制時(shí)容易出現(xiàn)過(guò)度繪制缭嫡〉抻可以通過(guò)canvas.clipRect()來(lái)幫組系統(tǒng)識(shí)別那些可見(jiàn)的區(qū)域,然后只在這個(gè)區(qū)域繪制妇蛀。canvas.quickreject()來(lái)判斷是否沒(méi)和某個(gè)矩形相交耕突,從而跳過(guò)那些非矩形區(qū)域內(nèi)的繪制操作。
啟動(dòng)優(yōu)化
應(yīng)用啟動(dòng)流程
啟動(dòng)分兩種類(lèi)型:冷啟動(dòng)和熱啟動(dòng)
- 冷啟動(dòng):系統(tǒng)會(huì)重新創(chuàng)建一個(gè)新的進(jìn)程分配給它评架,所以會(huì)先創(chuàng)建和初始化Application類(lèi)眷茁,再創(chuàng)建和初始化Activity,最后顯示在界面纵诞。
- 熱啟動(dòng):會(huì)從已有的進(jìn)程中啟動(dòng)上祈,所以熱啟動(dòng)不會(huì)再創(chuàng)建和初始化Application,而是直接創(chuàng)建和初始化Activity。
啟動(dòng) -> Application -> attachBaseContext() -> onCreate() -> Activity生命周期
啟動(dòng)耗時(shí)檢測(cè)
- adb shell am:使用adb shell獲取應(yīng)用真實(shí)啟動(dòng)時(shí)間代碼
adb shell am start -W [packageName]/[packageName.AppstartActivity]
執(zhí)行后得到三個(gè)時(shí)間
- ThisTime:一般和TotalTime時(shí)間一樣雇逞,如果啟動(dòng)時(shí)加來(lái)過(guò)度全透明的頁(yè)面預(yù)先處理一些事,這樣會(huì)比TotalTime凶氯埂塘砸;
- TotalTime:應(yīng)用啟動(dòng)時(shí)間,包括Application和Activity初始化到界面顯示晤锥;
- WaitTime:包括系統(tǒng)影響的耗時(shí)掉蔬。
但這個(gè)方法只能得到固定某個(gè)階段耗時(shí),不能知道具體方法耗時(shí)矾瘾∨危可用代碼打點(diǎn)方式來(lái)得到具體方法耗時(shí)。
- 代碼打點(diǎn)
啟動(dòng)優(yōu)化方案
啟動(dòng)主要完成三件事:UI布局壕翩、繪制和數(shù)據(jù)準(zhǔn)備蛉迹,因此優(yōu)化啟動(dòng)速度也是優(yōu)化這三個(gè)過(guò)程。
- UI布局優(yōu)化
- 減少布局層級(jí)
- 避免過(guò)度繪制
- 啟動(dòng)加載邏輯優(yōu)化
數(shù)據(jù)按需實(shí)現(xiàn)加載邏輯
- 分步加載:以大化小放妈,優(yōu)先級(jí)高的放前
- 異步加載:耗時(shí)多的異步化
- 延期加載:非必需的數(shù)據(jù)延時(shí)加載
合理的刷新機(jī)制
合理的刷新機(jī)制要注意以下幾點(diǎn)北救;
- 盡量減少刷新的次數(shù)
- 盡量避免后臺(tái)有高CPU線(xiàn)程運(yùn)行
- 縮小刷新區(qū)域
減少刷新次數(shù)
- 控制刷新頻率:比如刷新進(jìn)度調(diào)可1%刷新一次,而不是實(shí)時(shí)刷新
- 避免沒(méi)有必要的刷新:先判斷是否需要刷新芜抒,比如數(shù)據(jù)沒(méi)變化珍策、控件不在可見(jiàn)區(qū)域就沒(méi)必要刷新。
避免后臺(tái)線(xiàn)程影響
后臺(tái)線(xiàn)程如果開(kāi)銷(xiāo)很大宅倒,占用CPU過(guò)高攘宙,導(dǎo)致系統(tǒng)頻繁GC和CPU時(shí)間片資源緊張,有可能會(huì)導(dǎo)致頁(yè)面的卡頓拐迁。因此在需要迅速刷新的情況下避免這類(lèi)線(xiàn)程在高峰工作蹭劈。
縮小刷新區(qū)域
采用局部刷新來(lái)節(jié)約資源
- 自定義view時(shí):可以使用兩個(gè)局部更新數(shù)據(jù)的方法
invalidate(Rect dirty)
invalidate(int left, int top, int right, int bottom)
- 容器中的某個(gè)Item發(fā)生了變化,只需更新這個(gè)Item即可唠亚。
提升動(dòng)畫(huà)性能
從三個(gè)緯度來(lái)對(duì)比動(dòng)畫(huà)性能
- 流暢度:核心链方,控制每一幀動(dòng)畫(huà)在16ms以?xún)?nèi)完成
- 內(nèi)存:避免內(nèi)存泄漏,減小內(nèi)存開(kāi)銷(xiāo)
- 耗電:減小運(yùn)算量灶搜,優(yōu)化算法祟蚀,減小CPU占用
優(yōu)化建議:
- 盡量使用屬性動(dòng)畫(huà)
- 適當(dāng)使用硬件加速
使用硬件加速注意幾點(diǎn):
- 在軟件渲染時(shí),可以使用重用Bitmap的方法來(lái)節(jié)省內(nèi)存割卖,但開(kāi)啟硬件加速后不起作用前酿。
- 開(kāi)啟硬件加速的View在前臺(tái)運(yùn)行時(shí),需要耗費(fèi)額外的內(nèi)存鹏溯,加速的UI切換到后臺(tái)時(shí)罢维,產(chǎn)生的額外內(nèi)存有可能不釋放。
- 當(dāng)UI中存在過(guò)度繪制時(shí)丙挽,硬件加速容易發(fā)生問(wèn)題肺孵。
參考書(shū)籍:《Android應(yīng)用性能優(yōu)化最佳實(shí)踐》