一抱虐、前言
隨著項(xiàng)目版本的迭代,App的性能問題會(huì)逐漸暴露出來梢夯,而好的用戶體驗(yàn)與性能表現(xiàn)緊密相關(guān)言疗,從本篇文章開始,我將開啟一個(gè)Android應(yīng)用性能優(yōu)化的專題颂砸,從理論到實(shí)戰(zhàn)噪奄,從入門到深挖,手把手將性能優(yōu)化實(shí)踐到項(xiàng)目中人乓,歡迎持續(xù)關(guān)注勤篮!
那么第一篇文章我就從應(yīng)用的啟動(dòng)優(yōu)化開始,根據(jù)實(shí)際案例色罚,打造閃電般的App啟動(dòng)速度碰缔。
二、初識(shí)啟動(dòng)加速
來看一下Google官方文檔《Launch-Time Performance》 對(duì)應(yīng)用啟動(dòng)優(yōu)化的概述戳护;
應(yīng)用的啟動(dòng)分為冷啟動(dòng)金抡、熱啟動(dòng)瀑焦、溫啟動(dòng),而啟動(dòng)最慢梗肝、挑戰(zhàn)最大的就是冷啟動(dòng):系統(tǒng)和App本身都有更多的工作要從頭開始榛瓮!
應(yīng)用在冷啟動(dòng)之前,要執(zhí)行三個(gè)任務(wù):
加載啟動(dòng)App巫击;
App啟動(dòng)之后立即展示出一個(gè)空白的Window禀晓;
創(chuàng)建App的進(jìn)程;
而這三個(gè)任務(wù)執(zhí)行完畢之后會(huì)馬上執(zhí)行以下任務(wù):
創(chuàng)建App對(duì)象坝锰;
啟動(dòng)Main Thread粹懒;
創(chuàng)建啟動(dòng)的Activity對(duì)象;
加載View顷级;
布置屏幕凫乖;
進(jìn)行第一次繪制;
而一旦App進(jìn)程完成了第一次繪制愕把,系統(tǒng)進(jìn)程就會(huì)用Main Activity替換已經(jīng)展示的Background Window拣凹,此時(shí)用戶就可以使用App了。
作為普通應(yīng)用恨豁,App進(jìn)程的創(chuàng)建等環(huán)節(jié)我們是無法主動(dòng)控制的嚣镜,可以優(yōu)化的也就是Application、Activity創(chuàng)建以及回調(diào)等過程橘蜜。
同樣菊匿,Google也給出了啟動(dòng)加速的方向:
利用提前展示出來的Window,快速展示出來一個(gè)界面计福,給用戶快速反饋的體驗(yàn)跌捆;
避免在啟動(dòng)時(shí)做密集沉重的初始化(Heavy app initialization);
定位問題:避免I/O操作象颖、反序列化佩厚、網(wǎng)絡(luò)操作、布局嵌套等说订。
備注:方向1屬于治標(biāo)不治本抄瓦,只是表面上快;方向2陶冷、3可以真實(shí)的加快啟動(dòng)速度钙姊。
接下來我們就在項(xiàng)目中實(shí)際應(yīng)用。
三埂伦、啟動(dòng)加速之主題切換
按照官方文檔的說明:使用Activity的windowBackground主題屬性來為啟動(dòng)的Activity提供一個(gè)簡(jiǎn)單的drawable煞额。
Layout XML file:
Manifest file:
這樣在啟動(dòng)的時(shí)候,會(huì)先展示一個(gè)界面,這個(gè)界面就是Manifest中設(shè)置的Style膊毁,等Activity加載完畢后胀莹,再去加載Activity的界面,而在Activity的界面中媚媒,我們將主題重新設(shè)置為正常的主題嗜逻,從而產(chǎn)生一種快的感覺涩僻。不過如上文總結(jié)這種方式其實(shí)并沒有真正的加速啟動(dòng)過程缭召,而是通過交互體驗(yàn)來優(yōu)化了展示的效果。
**備注:截圖同樣來自官方文檔《Launch-Time Performance》 **逆日。
四嵌巷、啟動(dòng)加速之Avoid Heavy App Initialization
通過代碼分析我們可以得到App啟動(dòng)的業(yè)務(wù)工作流程圖:
這一章節(jié)我們重點(diǎn)關(guān)注初始化的部分:在Application以及首屏Activity中我們主要做了:
**MultiDex以及Tinker的初始化,最先執(zhí)行室抽;關(guān)于MultiDex的優(yōu)化本文不再贅述搪哪,參考我之前
Application中主要做了各種三方組件的初始化;
項(xiàng)目中除聽云之外其余所有三方組件都搶占先機(jī)坪圾,在Application主線程初始化晓折。這樣的初始化方式肯定是過重的:
考慮異步初始化三方組件,不阻塞主線程兽泄;
延遲部分三方組件的初始化漓概;實(shí)際上我們粗粒度的把所有三方組件都放到異步任務(wù)里,可能會(huì)出現(xiàn)WorkThread中尚未初始化完畢但MainThread中已經(jīng)使用的錯(cuò)誤病梢,因此這種情況建議延遲到使用前再去初始化胃珍;
而如何開啟WorkThread同樣也有講究,這個(gè)話題在下文詳談蜓陌。
項(xiàng)目修改:
將友盟觅彰、Bugly、聽云钮热、GrowingIO填抬、BlockCanary等組件放在WorkThread中初始化;
延遲地圖定位隧期、ImageLoader飒责、自有統(tǒng)計(jì)等組件的初始化:地圖及自有統(tǒng)計(jì)延遲4秒,此時(shí)應(yīng)用已經(jīng)打開厌秒;而ImageLoader
因?yàn)檎{(diào)用關(guān)系不能異步以及過久延遲读拆,初始化從Application延遲到SplashActivity;而EventBus因?yàn)樵貯ctivity中使用所以必須在Application中初始化鸵闪。
注意:閃屏頁(yè)的2秒停留可以利用檐晕,把耗時(shí)操作延遲到這個(gè)時(shí)間間隔里。
五、啟動(dòng)加速之Diagnosing The Problem
本節(jié)我們實(shí)際定位耗時(shí)的操作辟灰,在開發(fā)階段我們一般使用BlockCanary或者ANRWatchDog找耗時(shí)操作个榕,簡(jiǎn)單明了,但是無法得到每一個(gè)方法的執(zhí)行時(shí)間以及更詳細(xì)的對(duì)比信息芥喇。我們可以通過Method Tracing或者DDMS來獲得更全面詳細(xì)的信息西采。
啟動(dòng)應(yīng)用,點(diǎn)擊 Start Method Tracing继控,應(yīng)用啟動(dòng)后再次點(diǎn)擊械馆,會(huì)自動(dòng)打開剛才操作所記錄下的.trace文件,建議使用DDMS來查看武通,功能更加方便全面霹崎。
左側(cè)為發(fā)生的具體線程,右側(cè)為發(fā)生的時(shí)間軸冶忱,下面是發(fā)生的具體方法信息尾菇。注意兩列:Real Time/Call(實(shí)際發(fā)生時(shí)間),Calls+RecurCalls/Total(發(fā)生次數(shù))囚枪;
上圖我們可以得到以下信息:
可以直觀看到MainThread的時(shí)間軸很長(zhǎng)派诬,說明大多數(shù)任務(wù)都是在MainThread中執(zhí)行;
通過Real Time/Call 降序排列可以看到程序中的部分代碼確實(shí)非常耗時(shí)链沼;
在下一頁(yè)可以看出來部分三方SDK也比較耗時(shí)默赂;
即便是耗時(shí)操作,但是只要正確發(fā)生在WorkThread就沒問題忆植。因此我們需要確認(rèn)這些方法執(zhí)行的線程以及發(fā)生的時(shí)機(jī)放可。這些操作如果發(fā)生在主線程,可能不構(gòu)成ANR的發(fā)生條件朝刊,但是卡頓是再算難免的耀里!結(jié)合上章節(jié)圖App冷啟動(dòng)業(yè)務(wù)工作流程圖中業(yè)務(wù)操作以及分析圖失暴,再次查看代碼我們可以看到:部分耗時(shí)操作例如IO讀取等確實(shí)發(fā)生在主線程回窘。事實(shí)上在traceview里點(diǎn)擊執(zhí)行函數(shù)的名稱不僅可以跟蹤到父類及子類的方法耗時(shí),也可以在方法執(zhí)行時(shí)間軸中看到具體在哪個(gè)線程以及耗時(shí)的界面閃動(dòng)莽囤。
分析到部分耗時(shí)操作發(fā)生在主線程咙鞍,那我們把耗時(shí)操作都改到子線程是不是就萬事大吉了房官?非也!续滋!
卡頓不能都靠異步來解決翰守,錯(cuò)誤的使用工程線程不僅不能改善卡頓,反而可能加劇卡頓疲酌。是否需要開啟工作線程需要根據(jù)具體的性能瓶頸根源具體分析蜡峰,對(duì)癥下藥了袁,不可一概而論;
而如何開啟線程同樣也有學(xué)問:Thread湿颅、ThreadPoolExecutor载绿、AsyncTask、HandlerThread油航、IntentService等都各有利弊崭庸;例如通常情況下ThreadPoolExecutor比Thread更加高效、優(yōu)勢(shì)明顯谊囚,但是特定場(chǎng)景下單個(gè)時(shí)間點(diǎn)的表現(xiàn)Thread會(huì)比ThreadPoolExecutor好:同樣的創(chuàng)建對(duì)象怕享,ThreadPoolExecutor的開銷明顯比Thread大;
正確的開啟線程也不能包治百病秒啦,例如執(zhí)行網(wǎng)絡(luò)請(qǐng)求會(huì)創(chuàng)建線程池熬粗,而在Application中正確的創(chuàng)建線程池勢(shì)必也會(huì)降低啟動(dòng)速度;因此延遲操作也必不可少余境。
通過對(duì)traceview的詳細(xì)跟蹤以及代碼的詳細(xì)比對(duì),我發(fā)現(xiàn)卡頓發(fā)生在:
部分?jǐn)?shù)據(jù)庫(kù)及IO的操作發(fā)生在首屏Activity主線程灌诅;
Application中創(chuàng)建了線程池芳来;
首屏Activity網(wǎng)絡(luò)請(qǐng)求密集;
工作線程使用未設(shè)置優(yōu)先級(jí)猜拾;
信息未緩存即舌,重復(fù)獲取同樣信息;
流程問題:例如閃屏圖每次下載挎袜,當(dāng)次使用顽聂;
以及其它細(xì)節(jié)問題:
執(zhí)行無用老代碼;
執(zhí)行開發(fā)階段使用的代碼盯仪;
執(zhí)行重復(fù)邏輯紊搪;
調(diào)用三方SDK里或者Demo里的多余代碼;
項(xiàng)目修改:
1. 數(shù)據(jù)庫(kù)及IO操作都移到工作線程全景,并且設(shè)置線程優(yōu)先級(jí)為THREAD_PRIORITY_BACKGROUND耀石,這樣工作線程最多能獲取到10%的時(shí)間片,優(yōu)先保證主線程執(zhí)行爸黄。
2. 流程梳理滞伟,延后執(zhí)行;
實(shí)際上炕贵,這一步對(duì)項(xiàng)目啟動(dòng)加速最有效果梆奈。通過流程梳理發(fā)現(xiàn)部分流程調(diào)用時(shí)機(jī)偏早、失誤等称开,例如:
更新等操作無需在首屏尚未展示就調(diào)用亩钟,造成資源競(jìng)爭(zhēng);
調(diào)用了IOS為了規(guī)避審核而做的開關(guān),造成網(wǎng)絡(luò)請(qǐng)求密集径荔;
自有統(tǒng)計(jì)在Application的調(diào)用里創(chuàng)建數(shù)量固定為5的線程池督禽,造成資源競(jìng)爭(zhēng),在上圖traceview功能說明圖中最后一行可以看到編號(hào)12執(zhí)行5次总处,耗時(shí)排名前列狈惫;此處線程池的創(chuàng)建是必要但可以延后的。
修改廣告閃屏邏輯為下次生效鹦马。
3.其它優(yōu)化胧谈;
去掉無用但被執(zhí)行的老代碼;
去掉開發(fā)階段使用但線上被執(zhí)行的代碼荸频;
去掉重復(fù)邏輯執(zhí)行代碼菱肖;
去掉調(diào)用三方SDK里或者Demo里的多余代碼;
信息緩存旭从,常用信息只在第一次獲取稳强,之后從緩存中取和悦;
項(xiàng)目是多進(jìn)程架構(gòu)退疫,只在主進(jìn)程執(zhí)行Application的onCreate();
通過以上三步及三方組件的優(yōu)化:Application以及首屏Activity回調(diào)期間主線程就沒有耗時(shí)鸽素、爭(zhēng)搶資源等情況了褒繁。此外還涉及布局優(yōu)化、內(nèi)存優(yōu)化等部分技術(shù)馍忽,因?qū)τ趹?yīng)用冷啟動(dòng)一般不是瓶頸點(diǎn)棒坏,這里不展開詳談,可根據(jù)實(shí)際項(xiàng)目實(shí)際處理遭笋。
六坝冕、對(duì)比效果:
通過ADB命令統(tǒng)計(jì)應(yīng)用的啟動(dòng)時(shí)間:adb shell am start -W 首屏Activity。
同等條件下使用MX3及Nexus6P坐梯,啟動(dòng)5次徽诲,比較優(yōu)化前與優(yōu)化后的啟動(dòng)時(shí)間;
優(yōu)化前:
MX3
ThisTime | TotalTime | WaitTime |
---|---|---|
1237 | 2205 | 2214 |
1280 | 2181 | 2189 |
1622 | 2508 | 2513 |
1485 | 2434 | 2443 |
1442 | 2418 | 2429 |
Nexus6P
ThisTime | TotalTime | WaitTime |
---|---|---|
1229 | 1832 | 1868 |
1268 | 1849 | 1880 |
1184 | 1780 | 1812 |
1262 | 1845 | 1876 |
1164 | 1766 | 1807 |
優(yōu)化后:
MX3
ThisTime | TotalTime | WaitTime |
---|---|---|
865 | 1516 | 1523 |
911 | 1565 | 1573 |
812 | 1406 | 1418 |
962 | 1564 | 1574 |
925 | 1566 | 1577 |
Nexus6P
ThisTime | TotalTime | WaitTime |
---|---|---|
603 | 1192 | 1243 |
614 | 1076 | 1115 |
650 | 1120 | 1163 |
642 | 1107 | 1139 |
624 | 1084 | 1124 |
對(duì)比:
MX3提升35%
對(duì)比 | ThisTime平均數(shù) | TotalTime平均數(shù) | WaitTime平均數(shù) |
---|---|---|---|
優(yōu)化前 | 1413 | 2349 | 2357 |
優(yōu)化后 | 895 | 1523 | 1533 |
Nexus6P提升39%
對(duì)比 | ThisTime平均數(shù) | TotalTime平均數(shù) | WaitTime平均數(shù) |
---|---|---|---|
優(yōu)化前 | 1221 | 1814 | 1848 |
優(yōu)化后 | 626 | 1115 | 1156 |
- 命令含義:
ThisTime:最后一個(gè)啟動(dòng)的Activity的啟動(dòng)耗時(shí)吵血;
TotalTime:自己的所有Activity的啟動(dòng)耗時(shí)谎替;
WaitTime: ActivityManagerService啟動(dòng)App的Activity時(shí)的總時(shí)間(包括當(dāng)前Activity的onPause()和自己Activity的啟動(dòng))。
七蹋辅、問題:
1钱贯、還可以繼續(xù)優(yōu)化的方向?
項(xiàng)目里使用Retrofit網(wǎng)絡(luò)請(qǐng)求庫(kù)侦另,F(xiàn)astConverterFactory做Json解析器秩命,TraceView中看到FastConverterFactory在創(chuàng)建過程中也比較耗時(shí)尉共,考慮將其換為GsonConverterFactory。但是因?yàn)轭惖睦^承關(guān)系短時(shí)間內(nèi)無法直接替換弃锐,作為優(yōu)化點(diǎn)暫時(shí)遺留袄友;
可以考慮根據(jù)實(shí)際情況將啟動(dòng)時(shí)部分接口合并為一,減少網(wǎng)絡(luò)請(qǐng)求次數(shù)霹菊,降低頻率剧蚣;
相同功能的組件只保留一個(gè),例如:友盟旋廷、GrowingIO鸠按、自有統(tǒng)計(jì)等功能重復(fù);
使用ReDex 進(jìn)行優(yōu)化饶碘;實(shí)驗(yàn)Redex發(fā)現(xiàn)Apk體積確實(shí)是小了一點(diǎn)目尖,但是啟動(dòng)速度沒有變化,或許需要繼續(xù)研究扎运。
2瑟曲、異步、延遲初始化及操作的依據(jù)绪囱?
注意一點(diǎn):并不是每一個(gè)組件的初始化以及操作都可以異步或延遲测蹲;是否可以取決組件的調(diào)用關(guān)系以及自己項(xiàng)目具體業(yè)務(wù)的需要。保證一個(gè)準(zhǔn)則:可以異步的都異步鬼吵,不可以異步的盡量延遲。讓應(yīng)用先啟動(dòng)篮赢,再操作齿椅。
3、通用應(yīng)用啟動(dòng)加速套路启泣?
利用主題快速顯示界面涣脚;
異步初始化組件;
梳理業(yè)務(wù)邏輯寥茫,延遲初始化組件遣蚀、操作;
正確使用線程纱耻;
去掉無用代碼芭梯、重復(fù)邏輯等。
4弄喘、其它
將啟動(dòng)速度加快了35%不代表之前的代碼都是問題玖喘,從業(yè)務(wù)角度上將,代碼并沒有錯(cuò)誤蘑志,實(shí)現(xiàn)了業(yè)務(wù)需求累奈。但是在啟動(dòng)時(shí)這個(gè)注重速度的階段贬派,忽略的細(xì)節(jié)就會(huì)導(dǎo)致性能的瓶頸。
-
開發(fā)過程中澎媒,對(duì)核心模塊與應(yīng)用階段如啟動(dòng)時(shí)搞乏,使用TraceView進(jìn)行分析,盡早發(fā)現(xiàn)瓶頸戒努。
360°Android app全方位性能調(diào)優(yōu).png