一、概述
手機(jī)桌面點(diǎn)擊一個(gè)應(yīng)用,用戶(hù)希望應(yīng)用能及時(shí)響應(yīng)、快速加載。啟動(dòng)時(shí)間過(guò)長(zhǎng)的應(yīng)用可能會(huì)令用戶(hù)失望。這種糟糕的體驗(yàn)可能會(huì)導(dǎo)致用戶(hù)在 Play 商店針對(duì)您的應(yīng)用給出很低的評(píng)分,甚至完全棄用您的應(yīng)用。
本篇就來(lái)講解如何分析和優(yōu)化應(yīng)用的啟動(dòng)時(shí)間。首先介紹啟動(dòng)過(guò)程機(jī)制,然后討論如何檢測(cè)啟動(dòng)時(shí)間以及分析工具,最后給出通用啟動(dòng)優(yōu)化方案。
二、應(yīng)用啟動(dòng)流程介紹
根據(jù)官方文檔,應(yīng)用有三種啟動(dòng)狀態(tài):冷啟動(dòng)、溫啟動(dòng)、熱啟動(dòng)。
冷啟動(dòng) 冷啟動(dòng)是指應(yīng)用從頭開(kāi)始啟動(dòng):系統(tǒng)進(jìn)程在冷啟動(dòng)后才創(chuàng)建應(yīng)用進(jìn)程。發(fā)生冷啟動(dòng)的情況包括應(yīng)用自設(shè)備啟動(dòng)后或系統(tǒng)終止應(yīng)用后首次啟動(dòng)。例如,通過(guò)任務(wù)列表手動(dòng)殺掉應(yīng)用進(jìn)程后,又重新啟動(dòng)應(yīng)用。
熱啟動(dòng) 熱啟動(dòng)比冷啟動(dòng)簡(jiǎn)單得多,開(kāi)銷(xiāo)也更低。在熱啟動(dòng)中,系統(tǒng)的所有工作就是將您的 Activity 帶到前臺(tái)。只要應(yīng)用的所有 Activity 仍駐留在內(nèi)存中,應(yīng)用就不必重復(fù)執(zhí)行進(jìn)程、應(yīng)用、activity的創(chuàng)建。例如,按home鍵到桌面,然后又點(diǎn)圖標(biāo)啟動(dòng)應(yīng)用。
溫啟動(dòng) 溫啟動(dòng)包含了在冷啟動(dòng)期間發(fā)生的部分操作;同時(shí),它的開(kāi)銷(xiāo)要比熱啟動(dòng)高。有許多潛在狀態(tài)可視為溫啟動(dòng)。例如:用戶(hù)按返回鍵退出應(yīng)用后又重新啟動(dòng)應(yīng)用。這時(shí)進(jìn)程已在運(yùn)行,但應(yīng)用必須通過(guò)調(diào)用 onCreate() 從頭開(kāi)始重新創(chuàng)建 Activity。
啟動(dòng)優(yōu)化是在 冷啟動(dòng) 的基礎(chǔ)上進(jìn)行優(yōu)化。要優(yōu)化應(yīng)用以實(shí)現(xiàn)快速啟動(dòng),了解系統(tǒng)和應(yīng)用層面的情況以及它們?cè)诟鱾€(gè)狀態(tài)中的互動(dòng)方式很有幫助。
在冷啟動(dòng)開(kāi)始時(shí),系統(tǒng)有三個(gè)任務(wù),它們是:
- 加載并啟動(dòng)應(yīng)用。
- 在啟動(dòng)后立即顯示應(yīng)用的空白啟動(dòng)窗口。
- 創(chuàng)建應(yīng)用進(jìn)程。
系統(tǒng)一創(chuàng)建應(yīng)用進(jìn)程,應(yīng)用進(jìn)程就負(fù)責(zé)后續(xù)階段:
- 啟動(dòng)主線(xiàn)程。
- 創(chuàng)建應(yīng)用對(duì)象。
- 創(chuàng)建主 Activity。
- 加載視圖。
- 執(zhí)行初始繪制。
一旦應(yīng)用進(jìn)程完成第一次繪制,系統(tǒng)進(jìn)程就會(huì)換掉當(dāng)前顯示的后臺(tái)窗口(StartingWindow),替換為主 Activity。此時(shí),用戶(hù)可以開(kāi)始使用應(yīng)用。
下面是[官方文檔]中的啟動(dòng)過(guò)程流程圖,顯示系統(tǒng)進(jìn)程和應(yīng)用進(jìn)程之間如何交接工作。實(shí)際上對(duì)啟動(dòng)流程的簡(jiǎn)要概括。

三、優(yōu)化核心思想
問(wèn)題來(lái)了,啟動(dòng)優(yōu)化是對(duì) 啟動(dòng)流程的那些步驟進(jìn)行優(yōu)化呢?
這是一個(gè)好問(wèn)題。我們知道,用戶(hù)關(guān)心的是:點(diǎn)擊桌面圖標(biāo)后 要盡快的顯示第一個(gè)頁(yè)面,并且能夠進(jìn)行交互。 根據(jù)啟動(dòng)流程的分析,顯示頁(yè)面能和用戶(hù)交互,這是主線(xiàn)程做的事情。那么就要求 我們不能再主線(xiàn)程做耗時(shí)的操作。啟動(dòng)中的系統(tǒng)任務(wù)我們無(wú)法干預(yù),能干預(yù)的就是在創(chuàng)建應(yīng)用和創(chuàng)建 Activity 的過(guò)程中可能會(huì)出現(xiàn)的性能問(wèn)題。這一過(guò)程具體就是:
- Application的attachBaseContext
- Application的onCreate
- activity的onCreate
- activity的onStart
- activity的onResume
activity的onResume方法完成后才開(kāi)始首幀的繪制。所以這些方法中的耗時(shí)操作我們是要極力避免的。
并且,通常情況下,一個(gè)應(yīng)用的主頁(yè)的數(shù)據(jù)是需要進(jìn)行網(wǎng)絡(luò)請(qǐng)求的,那么用戶(hù)啟動(dòng)應(yīng)用是希望快速進(jìn)入主頁(yè)以及看到主頁(yè)數(shù)據(jù),這也是我們計(jì)算啟動(dòng)結(jié)束時(shí)間的一個(gè)依據(jù)。
四、時(shí)間檢測(cè)
4.1 Displayed
在 Android 4.4(API 級(jí)別 19)及更高版本中,logcat 包含一個(gè)輸出行,其中包含名為 “Displayed” 的值。此值代表從啟動(dòng)進(jìn)程到在屏幕上完成對(duì)應(yīng) Activity 的繪制所用的時(shí)間。經(jīng)過(guò)的時(shí)間包括以下事件序列:
- 啟動(dòng)進(jìn)程。
- 初始化對(duì)象。
- 創(chuàng)建并初始化 Activity。
- 擴(kuò)充布局。
- 首次繪制。
這是我的demo app 啟動(dòng)的日志打印,查看
2020-07-13 19:54:38.256 18137-18137/com.hfy.androidlearning I/hfy: onResume begin.
2020-07-13 19:54:38.257 18137-18137/com.hfy.androidlearning I/hfy: onResume end.
2020-07-13 19:54:38.269 1797-16782/? I/WindowManager: addWindow: Window{1402051 u0 com.hfy.androidlearning/com.hfy.demo01.MainActivity}
2020-07-13 19:54:38.391 1797-2017/? I/ActivityTaskManager: Displayed com.hfy.androidlearning/com.hfy.demo01.MainActivity: +2s251ms
復(fù)制代碼
可見(jiàn)“Displayed”的時(shí)間打印是在添加window之后,而添加window是在onResume方法之后。
4.2 adb shell
也可以使用adb命令運(yùn)行應(yīng)用來(lái)測(cè)量初步顯示所用時(shí)間:
adb shell am start -W [ApplicationId]/[根Activity的全路徑] 當(dāng)ApplicationId和package相同時(shí),根Activity全路徑可以省略前面的packageName。
Displayed 指標(biāo)和前面一樣出現(xiàn)在 logcat 輸出中:
2020-07-14 14:53:05.294 1797-2017/? I/ActivityTaskManager: Displayed com.hfy.androidlearning/com.hfy.demo01.MainActivity: +2s98ms
復(fù)制代碼
您的終端窗口在adb命令執(zhí)行后還應(yīng)顯示以下內(nèi)容:
hufeiyangdeMacBook-Pro:~ hufeiyang$ adb shell am start -W com.hfy.androidlearning/com.hfy.demo01.MainActivity
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.hfy.androidlearning/com.hfy.demo01.MainActivity }
Status: ok
LaunchState: COLD
Activity: com.hfy.androidlearning/com.hfy.demo01.MainActivity
TotalTime: 2098
WaitTime: 2100
Complete
復(fù)制代碼
我們關(guān)注TotalTime即可,即應(yīng)用的啟動(dòng)時(shí)間,包括 創(chuàng)建進(jìn)程 + Application初始化 + Activity初始化到界面顯示 的過(guò)程。
4.3 reportFullyDrawn()
可以使用 reportFullyDrawn() (API19及以上)方法測(cè)量從應(yīng)用啟動(dòng)到完全顯示所有資源和視圖層次結(jié)構(gòu)所用的時(shí)間。什么意思呢?前面核心思想中提到,主頁(yè)數(shù)據(jù)請(qǐng)求后完全呈現(xiàn)界面的過(guò)程也是一個(gè)優(yōu)化點(diǎn),而前面的“Displayed”、:“TotalTime”的時(shí)間統(tǒng)計(jì)都是啟動(dòng)到首幀繪制,那么如何獲取 從 啟動(dòng) 到 獲取網(wǎng)絡(luò)請(qǐng)求后再次完成刷新 的時(shí)間呢?
要解決此問(wèn)題,您可以手動(dòng)調(diào)用Activity的 reportFullyDrawn()方法,讓系統(tǒng)知道您的 Activity 已完成延遲加載。當(dāng)您使用此方法時(shí),logcat 顯示的值為從創(chuàng)建應(yīng)用對(duì)象到調(diào)用 reportFullyDrawn() 時(shí)所用的時(shí)間。使用示例如下:
@Override
protected void onResume() {
super.onResume();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
runOnUiThread(new Runnable() {
@Override
public void run() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
reportFullyDrawn();
}
}
});
}
}).start();
}
復(fù)制代碼
使用子線(xiàn)程睡1秒來(lái)模擬數(shù)據(jù)加載,然后調(diào)用reportFullyDrawn(),以下是 logcat 的輸出。
2020-07-14 15:26:00.979 1797-2017/? I/ActivityTaskManager: Displayed com.hfy.androidlearning/com.hfy.demo01.MainActivity: +2s133ms
2020-07-14 15:26:01.788 1797-2017/? I/ActivityTaskManager: Fully drawn com.hfy.androidlearning/com.hfy.demo01.MainActivity: +2s943ms
復(fù)制代碼
4.4 代碼打點(diǎn)
寫(xiě)一個(gè)打點(diǎn)工具類(lèi),開(kāi)始結(jié)束時(shí)分別記錄,把時(shí)間上報(bào)到服務(wù)器。
此方法可帶到線(xiàn)上,但代碼有侵入性。
開(kāi)始記錄的位置放在 Application 的 attachBaseContext 方法中,attachBaseContext 是我們應(yīng)用能接收到的最早的一個(gè)生命周期回調(diào)方法。
計(jì)算啟動(dòng)結(jié)束時(shí)間的兩種方式
一種是在 onWindowFocusChanged 方法中計(jì)算啟動(dòng)耗時(shí)。 onWindowFocusChanged 方法只是 Activity 的首幀時(shí)間,是 Activity 首次進(jìn)行繪制的時(shí)間,首幀時(shí)間和界面完整展示出來(lái)還有一段時(shí)間差,不能真正代表界面已經(jīng)展現(xiàn)出來(lái)了。
按首幀時(shí)間計(jì)算啟動(dòng)耗時(shí)并不準(zhǔn)確,我們要的是用戶(hù)真正看到我們界面的時(shí)間。 正確的計(jì)算啟動(dòng)耗時(shí)的時(shí)機(jī)是要等真實(shí)的數(shù)據(jù)展示出來(lái),比如在列表第一項(xiàng)的展示時(shí)再計(jì)算啟動(dòng)耗時(shí)。 (在 Adapter 中記錄啟動(dòng)耗時(shí)要加一個(gè)布爾值變量進(jìn)行判斷,避免 onBindViewHolder 方法被多次調(diào)用導(dǎo)致不必要的計(jì)算。)
//第一個(gè)item 且沒(méi)有記錄過(guò),就結(jié)束打點(diǎn)
if (helper.getLayoutPosition() == 1 && !mHasRecorded) {
mHasRecorded = true;
helper.getView(R.id.xxx).getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
helper.getView(R.id.xxx).getViewTreeObserver().removeOnPreDrawListener(this);
LogHelper.i("結(jié)束打點(diǎn)!");
return true;
}
});
}
復(fù)制代碼
4.5 AOP(Aspect Oriented Programming) 打點(diǎn)
面向切面編程,可以使用AspectJ。例如可以切Application的onCreate方法來(lái)計(jì)算其耗時(shí)。 特點(diǎn)是是對(duì)代碼無(wú)侵入性、可帶到線(xiàn)上。
五、分析工具介紹
分析方法耗時(shí)的工具: Systrace 、 Traceview,兩個(gè)是相互補(bǔ)充的關(guān)系,我們要在不同的場(chǎng)景下使用不同的工具,這樣才能發(fā)揮工具的最大作用。
5.1 Traceview
Traceview 能以圖形的形式展示代碼的執(zhí)行時(shí)間和調(diào)用棧信息,而且 Traceview 提供的信息非常全面,因?yàn)樗怂芯€(xiàn)程。
Traceview 的使用可以分為兩步:開(kāi)始跟蹤、分析結(jié)果。我們來(lái)看看具體操作。
通過(guò) Debug.startMethodTracing(tracepath) 開(kāi)始跟蹤方法,記錄一段時(shí)間內(nèi)的 CPU 使用情況。調(diào)用 Debug.stopMethodTracing() 停止跟蹤方法,然后系統(tǒng)就會(huì)為我們生成一個(gè).trace文件,我們可以通過(guò) Traceview 查看這個(gè)文件記錄的內(nèi)容。
文件生成的位置默認(rèn)在 Android/data/包名/files 下,下面來(lái)看一個(gè)例子。
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.i(TAG, "onCreate begin. ");
super.onCreate(savedInstanceState);
//默認(rèn)生成路徑:Android/data/包名/files/dmtrace.trace
Debug.startMethodTracing();
//也可以自定義路徑
//Debug.startMethodTracing(getExternalFilesDir(null)+"test.trace");
setContentView(R.layout.activity_main);
Intent intent = getIntent();
String name = intent.getStringExtra("name");
Log.i(TAG, "onCreate: name = " + name);
initConfig();
initView();
initData();
...
Debug.stopMethodTracing();
}
復(fù)制代碼
在MainActivity的onCreate前后方法中分別調(diào)用開(kāi)始停止記錄方法,運(yùn)行打開(kāi)應(yīng)用進(jìn)入首頁(yè)后,我們定位到 /sdcard/android/data/包名/files/ 目錄下查看文件管理器確實(shí)是有.trace文件:

然后雙擊打開(kāi):

以圖形來(lái)呈現(xiàn)方法跟蹤數(shù)據(jù)或函數(shù)跟蹤數(shù)據(jù),其中調(diào)用的時(shí)間段和時(shí)間在橫軸上表示,而其被調(diào)用方則在縱軸上顯示。 所以我們可以看到具體的方法及其耗時(shí)。
詳細(xì)介紹參考官方文檔《使用 CPU Profiler 檢查 CPU 活動(dòng)》。
可以看到在onCreate方法中,最耗時(shí)的是testHandler方法,它里面睡了一覺(jué)。
5.2 Systrace
Systrace 結(jié)合了 Android 內(nèi)核數(shù)據(jù),分析了線(xiàn)程活動(dòng)后會(huì)給我們生成一個(gè)非常精確 HTML 格式的報(bào)告。
Systrace原理:在系統(tǒng)的一些關(guān)鍵鏈路(如SystemServcie、虛擬機(jī)、Binder驅(qū)動(dòng))插入一些信息(Label)。然后,通過(guò)Label的開(kāi)始和結(jié)束來(lái)確定某個(gè)核心過(guò)程的執(zhí)行時(shí)間,并把這些Label信息收集起來(lái)得到系統(tǒng)關(guān)鍵路徑的運(yùn)行時(shí)間信息,最后得到整個(gè)系統(tǒng)的運(yùn)行性能信息。其中,Android Framework 里面一些重要的模塊都插入了label信息,用戶(hù)App中也可以添加自定義的Lable。
Systrace 提供的 Trace 工具類(lèi)默認(rèn)只能 API 18 以上的項(xiàng)目中才能使用,如果我們的兼容版本低于 API 18,我們可以使用 TraceCompat。 Systrace 的使用步驟和 Traceview 差不多,分為下面兩步。
- 調(diào)用跟蹤方法
- 查看跟蹤結(jié)果
來(lái)看示例,在onCreate前后分別使用TraceCompat.beginSection、TraceCompat.endSection方法:
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.i(TAG, "onCreate begin. ");
super.onCreate(savedInstanceState);
TraceCompat.beginSection("MainActivity onCreate");
Debug.startMethodTracing();//dmtrace.trace
// Debug.startMethodTracing(getExternalFilesDir(null)+"test.trace");
setContentView(R.layout.activity_main);
initConfig();
initView();
initData();
Debug.stopMethodTracing();
TraceCompat.endSection();
}
復(fù)制代碼
運(yùn)行app后,手動(dòng)殺掉。然后cd 到SDK 目錄下的 platform-tools/systrace 下,使用命令:
python systrace.py -t 10 -o /Users/hufeiyang/trace.html -a com.hfy.androidlearning
其中:-t 10是指跟蹤10秒,-o 表示把文件輸出到指定目錄下,-a 是指定應(yīng)用包名。
輸入完這行命令后,可以看到開(kāi)始跟蹤的提示。看到 “Starting tracing ”后,手動(dòng)打開(kāi)我們的應(yīng)用。
示例如下:
hufeiyangdeMacBook-Pro:~ hufeiyang$ cd /Users/hufeiyang/Library/Android/sdk/platform-tools/systrace
hufeiyangdeMacBook-Pro:systrace hufeiyang$ python systrace.py -t 10 -o /Users/hufeiyang/trace.html -a com.hfy.androidlearning
Starting tracing (10 seconds)
Tracing completed. Collecting output...
Outputting Systrace results...
Tracing complete, writing results
Wrote trace HTML file: file:///Users/hufeiyang/trace.html
復(fù)制代碼
跟蹤10秒,然后就在指定目錄生成了html文件,我們打開(kāi)看看:

這里我們同樣可以看到具體的耗時(shí),以及每一幀渲染耗費(fèi)的時(shí)間。具體參考官方文檔《Systrace 概覽》
小結(jié) Traceview 的兩個(gè)特點(diǎn)
- 可埋點(diǎn) Traceview 的好處之一是可以在代碼中埋點(diǎn),埋點(diǎn)后可以用 CPU Profiler 進(jìn)行分析。 因?yàn)槲覀儸F(xiàn)在優(yōu)化的是啟動(dòng)階段的代碼,如果我們打開(kāi) App 后直接通過(guò) CPU Profiler 進(jìn)行記錄的話(huà),就要求你有單身三十年的手速,點(diǎn)擊開(kāi)始記錄的時(shí)間要和應(yīng)用的啟動(dòng)時(shí)間完全一致。 有了 Traceview,哪怕你是老年人手速也可以記錄啟動(dòng)過(guò)程涉及的調(diào)用棧信息。
- 開(kāi)銷(xiāo)大 Traceview 的運(yùn)行時(shí)開(kāi)銷(xiāo)非常大,它會(huì)導(dǎo)致我們程序的運(yùn)行變慢。 之所以會(huì)變慢,是因?yàn)樗鼤?huì)通過(guò)虛擬機(jī)的 Profiler 抓取我們當(dāng)前所有線(xiàn)程的所有調(diào)用堆棧。 因?yàn)檫@個(gè)問(wèn)題,Traceview 也可能會(huì)帶偏我們的優(yōu)化方向。 比如我們有一個(gè)方法,這個(gè)方法在正常情況下的耗時(shí)不大,但是加上了 Traceview 之后可能會(huì)發(fā)現(xiàn)它的耗時(shí)變成了原來(lái)的十倍甚至更多。
Systrace 的兩個(gè)特點(diǎn)
- 開(kāi)銷(xiāo)小 Systrace 開(kāi)銷(xiāo)非常小,不像 Traceview,因?yàn)樗粫?huì)在我們埋點(diǎn)區(qū)間進(jìn)行記錄。 而 Traceview 是會(huì)把所有的線(xiàn)程的堆棧調(diào)用情況都記錄下來(lái)。
- 直觀 在 Systrace 中我們可以很直觀地看到 CPU 利用率的情況。 當(dāng)我們發(fā)現(xiàn) CPU 利用率低的時(shí)候,我們可以考慮讓更多代碼以異步的方式執(zhí)行,以提高 CPU 利用率。
Traceview 與 Systrace 的兩個(gè)區(qū)別
- 查看工具 Traceview 分析結(jié)果要使用 Profiler 查看。 Systrace 分析結(jié)果是在瀏覽器查看 HTML 文件。
- 埋點(diǎn)工具類(lèi) Traceview 使用的是 Debug.startMethodTracing()。 Systrace 用的是 Trace.beginSection() 和 TraceCompat.beginSection()。
六、啟動(dòng)優(yōu)化方案
優(yōu)化方案有兩個(gè)方向:
- 視覺(jué)優(yōu)化,啟動(dòng)耗時(shí)沒(méi)有變少,但是啟動(dòng)過(guò)程中給用戶(hù)更好的體驗(yàn)。
- 速度優(yōu)化,減少主線(xiàn)程的耗時(shí),真實(shí)做到快速啟動(dòng)。
6.1 視覺(jué)優(yōu)化
在《Activity的啟動(dòng)》中提到,在Activity啟動(dòng)前會(huì)展示一個(gè)名字叫StartingWindow的window,這個(gè)window的背景是取要啟動(dòng)Activity的Theme中配置的WindowBackground。
因?yàn)閱?dòng)根activity前是需要?jiǎng)?chuàng)建進(jìn)程等一系列操作,需要一定時(shí)間,而展示StartingWindow的目的是 告訴用戶(hù)你點(diǎn)擊是有反應(yīng)的,只是在處理中,然后Activity啟動(dòng)后,Activity的window就替換掉這個(gè)StartingWindow了。如果沒(méi)有這個(gè)StartingWindow,那么點(diǎn)擊后就會(huì)一段時(shí)間沒(méi)有反應(yīng),給用戶(hù)誤解。
而這,就是應(yīng)用啟動(dòng)開(kāi)始時(shí) 會(huì)展示白屏的原因了。
那么視覺(jué)優(yōu)化的方案 也就有了:替換第一個(gè)activity(通常是閃屏頁(yè))的Theme,把白色背景換成Logot圖,然后再Activity的onCreate中換回來(lái)。 這樣啟動(dòng)時(shí)看到的就是你配置的logo圖了。
具體操作一下:
<activity android:name=".MainActivity" android:theme="@style/SplashTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
復(fù)制代碼
這里我的而第一個(gè)activity是MainActivity,配置了theme是R.style.SplashTheme,來(lái)看下:
<style name="SplashTheme" parent="AppNoActionBarAlphaAnimTheme">
<item name="android:windowBackground">@drawable/splash_background</item>
</style>
復(fù)制代碼
看到 android:windowBackground已經(jīng)配置成了自定義的drawable,這個(gè)就是關(guān)鍵點(diǎn)了,而默認(rèn)是windowBackground是白色??纯醋远x的drawable:
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
android:opacity="opaque">
<!--兩層-->
<item android:drawable="@android:color/white"/>
<item>
<bitmap
android:src="@drawable/dog"
android:gravity="center"/>
</item>
</layer-list>
復(fù)制代碼
drawable的根節(jié)點(diǎn)是<layer-list>,然后一層是白色底,一層就是我們的logo圖片了。
最后,在activity的onCreate中把Theme換回R.style.AppTheme即可(要在super.onCreate之前)。
protected void onCreate(Bundle savedInstanceState) {
setTheme(R.style.AppTheme);
super.onCreate(savedInstanceState);
}
復(fù)制代碼
效果如下:
可以看到,確實(shí)視覺(jué)上體驗(yàn)比白屏好很多。
但實(shí)際上啟動(dòng)速度并沒(méi)有變快,下面就來(lái)看看可以真實(shí)提高啟動(dòng)速度的方案有哪些。
6.2 異步初始化
前面提到 提高啟動(dòng)速度,核心思想就是 減少主線(xiàn)程的耗時(shí)操作。啟動(dòng)過(guò)程中 可控住耗時(shí)的主線(xiàn)程 主要是Application的onCreate方法、Activity的onCreate、onStart、onResume方法。
通常我們會(huì)在Application的onCreate方法中進(jìn)行較多的初始化操作,例如第三方庫(kù)初始化,那么這一過(guò)程是就需要重點(diǎn)關(guān)注。
減少主線(xiàn)程耗時(shí)的方法,又可細(xì)分為異步初始化、延遲初始化,即把 主線(xiàn)程任務(wù) 放到子線(xiàn)程執(zhí)行 或 延后執(zhí)行。 下面就先來(lái)看看異步初始化是如何實(shí)現(xiàn)的。
執(zhí)行異步請(qǐng)求,一般是使用線(xiàn)程池,例如:
Runnable initTask = new Runnable() {
@Override
public void run() {
//init task
}
};
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(threadCount);
fixedThreadPool.execute(initTask);
復(fù)制代碼
但是通過(guò)線(xiàn)程池處理初始化任務(wù)的方式存在三個(gè)問(wèn)題:
- 代碼不夠優(yōu)雅 假如我們有 100 個(gè)初始化任務(wù),那像上面這樣的代碼就要寫(xiě) 100 遍,提交 100 次任務(wù)。
- 無(wú)法限制在 onCreate 中完成 有的第三方庫(kù)的初始化任務(wù)需要在 Application 的 onCreate 方法中執(zhí)行完成,雖然可以用 CountDownLatch 實(shí)現(xiàn)等待,但是還是有點(diǎn)繁瑣。
- 無(wú)法實(shí)現(xiàn)存在依賴(lài)關(guān)系 有的初始化任務(wù)之間存在依賴(lài)關(guān)系,比如極光推送需要設(shè)備 ID,而 initDeviceId() 這個(gè)方法也是一個(gè)初始化任務(wù)。
那么解決方案是啥?啟動(dòng)器!
LauncherStarter,即啟動(dòng)器,是針對(duì)這三個(gè)問(wèn)題的解決方案,結(jié)合CountDownLatch對(duì)線(xiàn)程池的再封裝,充分利用CPU多核,自動(dòng)梳理任務(wù)順序。
使用方式:
- 引入依賴(lài)
- 劃分任務(wù),確認(rèn)依賴(lài)和限制關(guān)系
- 添加任務(wù),執(zhí)行啟動(dòng)
首先依賴(lài)引入:
implementation 'com.github.zeshaoaaa:LaunchStarter:0.0.1'
復(fù)制代碼
然后把初始化任務(wù)劃分成一個(gè)個(gè)任務(wù);厘清依賴(lài)關(guān)系,例如任務(wù)2要依賴(lài)任務(wù)1完成后才能開(kāi)始;還有例如3任務(wù)需要在onCreate方法結(jié)束前完成;任務(wù)4要在主線(xiàn)程執(zhí)行。
然后添加這些任務(wù),開(kāi)始任務(wù),設(shè)置等待。
具體使用也比較簡(jiǎn)單,代碼如下:
public class MyApplication extends Application {
private static final String TAG = "MyApplication";
@Override
public void onCreate() {
super.onCreate();
TaskDispatcher.init(getBaseContext());
TaskDispatcher taskDispatcher = TaskDispatcher.createInstance();
// task2依賴(lài)task1;
// task3未完成時(shí)taskDispatcher.await()處需要等待;
// test4在主線(xiàn)程執(zhí)行
//每個(gè)任務(wù)都耗時(shí)一秒
Task1 task1 = new Task1();
Task2 task2 = new Task2();
Task3 task3 = new Task3();
Task4 task4Main = new Task4();
taskDispatcher.addTask(task1);
taskDispatcher.addTask(task2);
taskDispatcher.addTask(task3);
taskDispatcher.addTask(task4Main);
Log.i(TAG, "onCreate: taskDispatcher.start()");
taskDispatcher.start();//開(kāi)始
taskDispatcher.await();//等task3完成后才會(huì)往下走
Log.i(TAG, "onCreate: end.");
}
private static class Task1 extends Task {
@Override
public void run() {
Log.i(TAG, Thread.currentThread().getName()+" run start: task1");
doTask();
Log.i(TAG, Thread.currentThread().getName()+" run end: task1");
}
}
private static class Task2 extends Task {
@Override
public void run() {
Log.i(TAG, Thread.currentThread().getName()+" run start: task2");
doTask();
Log.i(TAG, Thread.currentThread().getName()+" run end: task2");
}
@Override
public List<Class<? extends Task>> dependsOn() {
//依賴(lài)task1,等task1執(zhí)行完再執(zhí)行
ArrayList<Class<? extends Task>> classes = new ArrayList<>();
classes.add(Task1.class);
return classes;
}
}
private static class Task3 extends Task {
@Override
public void run() {
Log.i(TAG, Thread.currentThread().getName()+" run start: task3");
doTask();
Log.i(TAG, Thread.currentThread().getName()+" run end: task3");
}
@Override
public boolean needWait() {
//task3未完成時(shí),在taskDispatcher.await()處需要等待。這里就是保證在onCreate結(jié)束前完成。
return true;
}
}
private static class Task4 extends MainTask {
//繼承自MainTask,即保證在主線(xiàn)程執(zhí)行
@Override
public void run() {
Log.i(TAG, Thread.currentThread().getName()+" run start: task4");
doTask();
Log.i(TAG, Thread.currentThread().getName()+" run end: task4");
}
}
private static void doTask() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
復(fù)制代碼
有4個(gè)初始化任務(wù),都耗時(shí)1秒,若都在主線(xiàn)程執(zhí)行,那么會(huì)耗時(shí)4秒。這里使用啟動(dòng)器執(zhí)行,并且保證了上面描述的任務(wù)要求限制。執(zhí)行完成后日志如下:
2020-07-17 12:06:20.648 26324-26324/com.hfy.androidlearning I/MyApplication: onCreate: taskDispatcher.start()
2020-07-17 12:06:20.650 26324-26324/com.hfy.androidlearning I/MyApplication: main run start: task4
2020-07-17 12:06:20.651 26324-26427/com.hfy.androidlearning I/MyApplication: TaskDispatcherPool-1-Thread-1 run start: task1
2020-07-17 12:06:20.657 26324-26428/com.hfy.androidlearning I/MyApplication: TaskDispatcherPool-1-Thread-2 run start: task3
2020-07-17 12:06:21.689 26324-26324/com.hfy.androidlearning I/MyApplication: main run end: task4
2020-07-17 12:06:21.689 26324-26427/com.hfy.androidlearning I/MyApplication: TaskDispatcherPool-1-Thread-1 run end: task1
2020-07-17 12:06:21.690 26324-26429/com.hfy.androidlearning I/MyApplication: TaskDispatcherPool-1-Thread-3 run start: task2
2020-07-17 12:06:21.697 26324-26428/com.hfy.androidlearning I/MyApplication: TaskDispatcherPool-1-Thread-2 run end: task3
2020-07-17 12:06:21.697 26324-26324/com.hfy.androidlearning I/MyApplication: onCreate: end.
2020-07-17 12:06:22.729 26324-26429/com.hfy.androidlearning I/MyApplication: TaskDispatcherPool-1-Thread-3 run end: task2
復(fù)制代碼
可見(jiàn)主線(xiàn)程耗時(shí)只有1秒。 另外,要注意的是,task3、task4一定是在onCreate內(nèi)完成了,task1、task2都可能是在onCreate結(jié)束后一段時(shí)間才完成,所以在Activity中就不能使用task1、task2相關(guān)的庫(kù)了。那么 在劃分任務(wù),確認(rèn)依賴(lài)和限制關(guān)系時(shí)就要注意了。
異步初始化就說(shuō)這么多,原理部分可直接閱讀源碼,很容易理解。接著看延遲初始化。
6.3 延遲初始化
在 Application 和 Activity 中可能存在優(yōu)先級(jí)不高的初始化任務(wù),可以考慮把這些任務(wù)進(jìn)行 延遲初始化。延遲初始化并不是減少了主線(xiàn)程耗時(shí),而是讓耗時(shí)操作讓位、讓資源給UI繪制,將耗時(shí)的操作延遲到UI加載完畢后。
那么問(wèn)題來(lái)了,如何延遲呢?
- 使用new Handler().postDelay()方法、或者view.postDelay()——但是延遲時(shí)間不好把握,不知道啥時(shí)候UI加載完畢。
- 使用View.getViewTreeObserver().addOnPreDrawListener()監(jiān)聽(tīng)——可以保證view繪制完成,但是此時(shí)發(fā)生交互呢,例如用戶(hù)在滑動(dòng)列表,那么就會(huì)造成卡頓了。
那么解決方案是啥?延遲啟動(dòng)器!
延遲啟動(dòng)器,利用IdleHandler特性,在CPU空閑時(shí)執(zhí)行,對(duì)延遲任務(wù)進(jìn)行分批初始化, 這樣 執(zhí)行時(shí)機(jī)明確、也緩解界面UI卡頓。 延遲啟動(dòng)器就是上面的LauncherStarter中的一個(gè)類(lèi)。
public class DelayInitDispatcher {
private Queue<Task> mDelayTasks = new LinkedList<>();
private MessageQueue.IdleHandler mIdleHandler = new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
if(mDelayTasks.size()>0){
Task task = mDelayTasks.poll();
new DispatchRunnable(task).run();
}
return !mDelayTasks.isEmpty();
}
};
public DelayInitDispatcher addTask(Task task){
mDelayTasks.add(task);
return this;
}
public void start(){
Looper.myQueue().addIdleHandler(mIdleHandler);
}
}
復(fù)制代碼
使用也很簡(jiǎn)單,例如在閃屏頁(yè)中添加任務(wù)開(kāi)始即可:
//SpalshActivity
DelayInitDispatcher delayInitDispatcher = new DelayInitDispatcher();
protected void onCreate(Bundle savedInstanceState) {
delayInitDispatcher.addTask(new Task() {
@Override
public void run() {
Log.i(TAG, "run: delay task begin");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.i(TAG, "run: delay task end");
}
});
delayInitDispatcher.start();
}
復(fù)制代碼
經(jīng)測(cè)試,確實(shí)是在是在布局展示后開(kāi)始任務(wù)。但是如果耗時(shí)較長(zhǎng)(例子中是3秒),過(guò)程中滑動(dòng)屏幕,是不能及時(shí)響應(yīng)的,會(huì)感覺(jué)到明顯的卡頓。
所以,能異步的task優(yōu)先使用異步啟動(dòng)器在Application的onCreate方法中加載,對(duì)于不能異步且耗時(shí)較少的task,我們可以利用延遲啟動(dòng)器進(jìn)行加載。如果任務(wù)可以到用時(shí)再加載,可以使用懶加載的方式。
IdleHandler原理分析:
//MessageQueue.java
Message next() {
// Return here if the message loop has already quit and been disposed.
// This can happen if the application tries to restart a looper after quit
// which is not supported.
final long ptr = mPtr;
if (ptr == 0) {
return null;
}
int pendingIdleHandlerCount = -1; // -1 only during first iteration
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
// Try to retrieve the next message. Return if found.
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
if (msg != null && msg.target == null) {
// Stalled by a barrier. Find the next asynchronous message in the queue.
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
if (now < msg.when) {
// Next message is not ready. Set a timeout to wake up when it is ready.
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// Got a message.
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
return msg;
}
} else {
// No more messages.
nextPollTimeoutMillis = -1;
}
// Process the quit message now that all pending messages have been handled.
if (mQuitting) {
dispose();
return null;
}
// If first time idle, then get the number of idlers to run.
// Idle handles only run if the queue is empty or if the first message
// in the queue (possibly a barrier) is due to be handled in the future.
if (pendingIdleHandlerCount < 0
&& (mMessages == null || now < mMessages.when)) {
pendingIdleHandlerCount = mIdleHandlers.size();
}
if (pendingIdleHandlerCount <= 0) {
// No idle handlers to run. Loop and wait some more.
mBlocked = true;
continue;
}
if (mPendingIdleHandlers == null) {
mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
}
mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
}
// Run the idle handlers.
// We only ever reach this code block during the first iteration.
for (int i = 0; i < pendingIdleHandlerCount; i++) {
final IdleHandler idler = mPendingIdleHandlers[i];
mPendingIdleHandlers[i] = null; // release the reference to the handler
boolean keep = false;
try {
keep = idler.queueIdle();
} catch (Throwable t) {
Log.wtf(TAG, "IdleHandler threw exception", t);
}
if (!keep) {
synchronized (this) {
mIdleHandlers.remove(idler);
}
}
}
// Reset the idle handler count to 0 so we do not run them again.
pendingIdleHandlerCount = 0;
// While calling an idle handler, a new message could have been delivered
// so go back and look again for a pending message without waiting.
nextPollTimeoutMillis = 0;
}
}
復(fù)制代碼
從消息隊(duì)列取消息時(shí),如果沒(méi)有取到消息,就執(zhí)行 空閑IdleHandler,執(zhí)行完就remove。
6.4 Multidex預(yù)加載優(yōu)化
安裝或者升級(jí)后 首次 MultiDex 花費(fèi)的時(shí)間過(guò)于漫長(zhǎng),我們需要進(jìn)行Multidex的預(yù)加載優(yōu)化。
5.0以上默認(rèn)使用ART,在安裝時(shí)已將Class.dex轉(zhuǎn)換為oat文件了,無(wú)需優(yōu)化,所以應(yīng)判斷只有在主進(jìn)程及SDK 5.0以下才進(jìn)行Multidex的預(yù)加載
抖音BoostMultiDex優(yōu)化實(shí)踐:
抖音BoostMultiDex優(yōu)化實(shí)踐:Android低版本上APP首次啟動(dòng)時(shí)間減少80%(一)
快速接入:
- build.gradle的dependencies中添加依賴(lài):
dependencies {
// For specific version number, please refer to app demo
implementation 'com.bytedance.boost_multidex:boost_multidex:1.0.1'
}
復(fù)制代碼
- 與官方MultiDex類(lèi)似,在Application.attachBaseContext的最前面進(jìn)行初始化即可:
public class YourApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
BoostMultiDex.install(base);
}
復(fù)制代碼
6.5 頁(yè)面數(shù)據(jù)預(yù)加載
閃屏頁(yè)、首頁(yè)的數(shù)據(jù)預(yù)加載:閃屏廣告、首頁(yè)數(shù)據(jù) 加載后緩存到本地,下次進(jìn)入時(shí)直接讀取緩存。 首頁(yè)讀取緩存到內(nèi)存的操作還可以提前到閃屏頁(yè)。
6.6 頁(yè)面繪制優(yōu)化
閃屏頁(yè)與主頁(yè)的繪制優(yōu)化,這里涉及到繪制優(yōu)化相關(guān)知識(shí)了,例如減少布局層級(jí)等。
七、總結(jié)
我們先介紹了啟動(dòng)流程、優(yōu)化思想、耗時(shí)檢測(cè)、分析工具,然后給出了常用優(yōu)化方案:異步初始化、延遲初始化。涉及了很多新知識(shí)和工具,一些地方文章中沒(méi)有展開(kāi),可以參考給出的連接詳細(xì)學(xué)習(xí)。畢竟性能優(yōu)化是多樣技術(shù)知識(shí)的綜合使用,需要系統(tǒng)掌握對(duì)應(yīng)工作流程被、分析工具、解決方案,才能對(duì)性能進(jìn)行深層次的優(yōu)化。
好了,今天就到這里,歡迎留言討論~
點(diǎn)關(guān)注,更多Android開(kāi)發(fā)技能~~