隨著App的邏輯不斷龐大,一不注意就會將耗時的操作放置在應(yīng)用啟動過程之中骨宠,導(dǎo)致應(yīng)用啟動速度越來越慢,用戶體驗(yàn)也越來越差。優(yōu)化啟動速度是幾乎所有大型App應(yīng)用開發(fā)者需要考慮的問題挟憔。優(yōu)化啟動速度之前首先需要準(zhǔn)確測量App啟動時間,這樣有利于我們更準(zhǔn)確可量化地看出優(yōu)化效果烟号,也可以指導(dǎo)我們進(jìn)行持續(xù)優(yōu)化曲楚。轉(zhuǎn)載請注明出處:Lawrence_Shen
同時可以參考2019年的性能分析文章:Android性能分析&啟動優(yōu)化
- 使用命令行方式
使用命令行方式統(tǒng)計(jì)多次啟動某個Activity的平均用時可以在shell中執(zhí)行如下指令:
adb shell am start -S -R 10 -W com.example.app/.MainActivity
其中-S
表示每次啟動前先強(qiáng)行停止,-R
表示重復(fù)測試次數(shù)褥符。每一次的輸出如下所示信息龙誊。
Stopping: com.example.app
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.example.app/.MainActivity }
Status: ok
Activity: com.example.app/.MainActivity
ThisTime: 1059
TotalTime: 1059
WaitTime: 1073
Complete
其中TotalTime
代表當(dāng)前Activity啟動時間,將多次TotalTime
加起來求平均即可得到啟動這個Activity的時間喷楣。
缺點(diǎn)
- 應(yīng)用的啟動過程往往不只一個Activity趟大,有可能是先進(jìn)入一個啟動頁鹤树,然后再從啟動頁打開真正的首頁。某些情況下還有可能中間經(jīng)過更多的Activity逊朽,這個時候需要將多個Activity的時間加起來罕伯。
- 將多個Activity啟動時間加起來并不完全等于用戶感知的啟動時間。例如在啟動頁可能是先等待某些初始化完成或者某些動畫播放完畢后再進(jìn)入首頁叽讳。使用命令行統(tǒng)計(jì)的方式只是計(jì)算了Activity的啟動以及初始化時間追他,并不能體現(xiàn)這種等待任務(wù)的時間。
- 沒有在AndroidManifest.xml對應(yīng)的Activity聲明中指定
<intent-filter>
或者屬性沒有android:exported="true"
的Activity不能使用這種命令行的形式計(jì)算啟動時間岛蚤。
-思考更準(zhǔn)確的方式
以上基于命令行的方式存在諸多問題邑狸,迫使我們思考怎樣才能得到從用戶角度上觀察更準(zhǔn)確的啟動時間。在嘗試其他方法之前涤妒,我們先定義一下怎樣才是從用戶角度上觀察的啟動時間单雾。
冷啟動、熱啟動(注意不是官方的定義她紫,是我們從用戶角度考慮的定義)
- 冷啟動時間:冷啟動表示用戶首次打開應(yīng)用硅堆,這時進(jìn)程還沒創(chuàng)建,包含了Application創(chuàng)建的過程贿讹。冷啟動時間指從第一次用戶點(diǎn)擊Launcher中的應(yīng)用圖標(biāo)開始渐逃,到首頁內(nèi)容全部展示出來的時間。
- 熱啟動時間:熱啟動表示用戶在首頁按了返回民褂,首頁Activity已經(jīng)Destroy茄菊,不過Application仍在內(nèi)存中存在,對應(yīng)的進(jìn)程并沒有被殺掉助赞,不包含Application創(chuàng)建過程买羞。熱啟動時間指在Application仍然存在的情況下,從用戶點(diǎn)擊桌面圖標(biāo)雹食,到首頁內(nèi)容全部展示出來的時間畜普。
App啟動流程
要優(yōu)化以及分析啟動時間,需要先了解App的啟動流程群叶。以冷啟動為例子吃挑,Application以及Activity的啟動流程如下,參考文章[3][4][5][6]:
更為直觀和簡單的流程圖參考Colt McAnlis在Android Performance Patterns Season 6中的表述街立。有興趣的同學(xué)可以點(diǎn)擊鏈接看看(Youtube鏈接)舶衬。
從流程圖以及參考Colt McAnlis的Android Performance Patterns[6]得知,在冷啟動的過程中赎离,首先會通過AMS在System進(jìn)程展示一個Starting Window(通常情況下是個白屏逛犹,可以通過設(shè)置Application的theme修改),接著AMS會通過Zygote創(chuàng)建應(yīng)用程序的進(jìn)程,并通過一系列的步驟后調(diào)用Application的attachBaseContext()
虽画、onCreate()
然后最終調(diào)用Activity的onCreate()
以及進(jìn)行View相關(guān)的初始化工作舞蔽。在Activity展示出來后會替換掉之前的Starting Window,這樣啟動過程結(jié)束码撰。
如何加log
參考[1]發(fā)現(xiàn)在Activity中onWindowFocusChanged()
方法是最好的Activity對用戶可見的標(biāo)志渗柿,因此綜合上一節(jié)的分析,我們可以考慮在Application的attachBaseContext()
方法中開始計(jì)算冷啟動計(jì)時脖岛,然后在真正首頁Activity的onWindowFocusChanged()
中停止冷啟動計(jì)時朵栖,這樣就可以初步得到應(yīng)用的冷啟動時間。
public void onWindowFocusChanged(boolean hasFocus)
Called when the current
android.view.Window
of the activity gains or loses focus. This is the best indicator of whether this activity is visible to the user.
為了方便統(tǒng)計(jì)柴梆,設(shè)置一個Util類專門做計(jì)時陨溅,添加的代碼如下:
/**
* 計(jì)時統(tǒng)計(jì)工具類
*/
public class TimeUtils {
private static HashMap<String, Long> sCalTimeMap = new HashMap<>();
public static final String COLD_START = "cold_start";
public static final String HOT_START = "hot_start";
public static long sColdStartTime = 0;
/**
* 記錄某個事件的開始時間
* @param key 事件名稱
*/
public static void beginTimeCalculate(String key) {
long currentTime = System.currentTimeMillis();
sCalTimeMap.put(key, currentTime);
}
/**
* 獲取某個事件的運(yùn)行時間
*
* @param key 事件名稱
* @return 返回某個事件的運(yùn)行時間,調(diào)用這個方法之前沒有調(diào)用 {@link #beginTimeCalculate(String)} 則返回-1
*/
public static long getTimeCalculate(String key) {
long currentTime = System.currentTimeMillis();
Long beginTime = sCalTimeMap.get(key);
if (beginTime == null) {
return -1;
} else {
sCalTimeMap.remove(key);
return currentTime - beginTime;
}
}
/**
* 清除某個時間運(yùn)行時間計(jì)時
*
* @param key 事件名稱
*/
public static void clearTimeCalculate(String key) {
sCalTimeMap.remove(key);
}
/**
* 清除啟動時間計(jì)時
*/
public static void clearStartTimeCalculate() {
clearTimeCalculate(HOT_START);
clearTimeCalculate(COLD_START);
sColdStartTime = 0;
}
}
然后在Application的attachBaseContext()
方法中添加如下代碼:
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
if (/**如果是主進(jìn)程**/) {
TimeUtils.beginTimeCalculate(TimeUtils.COLD_START);
}
}
在第一個Activity的onCreate()
方法中添加如下代碼:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
calculateStartTime();
....
}
private void calculateStartTime() {
long coldStartTime = TimeUtils.getTimeCalculate(TimeUtils.COLD_START);
// 這里記錄的TimeUtils.coldStartTime是指Application啟動的時間轩性,最終的冷啟動時間等于Application啟動時間+熱啟動時間
TimeUtils.sColdStartTime = coldStartTime > 0 ? coldStartTime : 0;
TimeUtils.beginTimeCalculate(DictTimeUtil.HOT_START);
}
在真正的首頁Activity的 onWindowFocusChanged()
方法中添加如下代碼:
@Override
public void onWindowFocusChanged(boolean hasFocus) {
if (hasFocus && /**沒有經(jīng)過廣告或者引導(dǎo)頁**/) {
long hotStartTime = TimeUtils.getTimeCalculate(TimeUtils.HOT_START);
if (TimeUtils.sColdStartTime > 0 && hotStartTime > 0) {
// 真正的冷啟動時間 = Application啟動時間 + 熱啟動時間
long coldStartTime = TimeUtils.sColdStartTime + hotStartTime;
// 過濾掉異常啟動時間
if (coldStartTime < 50000) {
// 上傳冷啟動時間coldStartTime
}
} else if (hotStartTime > 0) {
// 過濾掉異常啟動時間
if (hotStartTime < 30000) {
// 上傳熱啟動時間hotStartTime
}
}
}
}
避免坑的Checklist
上面的分析給了我們初步的加log的起始和結(jié)束點(diǎn)声登,然而在實(shí)際的統(tǒng)計(jì)中會發(fā)現(xiàn)得到的數(shù)據(jù)有20%左右是不準(zhǔn)確的狠鸳,體現(xiàn)在計(jì)時數(shù)據(jù)非常大揣苏,有些甚至?xí)@示冷啟動時間超過一天。經(jīng)過分析件舵,在計(jì)算啟動計(jì)時的時候需要注意一些問題卸察。以下列舉一下添加log時候需要注意的checklist。
應(yīng)用在啟動過程可能會有廣告(我們的業(yè)務(wù)是有道詞典)铅祸,第一次啟動會有引導(dǎo)頁坑质,需要根據(jù)業(yè)務(wù)情況標(biāo)記在沒有廣告、沒有引導(dǎo)頁的時候才計(jì)算临梗。這種情況要注意在非正常啟動的時候忽略啟動時間統(tǒng)計(jì)涡扼。
由于詞典首頁之前還有幾個Activity,在沒到首頁Activity之前如果過早的返回盟庞,會出現(xiàn)冷啟動時間過長的問題吃沪。這是因?yàn)樵~典返回的時候并沒有殺掉進(jìn)程,而時間統(tǒng)計(jì)信息是保存在內(nèi)存中的什猖,而等下次再進(jìn)入的時候因?yàn)槭菬釂硬粫匦麻_始冷啟動計(jì)時票彪。這導(dǎo)致了這次熱啟動實(shí)際上打log的時候發(fā)現(xiàn)有上次冷啟動的開始時間,算成了冷啟動不狮,而且因?yàn)閱訒r間是上一次的降铸,所以這次冷啟動log的時間比實(shí)際時間長。這種情況要注意在首頁Activity之前的其他Activity
onPause()
方法中調(diào)用TimeUtils.clearStartTimeCalculate();
清除計(jì)時摇零。除了正常的啟動流程推掸,應(yīng)用還有很多可能會導(dǎo)致Application的創(chuàng)建的入口,例如點(diǎn)擊桌面小插件、系統(tǒng)賬號同步谅畅、Deep Link跳轉(zhuǎn)俊嗽、直接進(jìn)入設(shè)置了
<action android:name="android.intent.action.PROCESS_TEXT" />
的Activity、push達(dá)到等铃彰。我們需要檢查所有有可能引起Application創(chuàng)建绍豁,但是不是正常啟動流程的地方,調(diào)用TimeUtils.clearStartTimeCalculate();
清除計(jì)時牙捉,避免引起冷啟動時間計(jì)算過長錯誤的問題竹揍。
- 使用第三方工具
為了測試啟動的過程中哪些方法比較耗時,我們可以使用Android Studio中集成的Android Monitor提供的Method Tracering或者Systrace邪铲。不過在實(shí)踐中發(fā)現(xiàn)芬位,有另外一個nimbledroid工具使用更加簡便且能更明確指出耗時的地方。上傳了應(yīng)用之后會自動分析情景如下圖所示带到。其中會自動檢測出首頁的Activity并且給出冷啟動的啟動情況昧碉。
點(diǎn)擊進(jìn)入Cold Startup的情景可以看到主要耗時的方法如下圖。
至于為什么nimbledroid會知道那個是我們首頁的Activity揽惹,官網(wǎng)上解析如下:
We use a heuristic to tell when an app finishes startup by detecting when (1) the main Activity has been displayed and (2) things like animated progress bars in the main Activity have stopped. Based on our experiments, this heuristic works in most cases.
點(diǎn)擊進(jìn)入某個方法被饿,可以看到這個方法具體是由于調(diào)用了哪個子方法導(dǎo)致了耗時的問題。
通過nimbledroid這個工具搪搏,我們可以比較輕松地發(fā)現(xiàn)一些比較明顯的問題狭握,并可以指導(dǎo)我們進(jìn)行啟動優(yōu)化。同時nimbledroid還支持Memory Leaks疯溺、網(wǎng)絡(luò)監(jiān)測以及結(jié)果分享等一些功能论颅,更多的功能有待讀者繼續(xù)發(fā)現(xiàn)。
- 后記
統(tǒng)計(jì)和分析啟動時間有利于指導(dǎo)我們優(yōu)化啟動時間囱嫩。以上介紹了有道詞典在進(jìn)行啟動優(yōu)化中的分析過程恃疯。通過詳細(xì)了解Android應(yīng)用啟動的流程,進(jìn)行準(zhǔn)確的log記錄墨闲,并且結(jié)合第三方工具今妄,我們最終得到準(zhǔn)確的啟動時間統(tǒng)計(jì)數(shù)據(jù)以及啟動優(yōu)化的一些頭緒。具體優(yōu)化的方法參加下一篇文章《如何優(yōu)化Androd App啟動速度》损俭。
- 參考
【1】單刀土豆蛙奖,2016.Android 開發(fā)之 App 啟動時間統(tǒng)計(jì)
【2】Android Developer,Launch-Time Performance
【3】./multi_core_dump杆兵,2010.Android Application Launch
【4】./multi_core_dump雁仲,2010.Android Application Launch Part 2
【5】羅升陽,2012.Android系統(tǒng)源代碼情景分析
【6】Colt McAnlis琐脏,2016.Android Performance Patterns Season 6