應(yīng)用啟動(dòng)的時(shí)間作為應(yīng)用的門(mén)面,重要性可想而知躺酒。尤其在如今的快時(shí)代,一款秒開(kāi)的App比一款啟動(dòng)需要耗費(fèi)好幾秒的App更容易被用戶喜愛(ài)和長(zhǎng)期使用,整的不好還容易被用戶永久拉入黑名單轩勘。這時(shí)候,應(yīng)用的啟動(dòng)優(yōu)化就必不可少了怯邪。那么接下來(lái)就來(lái)了解下關(guān)于啟動(dòng)優(yōu)化的一些注意事項(xiàng)绊寻。
一、應(yīng)用啟動(dòng)類型
1.1.冷啟動(dòng)
冷啟動(dòng)是指應(yīng)用程序從零開(kāi)始悬秉,系統(tǒng)的進(jìn)程在此啟動(dòng)之前沒(méi)有創(chuàng)建應(yīng)用程序的進(jìn)程澄步,或者由于系統(tǒng)殺死了應(yīng)用后再啟動(dòng)。在冷啟動(dòng)開(kāi)始時(shí)和泌,系統(tǒng)有三個(gè)任務(wù)村缸。這些任務(wù)包括:
- 加載并啟動(dòng)應(yīng)用程序。
- 啟動(dòng)后立即顯示一個(gè)空白的啟動(dòng)窗口武氓。
- 創(chuàng)建應(yīng)用程序app進(jìn)程梯皿。
一旦app進(jìn)程創(chuàng)建完成,系統(tǒng)就開(kāi)始下一階段:
- 創(chuàng)建app對(duì)象县恕;
- 啟動(dòng)主線程东羹。
- 創(chuàng)建主Activity
- 開(kāi)始對(duì)View進(jìn)行布局。
1.2.熱啟動(dòng)
熱啟動(dòng)不同于冷啟動(dòng)忠烛,熱啟動(dòng)在啟動(dòng)應(yīng)用時(shí)属提,系統(tǒng)中已經(jīng)有了該應(yīng)用的進(jìn)程,啟動(dòng)時(shí)也就少了創(chuàng)建進(jìn)程等一系列耗時(shí)的操作。
1.3.溫啟動(dòng)
溫啟動(dòng)的啟動(dòng)速度處于冷啟動(dòng)和熱啟動(dòng)之間冤议,溫啟動(dòng)會(huì)重新走Activity的onCreate生命周期斟薇。
二、應(yīng)用啟動(dòng)流程
優(yōu)化應(yīng)用的啟動(dòng)速度主要是在于冷啟動(dòng)時(shí)恕酸,應(yīng)用的啟動(dòng)耗時(shí)堪滨。冷啟動(dòng)時(shí)應(yīng)用會(huì)從零開(kāi)啟,這就需要先了解一下當(dāng)我們點(diǎn)擊Launcher上app圖標(biāo)后尸疆,進(jìn)程之間做了什么處理椿猎?
2.1.啟動(dòng)基本流程
- 點(diǎn)擊App圖標(biāo),Launcher進(jìn)程向SystemServer進(jìn)程發(fā)起startActivity請(qǐng)求寿弱;
- SystemServer進(jìn)程收到請(qǐng)求后犯眠,向Zygote進(jìn)程發(fā)送創(chuàng)建App進(jìn)程的請(qǐng)求;
- Zygote進(jìn)程fork出新的App進(jìn)程症革,App進(jìn)程就開(kāi)始向SystemServer進(jìn)程發(fā)出attachApplication請(qǐng)求筐咧,在此同時(shí),App進(jìn)程會(huì)執(zhí)行bindApplication噪矛,即創(chuàng)建Application量蕊,調(diào)用Application的onCreate;
- SystemServer在收到attachApplication請(qǐng)求后艇挨,再次向App進(jìn)程發(fā)送scheduleLauncherActivity請(qǐng)求残炮;
- App進(jìn)程收到請(qǐng)求后,通過(guò)handler向主線程發(fā)送LAUNCHE_ACTIVITY消息缩滨,主線程收到消息后通過(guò)反射機(jī)制創(chuàng)建目標(biāo)Activity势就,并回調(diào)Activity.OnCreate()等方法。到此脉漏,App才正式啟動(dòng)苞冯,開(kāi)始Activity的生命周期。
從應(yīng)用的啟動(dòng)流程可以發(fā)現(xiàn)侧巨,應(yīng)用的啟動(dòng)其實(shí)是App進(jìn)程與SystemServer進(jìn)程舅锄,Zygote進(jìn)程相互配合的過(guò)程。對(duì)于啟動(dòng)速度的優(yōu)化司忱,應(yīng)用層我們所要關(guān)注并且能夠干預(yù)的也就是Application和Activity的創(chuàng)建皇忿。
2.2.Application
App運(yùn)行時(shí),會(huì)首先自動(dòng)創(chuàng)建Application類并實(shí)例化 Application對(duì)象坦仍,且只有一個(gè)禁添。Application的創(chuàng)建時(shí)間比Activity要早,在上面應(yīng)用的啟動(dòng)流程中也提到桨踪,在啟動(dòng)App時(shí),會(huì)創(chuàng)建Application芹啥,那么就需要先去了解下它的生命周期锻离。
- attachBaseContext():得到應(yīng)用上下文的Context铺峭,在應(yīng)用創(chuàng)建時(shí)會(huì)首先調(diào)用;
- onCreate():同樣在應(yīng)用創(chuàng)建時(shí)調(diào)用汽纠,但比attachBaseContext()要晚卫键;
- onTerminate():應(yīng)用結(jié)束時(shí)調(diào)用;
- onConfigurationChange():系統(tǒng)配置發(fā)生變化時(shí)調(diào)用虱朵;
- onLowMemory():系統(tǒng)低內(nèi)存時(shí)調(diào)用莉炉;
- onTrimMemory():系統(tǒng)要求應(yīng)用釋放內(nèi)存時(shí)調(diào)用。
從Application的生命周期可以看到碴犬,應(yīng)用創(chuàng)建時(shí)會(huì)依次調(diào)用attachBaseContext()和onCreate()絮宁,這兩個(gè)生命周期包含在應(yīng)用的啟動(dòng)流程中,啟動(dòng)速度優(yōu)化可以以此為一個(gè)切入點(diǎn)服协。
2.3.Activity
從點(diǎn)擊圖標(biāo)到用戶看見(jiàn)前臺(tái)數(shù)據(jù)所經(jīng)歷的生命周期绍昂,也就是Activity的onCreate(),onResume()。眾所周知偿荷,Activity會(huì)在onCreate()中加載布局以及進(jìn)行數(shù)據(jù)的初始化窘游。既然包含在應(yīng)用的啟動(dòng)中,那么也可以作為一個(gè)切入點(diǎn)跳纳。
2.4.小結(jié)
從上面的啟動(dòng)流程到Application和Activity的介紹忍饰,應(yīng)用啟動(dòng)優(yōu)化的切入點(diǎn)也就如下圖所示:
作為應(yīng)用層所能監(jiān)控并且能處理的,第一點(diǎn)是屬于Application的創(chuàng)建寺庄,第二點(diǎn)就是Activity的創(chuàng)建艾蓝。那么接下來(lái)就需要去監(jiān)測(cè)各個(gè)部分所耗費(fèi)的時(shí)間,再針對(duì)性的進(jìn)行優(yōu)化铣揉。
三饶深、啟動(dòng)耗時(shí)的監(jiān)測(cè)
3.1.logcat生成所有l(wèi)og
在連接上設(shè)備后可以在串口或者adb中利用命令打印出所有的log:
- 生成log文件:logcat > /data/xxx.txt
- 利用adb將文件pull出來(lái):adb pull /data/xxx.txt
執(zhí)行完上面兩條命令后,就會(huì)生成一個(gè)全log的txt文件逛拱,pull出文件后敌厘,可以在文件中查看所有的信息,包括啟動(dòng)發(fā)生的時(shí)間朽合,類名俱两,線程號(hào)等。
logcat生成的xxx.txt雖然包含了很詳細(xì)的信息曹步,但是還需要我們自己去計(jì)算各個(gè)生命周期間所耗費(fèi)的時(shí)間宪彩。
3.2.adb命令執(zhí)行
除了3.1提到的用logcat打印全log外,adb還有一條命令可以直接生成應(yīng)用啟動(dòng)的時(shí)間讲婚。<u>adb shell am start -W [packageName]/[AppstartActivity]</u>
執(zhí)行完后臺(tái)會(huì)生成ThisTime尿孔、TotalTime和WaitTime這三個(gè)時(shí)間,ThisTime代表一連串啟動(dòng) Activity 的最后一個(gè) Activity 的啟動(dòng)耗時(shí);TotalTime表示應(yīng)用的啟動(dòng)時(shí)間活合,包括創(chuàng)建進(jìn)程雏婶,Application初始化和Activity初始化到界面顯示,一般來(lái)說(shuō)與ThisTime一樣白指;WaitTime則表示AMS啟動(dòng)Activity的總耗時(shí)留晚,一般比TotalTime大。
對(duì)于監(jiān)測(cè)應(yīng)用的啟動(dòng)速度告嘲,我們只需要關(guān)注TotalTime這個(gè)值错维。
如上所述,利用adb命令得到啟動(dòng)時(shí)間橄唬,也只是一個(gè)階段的總時(shí)間赋焕,卻不能如3.1一樣監(jiān)測(cè)到每個(gè)生命周期所耗費(fèi)的時(shí)間,無(wú)法得到具體的耗時(shí)轧坎,無(wú)疑對(duì)啟動(dòng)速度針對(duì)性優(yōu)化沒(méi)有多大的幫助宏邮。
3.3.代碼打點(diǎn)
代碼打點(diǎn)是通過(guò)代碼編寫(xiě)一個(gè)工具類,通過(guò)代碼的形式獲取每個(gè)方法的執(zhí)行的時(shí)間缸血,這個(gè)方法與3.1所達(dá)到的目的是一致的蜜氨,都能得到每個(gè)周期具體的耗時(shí)。唯一不同的是3.1的方式需要不斷敲擊命令捎泻,再在文件中去查找有效信息飒炎,無(wú)疑是耗費(fèi)人力的,而通過(guò)代碼打點(diǎn)的方式可以實(shí)時(shí)監(jiān)測(cè)每個(gè)方法的耗時(shí)笆豁,也可以生成信息上傳到服務(wù)器郎汪。
下面是一個(gè)基本的代碼打點(diǎn)的案例:
public class TimeMonitorManager {
private static final String TAG = "TimeMonitorManager";
private HashMap<String, Long> mTimeTagMap = new HashMap<>();
private long mStartTime = 0;
private static volatile TimeMonitorManager mMonitorManager;
private TimeMonitorManager() {
}
public static TimeMonitorManager getInstance() {
if (mMonitorManager == null) {
synchronized (TimeMonitorManager.class) {
if (mMonitorManager == null) {
mMonitorManager = new TimeMonitorManager();
}
}
}
return mMonitorManager;
}
/**
* 開(kāi)始監(jiān)聽(tīng).
*/
public void startMonitor() {
if (mTimeTagMap.size() > 0) {
mTimeTagMap.clear();
}
mStartTime = System.currentTimeMillis();
}
/**
* 結(jié)束監(jiān)聽(tīng).
* @param tag 所要打印的tag.
*/
public void endMonitor(String tag) {
if (mTimeTagMap.get(tag) != null) {
mTimeTagMap.remove(tag);
}
long time = System.currentTimeMillis() - mStartTime;
mTimeTagMap.put(tag, time);
showData();
}
private void showData() {
if (mTimeTagMap.size() <= 0) {
return;
}
for (String tag: mTimeTagMap.keySet()
) {
long time = mTimeTagMap.get(tag);
Log.d(TAG, tag + ": " + time);
}
}
}
在需要打點(diǎn)開(kāi)始的地方調(diào)用startMonitor(),在結(jié)束的地方調(diào)用endMonitor(String tag),例如Activity在加載布局,也就是setContentView(R.layout.activity_main)時(shí)是耗時(shí)的,根據(jù)打點(diǎn)規(guī)則,在setContentView(R.layout.activity_main);前后調(diào)用TimeMonitorManager的方法,以達(dá)到監(jiān)測(cè)setContentView所耗費(fèi)的時(shí)間.如下:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TimeMonitorManager.getInstance().startMonitor();
setContentView(R.layout.activity_main);
TimeMonitorManager.getInstance().endMonitor(TAG + " onCreate setContentView");
}
運(yùn)行后Logcat中打印的效果如下:
TimeMonitorManager: MainActivity onCreate setContentView: 60
可以發(fā)現(xiàn),Activity在setContentView()時(shí)所耗費(fèi)的時(shí)間大約為60ms.
利用TimeMonitorManager打點(diǎn)的好處在于可以清楚的監(jiān)測(cè)到每一處的耗時(shí),更加精準(zhǔn);
從第二節(jié)應(yīng)用啟動(dòng)流程就了解到,應(yīng)用啟動(dòng)耗時(shí)能夠監(jiān)測(cè)的切入點(diǎn)為Application的創(chuàng)建和Activity的創(chuàng)建闯狱,我們就可以通過(guò)代碼打點(diǎn)的方式在以下這些地方進(jìn)行打點(diǎn):
- Application的onCreate()
- Activity的onCreate()煞赢,onStart(),onResume()
- 初始化對(duì)象的方法,注冊(cè)等方法
- 一些耗時(shí)的操作
四哄孤、優(yōu)化方向
從上面三節(jié)可以了解到照筑,影響應(yīng)用啟動(dòng)時(shí)間的要素一般可分為Application里一些數(shù)據(jù)的準(zhǔn)備,Activity的布局以及在初始化的耗時(shí)操作瘦陈。下面也將從這幾個(gè)方面分別描述下優(yōu)化策略凝危。
4.1.布局優(yōu)化
4.1.1.按需選擇布局方式
我們都知道onCreate()里的setContentView()是用來(lái)加載布局,應(yīng)用啟動(dòng)時(shí)會(huì)去解析xml中的結(jié)構(gòu)晨逝,當(dāng)一個(gè)xml里結(jié)構(gòu)嵌套過(guò)多蛾默,系統(tǒng)需要去解析的時(shí)間就大大增加了。
當(dāng)布局比較復(fù)雜捉貌,可以使用ConstraintLayout布局支鸡,ConstraintLayout是Android Studio 2.2新增的一個(gè)功能冬念,它的一大特點(diǎn)就是為了解決布局嵌套。具體使用方法可參考:
https://blog.csdn.net/sinyu890807/article/details/53122387
另外牧挣,當(dāng)版本較低且布局復(fù)雜刘急,RelativeLayout布局優(yōu)化的效果是要優(yōu)于LinearLayout。但是當(dāng)布局簡(jiǎn)單時(shí)浸踩,LinearLayout卻優(yōu)于RelativeLayout,所以大家可依照具體情況進(jìn)行選擇统求。
4.1.2.< include >检碗、< merge >
< include >與< merge >是布局優(yōu)化的兩個(gè)利器。< include >標(biāo)簽是可以允許在一個(gè)布局當(dāng)中引入另外一個(gè)布局码邻,當(dāng)多個(gè)布局中有用到相同的部分折剃,就可以采用< include >標(biāo)簽將相同的部分提取出來(lái),利用< include >將公告部分替代像屋。
而< merge >標(biāo)簽的作用是作為< include >標(biāo)簽的一種輔助擴(kuò)展來(lái)使用的怕犁,它的主要作用是為了防止在引用布局文件時(shí)產(chǎn)生多余的布局嵌套。
4.1.3.ViewStub
ViewStub是一個(gè)比較輕量級(jí)的控件己莺,沒(méi)有大小奏甫,不需要繪制,同時(shí)也不參與布局凌受,所以消耗的資源是非常小的阵子。ViewStub的使用就在于當(dāng)我們存在時(shí)而需要顯示時(shí)而不顯示的view的時(shí)候,就可以使用它胜蛉。例如我們進(jìn)行網(wǎng)絡(luò)請(qǐng)求時(shí)的loading bar挠进,請(qǐng)求時(shí),會(huì)顯示誊册,當(dāng)請(qǐng)求結(jié)束领突,loading bar就會(huì)消失,這個(gè)時(shí)候就可以使用ViewStub案怯,減少資源的消耗的同時(shí)也減少了布局解析的時(shí)間君旦。
另外,針對(duì)布局的優(yōu)化殴泰,AS提供了Profile和Hierarchy View兩個(gè)工具分別檢查View的繪制和分析布局于宙。
具體使用可參考
https://blog.csdn.net/guolin_blog/article/details/43376527
4.2.邏輯加載優(yōu)化
邏輯耗時(shí)一般分為Application中和Activity中的邏輯加載。在Application或者Activity中進(jìn)行初始化的時(shí)候悍汛,有些的邏輯初始化是必要的捞魁,而有些初始化非必要,可以適當(dāng)?shù)难訒r(shí)去加載离咐。例如在最近的項(xiàng)目中谱俭,應(yīng)用啟動(dòng)時(shí)需要提前去連接服務(wù)并且去注冊(cè)回調(diào)接口奉件,為了提前連接上服務(wù),將連接的操作就放置在了Application里進(jìn)行初始化昆著。這就是屬于必要的邏輯操作县貌。對(duì)于不同優(yōu)先級(jí)的邏輯,我們可以大致分為以下幾點(diǎn):
4.2.1.異步加載(必要且耗時(shí))
有些邏輯處理的優(yōu)先級(jí)比較高凑懂,并且初始化耗時(shí)煤痕,可以采用異步加載的方式,利用RxJava接谨,HandlerThread摆碉,IntentService等在后臺(tái)進(jìn)行加載,這樣就不會(huì)阻塞主線程脓豪,UI展現(xiàn)到用戶眼前的時(shí)間也會(huì)縮短巷帝。
4.2.2.延時(shí)加載(非必要且耗時(shí))
當(dāng)邏輯操作的優(yōu)先級(jí)不是很高時(shí),可以采取延時(shí)加載的方式扫夜,也就是應(yīng)用啟動(dòng)過(guò)程中暫時(shí)不去初始化這些邏輯楞泼,將之前在Application或者Activity onCreate()中的操作移除,在主線程空閑的時(shí)候再進(jìn)行加載操作笤闯。
另外堕阔,MessageQueue內(nèi)部有一個(gè)接口IdleHandler,可以很好的處理延時(shí)問(wèn)題望侈。IdleHandler在looper里面的message都處理完了的時(shí)候就會(huì)回調(diào)這個(gè)接口印蔬,返回false,就會(huì)移除它脱衙,返回true就會(huì)在下次message處理完了的時(shí)候繼續(xù)回調(diào)侥猬。
舉個(gè)例子,一般情況下捐韩,應(yīng)用啟動(dòng)時(shí)會(huì)去繪制布局退唠,會(huì)去調(diào)用measure, layout, draw等方法,在執(zhí)行這些操作后荤胁,用戶才會(huì)去看見(jiàn)UI瞧预,而之前優(yōu)先級(jí)不高的初始化,就可以延時(shí)在這些操作后加載仅政,那么主要的問(wèn)題就是我們?nèi)绾稳ヅ袛鄊easure, layout, draw等操作已經(jīng)完成了垢油?延時(shí)加載的時(shí)機(jī)在哪?IdleHandler就幫我們解決了這個(gè)問(wèn)題圆丹,上面也都知道IdleHandler是在隊(duì)列為空的時(shí)候會(huì)去回調(diào)它滩愁,measure, layout, draw都可以作為一個(gè)個(gè)message,IdleHandler就會(huì)在他們執(zhí)行完成后響應(yīng)辫封。這個(gè)時(shí)候就可以進(jìn)行之前需要延時(shí)的初始化操作硝枉。
另外廉丽,當(dāng)代碼中同時(shí)有UI繪制和邏輯加載,可以在IdleHandler回調(diào)中再去處理邏輯加載妻味,UI繪制與邏輯分開(kāi)操作正压,可以減少數(shù)據(jù)空白時(shí)間長(zhǎng)的問(wèn)題。
4.2.3.分步加載
當(dāng)初始化對(duì)象有很多時(shí)责球,且必要焦履,可以采取分步加載的方式,將邏輯的優(yōu)先級(jí)區(qū)分開(kāi)來(lái)雏逾,優(yōu)先級(jí)高的先加載裁良。
五、總結(jié)
啟動(dòng)優(yōu)化需要針對(duì)不同的業(yè)務(wù)做出不同的優(yōu)化方式校套,例如可以采用Multidex預(yù)加載優(yōu)化,但是虛擬機(jī)在5.0以上默認(rèn)就使用ART牧抵,對(duì)于項(xiàng)目是5.0以上版本就不需要去優(yōu)化此方面笛匙。
總的來(lái)說(shuō),優(yōu)化方向可以分為布局優(yōu)化犀变,減少解析xml和繪制的時(shí)間妹孙;邏輯優(yōu)化,將必要且耗時(shí)的操作異步加載获枝,將非必要的采用延時(shí)加載蠢正,另外,將操作優(yōu)先級(jí)高的可以優(yōu)先加載省店。
最后嚣崭,啟動(dòng)速度優(yōu)化是一個(gè)大工程,后續(xù)還需要針對(duì)具體場(chǎng)景繼續(xù)深度挖掘懦傍。
參考:《Android應(yīng)用性能優(yōu)化最佳實(shí)踐》