一霸奕、概述
手機(jī)桌面點(diǎn)擊一個(gè)應(yīng)用,用戶希望應(yīng)用能及時(shí)響應(yīng)吉拳、快速加載质帅。啟動(dòng)時(shí)間過長(zhǎng)的應(yīng)用可能會(huì)令用戶失望。這種糟糕的體驗(yàn)可能會(huì)導(dǎo)致用戶在 Play 商店針對(duì)您的應(yīng)用給出很低的評(píng)分留攒,甚至完全棄用您的應(yīng)用煤惩。
本篇就來講解如何分析和優(yōu)化應(yīng)用的啟動(dòng)時(shí)間。首先介紹啟動(dòng)過程機(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)用從頭開始啟動(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)腔剂。例如媒区,通過任務(wù)列表手動(dòng)殺掉應(yīng)用進(jìn)程后,又重新啟動(dòng)應(yīng)用桶蝎。
熱啟動(dòng) 熱啟動(dòng)比冷啟動(dòng)簡(jiǎn)單得多驻仅,開銷也更低。在熱啟動(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í)嬉愧,它的開銷要比熱啟動(dòng)高贩挣。有許多潛在狀態(tài)可視為溫啟動(dòng)。例如:用戶按返回鍵退出應(yīng)用后又重新啟動(dòng)應(yīng)用没酣。這時(shí)進(jìn)程已在運(yùn)行王财,但應(yīng)用必須通過調(diào)用 onCreate() 從頭開始重新創(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)開始時(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)主線程视事。
- 創(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í)腾仅,用戶可以開始使用應(yīng)用乒裆。
下面是[官方文檔]中的啟動(dòng)過程流程圖,顯示系統(tǒng)進(jìn)程和應(yīng)用進(jìn)程之間如何交接工作推励。實(shí)際上對(duì)啟動(dòng)流程的簡(jiǎn)要概括鹤耍。
三、優(yōu)化核心思想
問題來了验辞,啟動(dòng)優(yōu)化是對(duì) 啟動(dòng)流程的那些步驟進(jìn)行優(yōu)化呢稿黄?
這是一個(gè)好問題。我們知道跌造,用戶關(guān)心的是:點(diǎn)擊桌面圖標(biāo)后 要盡快的顯示第一個(gè)頁面杆怕,并且能夠進(jìn)行交互族购。 根據(jù)啟動(dòng)流程的分析,顯示頁面能和用戶交互陵珍,這是主線程做的事情寝杖。那么就要求 我們不能再主線程做耗時(shí)的操作。啟動(dòng)中的系統(tǒng)任務(wù)我們無法干預(yù)互纯,能干預(yù)的就是在創(chuàng)建應(yīng)用和創(chuàng)建 Activity 的過程中可能會(huì)出現(xiàn)的性能問題瑟幕。這一過程具體就是:
- Application的attachBaseContext
- Application的onCreate
- activity的onCreate
- activity的onStart
- activity的onResume
activity的onResume方法完成后才開始首幀的繪制。所以這些方法中的耗時(shí)操作我們是要極力避免的留潦。
并且只盹,通常情況下,一個(gè)應(yīng)用的主頁的數(shù)據(jù)是需要進(jìn)行網(wǎng)絡(luò)請(qǐng)求的愤兵,那么用戶啟動(dòng)應(yīng)用是希望快速進(jìn)入主頁以及看到主頁數(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)過的時(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ù)制代碼
可見“Displayed”的時(shí)間打印是在添加window之后,而添加window是在onResume方法之后厉亏。
4.2 adb shell
也可以使用adb命令運(yùn)行應(yīng)用來測(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初始化到界面顯示 的過程。
4.3 reportFullyDrawn()
可以使用 reportFullyDrawn() (API19及以上)方法測(cè)量從應(yīng)用啟動(dòng)到完全顯示所有資源和視圖層次結(jié)構(gòu)所用的時(shí)間恬试。什么意思呢窝趣?前面核心思想中提到,主頁數(shù)據(jù)請(qǐng)求后完全呈現(xiàn)界面的過程也是一個(gè)優(yōu)化點(diǎn)训柴,而前面的“Displayed”哑舒、:“TotalTime”的時(shí)間統(tǒng)計(jì)都是啟動(dòng)到首幀繪制,那么如何獲取 從 啟動(dòng) 到 獲取網(wǎng)絡(luò)請(qǐng)求后再次完成刷新 的時(shí)間呢畦粮?
要解決此問題散址,您可以手動(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ù)制代碼
使用子線程睡1秒來模擬數(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)
寫一個(gè)打點(diǎn)工具類,開始結(jié)束時(shí)分別記錄贡翘,把時(shí)間上報(bào)到服務(wù)器蹈矮。
此方法可帶到線上,但代碼有侵入性鸣驱。
開始記錄的位置放在 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í)間和界面完整展示出來還有一段時(shí)間差闸翅,不能真正代表界面已經(jīng)展現(xiàn)出來了再芋。
按首幀時(shí)間計(jì)算啟動(dòng)耗時(shí)并不準(zhǔn)確,我們要的是用戶真正看到我們界面的時(shí)間坚冀。 正確的計(jì)算啟動(dòng)耗時(shí)的時(shí)機(jī)是要等真實(shí)的數(shù)據(jù)展示出來济赎,比如在列表第一項(xiàng)的展示時(shí)再計(jì)算啟動(dòng)耗時(shí)。 (在 Adapter 中記錄啟動(dòng)耗時(shí)要加一個(gè)布爾值變量進(jìn)行判斷记某,避免 onBindViewHolder 方法被多次調(diào)用導(dǎo)致不必要的計(jì)算司训。)
//第一個(gè)item 且沒有記錄過,就結(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方法來計(jì)算其耗時(shí)贺拣。 特點(diǎn)是是對(duì)代碼無侵入性、可帶到線上捂蕴。
五譬涡、分析工具介紹
分析方法耗時(shí)的工具: Systrace 、 Traceview啥辨,兩個(gè)是相互補(bǔ)充的關(guān)系涡匀,我們要在不同的場(chǎng)景下使用不同的工具,這樣才能發(fā)揮工具的最大作用溉知。
5.1 Traceview
Traceview 能以圖形的形式展示代碼的執(zhí)行時(shí)間和調(diào)用棧信息陨瘩,而且 Traceview 提供的信息非常全面腕够,因?yàn)樗怂芯€程。
Traceview 的使用可以分為兩步:開始跟蹤舌劳、分析結(jié)果帚湘。我們來看看具體操作。
通過 Debug.startMethodTracing(tracepath) 開始跟蹤方法甚淡,記錄一段時(shí)間內(nèi)的 CPU 使用情況大诸。調(diào)用 Debug.stopMethodTracing() 停止跟蹤方法,然后系統(tǒng)就會(huì)為我們生成一個(gè).trace文件贯卦,我們可以通過 Traceview 查看這個(gè)文件記錄的內(nèi)容资柔。
文件生成的位置默認(rèn)在 Android/data/包名/files 下,下面來看一個(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)用開始停止記錄方法贿堰,運(yùn)行打開應(yīng)用進(jìn)入首頁后,我們定位到 /sdcard/android/data/包名/files/ 目錄下查看文件管理器確實(shí)是有.trace文件:
然后雙擊打開:
以圖形來呈現(xiàn)方法跟蹤數(shù)據(jù)或函數(shù)跟蹤數(shù)據(jù)啡彬,其中調(diào)用的時(shí)間段和時(shí)間在橫軸上表示羹与,而其被調(diào)用方則在縱軸上顯示。 所以我們可以看到具體的方法及其耗時(shí)外遇。
詳細(xì)介紹參考官方文檔《使用 CPU Profiler 檢查 CPU 活動(dòng)》注簿。
可以看到在onCreate方法中,最耗時(shí)的是testHandler方法跳仿,它里面睡了一覺诡渴。
5.2 Systrace
Systrace 結(jié)合了 Android 內(nèi)核數(shù)據(jù),分析了線程活動(dòng)后會(huì)給我們生成一個(gè)非常精確 HTML 格式的報(bào)告菲语。
Systrace原理:在系統(tǒng)的一些關(guān)鍵鏈路(如SystemServcie妄辩、虛擬機(jī)、Binder驅(qū)動(dòng))插入一些信息(Label)山上。然后眼耀,通過Label的開始和結(jié)束來確定某個(gè)核心過程的執(zhí)行時(shí)間,并把這些Label信息收集起來得到系統(tǒng)關(guān)鍵路徑的運(yùn)行時(shí)間信息佩憾,最后得到整個(gè)系統(tǒng)的運(yùn)行性能信息哮伟。其中,Android Framework 里面一些重要的模塊都插入了label信息妄帘,用戶App中也可以添加自定義的Lable楞黄。
Systrace 提供的 Trace 工具類默認(rèn)只能 API 18 以上的項(xiàng)目中才能使用,如果我們的兼容版本低于 API 18抡驼,我們可以使用 TraceCompat鬼廓。 Systrace 的使用步驟和 Traceview 差不多,分為下面兩步致盟。
- 調(diào)用跟蹤方法
- 查看跟蹤結(jié)果
來看示例碎税,在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)用包名荔泳。
輸入完這行命令后,可以看到開始跟蹤的提示虐杯。看到 “Starting tracing ”后玛歌,手動(dòng)打開我們的應(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文件支子,我們打開看看:
這里我們同樣可以看到具體的耗時(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)階段的代碼巩搏,如果我們打開 App 后直接通過 CPU Profiler 進(jìn)行記錄的話昨登,就要求你有單身三十年的手速,點(diǎn)擊開始記錄的時(shí)間要和應(yīng)用的啟動(dòng)時(shí)間完全一致贯底。 有了 Traceview丰辣,哪怕你是老年人手速也可以記錄啟動(dòng)過程涉及的調(diào)用棧信息。
- 開銷大 Traceview 的運(yùn)行時(shí)開銷非常大禽捆,它會(huì)導(dǎo)致我們程序的運(yùn)行變慢笙什。 之所以會(huì)變慢,是因?yàn)樗鼤?huì)通過虛擬機(jī)的 Profiler 抓取我們當(dāng)前所有線程的所有調(diào)用堆棧胚想。 因?yàn)檫@個(gè)問題琐凭,Traceview 也可能會(huì)帶偏我們的優(yōu)化方向。 比如我們有一個(gè)方法浊服,這個(gè)方法在正常情況下的耗時(shí)不大统屈,但是加上了 Traceview 之后可能會(huì)發(fā)現(xiàn)它的耗時(shí)變成了原來的十倍甚至更多。
Systrace 的兩個(gè)特點(diǎn)
- 開銷小 Systrace 開銷非常小牙躺,不像 Traceview鸿吆,因?yàn)樗粫?huì)在我們埋點(diǎn)區(qū)間進(jìn)行記錄。 而 Traceview 是會(huì)把所有的線程的堆棧調(diào)用情況都記錄下來述呐。
- 直觀 在 Systrace 中我們可以很直觀地看到 CPU 利用率的情況。 當(dāng)我們發(fā)現(xiàn) CPU 利用率低的時(shí)候蕉毯,我們可以考慮讓更多代碼以異步的方式執(zhí)行乓搬,以提高 CPU 利用率思犁。
Traceview 與 Systrace 的兩個(gè)區(qū)別
- 查看工具 Traceview 分析結(jié)果要使用 Profiler 查看。 Systrace 分析結(jié)果是在瀏覽器查看 HTML 文件进肯。
- 埋點(diǎn)工具類 Traceview 使用的是 Debug.startMethodTracing()激蹲。 Systrace 用的是 Trace.beginSection() 和 TraceCompat.beginSection()。
六江掩、啟動(dòng)優(yōu)化方案
優(yōu)化方案有兩個(gè)方向:
- 視覺優(yōu)化学辱,啟動(dòng)耗時(shí)沒有變少,但是啟動(dòng)過程中給用戶更好的體驗(yàn)环形。
- 速度優(yōu)化策泣,減少主線程的耗時(shí),真實(shí)做到快速啟動(dòng)抬吟。
6.1 視覺優(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的目的是 告訴用戶你點(diǎn)擊是有反應(yīng)的钙畔,只是在處理中茫陆,然后Activity啟動(dòng)后,Activity的window就替換掉這個(gè)StartingWindow了擎析。如果沒有這個(gè)StartingWindow簿盅,那么點(diǎn)擊后就會(huì)一段時(shí)間沒有反應(yīng),給用戶誤解叔锐。
而這挪鹏,就是應(yīng)用啟動(dòng)開始時(shí) 會(huì)展示白屏的原因了。
那么視覺優(yōu)化的方案 也就有了:替換第一個(gè)activity(通常是閃屏頁)的Theme愉烙,把白色背景換成Logot圖讨盒,然后再Activity的onCreate中換回來。 這樣啟動(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,來看下:
<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是白色≌岚看看自定義的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í)視覺上體驗(yàn)比白屏好很多秦躯。
但實(shí)際上啟動(dòng)速度并沒有變快忆谓,下面就來看看可以真實(shí)提高啟動(dòng)速度的方案有哪些。
6.2 異步初始化
前面提到 提高啟動(dòng)速度踱承,核心思想就是 減少主線程的耗時(shí)操作倡缠。啟動(dòng)過程中 可控住耗時(shí)的主線程 主要是Application的onCreate方法、Activity的onCreate茎活、onStart昙沦、onResume方法。
通常我們會(huì)在Application的onCreate方法中進(jìn)行較多的初始化操作妙色,例如第三方庫(kù)初始化桅滋,那么這一過程是就需要重點(diǎn)關(guān)注。
減少主線程耗時(shí)的方法身辨,又可細(xì)分為異步初始化丐谋、延遲初始化,即把 主線程任務(wù) 放到子線程執(zhí)行 或 延后執(zhí)行煌珊。 下面就先來看看異步初始化是如何實(shí)現(xiàn)的号俐。
執(zhí)行異步請(qǐng)求,一般是使用線程池定庵,例如:
Runnable initTask = new Runnable() {
@Override
public void run() {
//init task
}
};
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(threadCount);
fixedThreadPool.execute(initTask);
復(fù)制代碼
但是通過線程池處理初始化任務(wù)的方式存在三個(gè)問題:
- 代碼不夠優(yōu)雅 假如我們有 100 個(gè)初始化任務(wù)吏饿,那像上面這樣的代碼就要寫 100 遍,提交 100 次任務(wù)蔬浙。
- 無法限制在 onCreate 中完成 有的第三方庫(kù)的初始化任務(wù)需要在 Application 的 onCreate 方法中執(zhí)行完成猪落,雖然可以用 CountDownLatch 實(shí)現(xiàn)等待,但是還是有點(diǎn)繁瑣畴博。
- 無法實(shí)現(xiàn)存在依賴關(guān)系 有的初始化任務(wù)之間存在依賴關(guān)系笨忌,比如極光推送需要設(shè)備 ID,而 initDeviceId() 這個(gè)方法也是一個(gè)初始化任務(wù)俱病。
那么解決方案是啥官疲?啟動(dòng)器!
LauncherStarter亮隙,即啟動(dòng)器途凫,是針對(duì)這三個(gè)問題的解決方案,結(jié)合CountDownLatch對(duì)線程池的再封裝溢吻,充分利用CPU多核维费,自動(dòng)梳理任務(wù)順序
。
使用方式:
- 引入依賴
- 劃分任務(wù),確認(rèn)依賴和限制關(guān)系
- 添加任務(wù)犀盟,執(zhí)行啟動(dòng)
首先依賴引入:
implementation 'com.github.zeshaoaaa:LaunchStarter:0.0.1'
復(fù)制代碼
然后把初始化任務(wù)劃分成一個(gè)個(gè)任務(wù)噪漾;厘清依賴關(guān)系,例如任務(wù)2要依賴任務(wù)1完成后才能開始且蓬;還有例如3任務(wù)需要在onCreate方法結(jié)束前完成;任務(wù)4要在主線程執(zhí)行题翰。
然后添加這些任務(wù)恶阴,開始任務(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依賴task1;
// task3未完成時(shí)taskDispatcher.await()處需要等待血公;
// test4在主線程執(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();//開始
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() {
//依賴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摔笤,即保證在主線程執(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秒垦写,若都在主線程執(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ù)制代碼
可見主線程耗時(shí)只有1秒。 另外分蓖,要注意的是尔艇,task3、task4一定是在onCreate內(nèi)完成了么鹤,task1终娃、task2都可能是在onCreate結(jié)束后一段時(shí)間才完成,所以在Activity中就不能使用task1午磁、task2相關(guān)的庫(kù)了尝抖。那么 在劃分任務(wù),確認(rèn)依賴和限制關(guān)系時(shí)就要注意了迅皇。
異步初始化就說這么多昧辽,原理部分可直接閱讀源碼,很容易理解登颓。接著看延遲初始化搅荞。
6.3 延遲初始化
在 Application 和 Activity 中可能存在優(yōu)先級(jí)不高的初始化任務(wù),可以考慮把這些任務(wù)進(jìn)行 延遲初始化。延遲初始化并不是減少了主線程耗時(shí)咕痛,而是讓耗時(shí)操作讓位痢甘、讓資源給UI繪制,將耗時(shí)的操作延遲到UI加載完畢后茉贡。
那么問題來了塞栅,如何延遲呢?
- 使用new Handler().postDelay()方法腔丧、或者view.postDelay()——但是延遲時(shí)間不好把握放椰,不知道啥時(shí)候UI加載完畢。
- 使用View.getViewTreeObserver().addOnPreDrawListener()監(jiān)聽——可以保證view繪制完成愉粤,但是此時(shí)發(fā)生交互呢砾医,例如用戶在滑動(dòng)列表,那么就會(huì)造成卡頓了衣厘。
那么解決方案是啥如蚜?延遲啟動(dòng)器!
延遲啟動(dòng)器影暴,利用IdleHandler特性错邦,在CPU空閑時(shí)執(zhí)行,對(duì)延遲任務(wù)進(jìn)行分批初始化坤检, 這樣 執(zhí)行時(shí)機(jī)明確兴猩、也緩解界面UI卡頓。 延遲啟動(dòng)器就是上面的LauncherStarter中的一個(gè)類早歇。
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)單倾芝,例如在閃屏頁中添加任務(wù)開始即可:
//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í)是在是在布局展示后開始任務(wù)箭跳。但是如果耗時(shí)較長(zhǎng)(例子中是3秒)晨另,過程中滑動(dòng)屏幕,是不能及時(shí)響應(yīng)的谱姓,會(huì)感覺到明顯的卡頓借尿。
所以,能異步的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í),如果沒有取到消息慨绳,就執(zhí)行 空閑IdleHandler掉冶,執(zhí)行完就remove真竖。
6.4 Multidex預(yù)加載優(yōu)化
安裝或者升級(jí)后 首次 MultiDex 花費(fèi)的時(shí)間過于漫長(zhǎng)耻瑟,我們需要進(jìn)行Multidex的預(yù)加載優(yōu)化咧最。
5.0以上默認(rèn)使用ART散劫,在安裝時(shí)已將Class.dex轉(zhuǎn)換為oat文件了骇窍,無需優(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中添加依賴:
dependencies {
// For specific version number, please refer to app demo
implementation 'com.bytedance.boost_multidex:boost_multidex:1.0.1'
}
復(fù)制代碼
- 與官方MultiDex類似楔绞,在Application.attachBaseContext的最前面進(jìn)行初始化即可:
public class YourApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
BoostMultiDex.install(base);
}
復(fù)制代碼
6.5 頁面數(shù)據(jù)預(yù)加載
閃屏頁癣蟋、首頁的數(shù)據(jù)預(yù)加載:閃屏廣告拐袜、首頁數(shù)據(jù) 加載后緩存到本地,下次進(jìn)入時(shí)直接讀取緩存梢薪。 首頁讀取緩存到內(nèi)存的操作還可以提前到閃屏頁。
6.6 頁面繪制優(yōu)化
閃屏頁與主頁的繪制優(yōu)化尝哆,這里涉及到繪制優(yōu)化相關(guān)知識(shí)了秉撇,例如減少布局層級(jí)等。
七秋泄、總結(jié)
我們先介紹了啟動(dòng)流程琐馆、優(yōu)化思想、耗時(shí)檢測(cè)恒序、分析工具瘦麸,然后給出了常用優(yōu)化方案:異步初始化、延遲初始化歧胁。涉及了很多新知識(shí)和工具滋饲,一些地方文章中沒有展開,可以參考給出的連接詳細(xì)學(xué)習(xí)喊巍。畢竟性能優(yōu)化是多樣技術(shù)知識(shí)的綜合使用屠缭,需要系統(tǒng)掌握對(duì)應(yīng)工作流程被、分析工具崭参、解決方案呵曹,才能對(duì)性能進(jìn)行深層次的優(yōu)化。
好了何暮,今天就到這里奄喂,歡迎留言討論~
點(diǎn)關(guān)注,更多Android開發(fā)技能~~