兩年前我做過(guò)了類似的啟動(dòng)優(yōu)化分析《如何統(tǒng)計(jì)Android App啟動(dòng)時(shí)間》和《如何優(yōu)化Androd App啟動(dòng)速度》。兩年過(guò)后,今天看來(lái),之前說(shuō)的nimbledroid工具已經(jīng)需要收費(fèi),而且Android Studio自帶的Android Profiler已經(jīng)足夠強(qiáng)大,并且Systrace也有了更為強(qiáng)大的Perfetto UI分析工具。我們是時(shí)候來(lái)重新學(xué)習(xí)一下目前性能分析的方法以及如何在分析的基礎(chǔ)上做啟動(dòng)優(yōu)化這個(gè)事情。轉(zhuǎn)載請(qǐng)注明來(lái)源「Bug總柴」
性能分析工具
首先我們來(lái)學(xué)習(xí)一下如何使用性能分析的工具。我們從一個(gè)具體的例子出發(fā),就是如何分析應(yīng)用啟動(dòng)的性能。
Android Profiler
配置
我們來(lái)先看看Android Profiler。為了能在應(yīng)用一啟動(dòng)就能馬上捕捉到分析數(shù)據(jù),我們需要按照下面的步驟配置一下:
-
選擇 Run -> Edit Configurations
步驟一 - 在設(shè)置里面選擇Profiling的tab,然后選中Start recording CPU activity on startup。注意這里選擇的Sample Java Methods,表示可以定位到Java代碼。其他選項(xiàng)的含義查看cpu-profiler#configurations。
如果想有更詳細(xì)的信息的話,可以選中Enable advanced profiling。
步驟二 -
在配置完之后選擇Run -> Profiler
步驟三
在頁(yè)面啟動(dòng)完成之后停止監(jiān)測(cè),可以得到啟動(dòng)過(guò)程的CPU、內(nèi)存網(wǎng)絡(luò)和電量消耗信息,如下圖:
Android Profiler
CPU監(jiān)控
分析過(guò)程
點(diǎn)擊進(jìn)入CPU模塊

可以選擇線程,并看到線程的具體代碼耗時(shí)。
如以下例子

綠色表示我們寫的代碼耗時(shí),我們可以選擇主線程進(jìn)行觀察。這里顯示在Applicaiton onCreate過(guò)程中需要耗費(fèi)620ms。其中比較耗時(shí)的方法是registerByCourseKey和initYouzanSDK。并且通過(guò)Call Chart視圖不斷的往下看可以看出導(dǎo)致這個(gè)方法耗時(shí)的具體原因


通過(guò)這樣不斷的往下分析,就能大致定位到啟動(dòng)CPU耗時(shí)的原因。下面我們舉一個(gè)具體的優(yōu)化例子。
優(yōu)化例子
優(yōu)化前:

如果上圖所示,在啟動(dòng)過(guò)程中RxBroadcast的時(shí)候帶來(lái)了較大的耗時(shí)

查看代碼:
private fun initBroadcast() {
val filter = IntentFilter()
……
disposables.add(RxBroadcast.fromLocalBroadcast(context, filter)
.subscribe({ intent ->
……
},
{ throwable: Throwable ->
……
}
))
}
確實(shí)在initBroadcast使用了RxBroadcast.fromLocalBroadcast()方法,我們嘗試使用LocalBroadcastManager.registerReceiver代替。修改為如下代碼:
private fun initBroadcast() {
val filter = IntentFilter()
……
LocalBroadcastManager.getInstance(context).registerReceiver(broadcastReceiver, filter)
}
優(yōu)化后重新進(jìn)行啟動(dòng)CPU分析:

可以看出初始化的時(shí)間比優(yōu)化前減少了90ms。由此我們也可以得到結(jié)論,使用RxBroadcast雖然比較炫酷,但是這是一個(gè)比較耗時(shí)的行為,因此應(yīng)該盡量減少RxBroadcast的使用。
注意事項(xiàng)
-
需要注意的是這里的耗時(shí)有些是在CPU處于Sleep狀態(tài)下的。
在Sleep狀態(tài)表示CPU被其他線程占用,這個(gè)時(shí)候需要分析主線程Sleep狀態(tài)下其他線程的情況。例如:
sleep
這里顯示主線程在00:06左右的時(shí)間處于Sleeping狀態(tài),這個(gè)時(shí)候查看其他線程的CPU占用
memoryag
發(fā)現(xiàn)在MemoryAg的線程在占用CPU資源,這種情況下不應(yīng)該認(rèn)為對(duì)應(yīng)的主線程方法耗時(shí),而是要考慮例如內(nèi)存回收或者其他線程占用了CPU資源的情況。 - 還需要注意不是每次點(diǎn)擊"Profiler"都會(huì)正常把信息記錄下來(lái),偶爾會(huì)出現(xiàn)應(yīng)用閃退的情況,這可能是Android Studio的Bug或者是日志太大了的問(wèn)題。這種情況不要灰心,多試幾次就會(huì)好。
Perfetto UI
使用過(guò)程
在Android 10的手機(jī)上,開發(fā)者模式新增加了一個(gè)“系統(tǒng)跟蹤”的功能,我們首先將開發(fā)者模式下的“系統(tǒng)跟蹤”打開:


我們也可以從“類別”選項(xiàng)中選擇我們關(guān)注的信息類別:

設(shè)置完之后我們會(huì)發(fā)現(xiàn)下拉快捷選項(xiàng)多了個(gè)棒棒糖形狀的圖標(biāo)

這個(gè)時(shí)候殺掉我們需要調(diào)試的應(yīng)用,然后點(diǎn)擊開啟棒棒糖,接著打開應(yīng)用,等待應(yīng)用完全打開之后,再點(diǎn)擊一次棒棒糖,結(jié)束錄制。


然后我們保存錄制后的文件,后綴為“.perfetto-trace”
然后我們?cè)?a target="_blank">perfetto ui網(wǎng)站上選擇Open trace file上傳剛剛得到的文件

渲染之后我們可以得到類似于之前systrace的分析,通過(guò)Perfetto UI我們可以更加容易操控

分析過(guò)程
首先我們需要知道,通過(guò)“系統(tǒng)跟蹤”得到的結(jié)果是類似于在Android Studio里面Profiler選擇“Trace System Calls”的結(jié)果,我們可以看到系統(tǒng)中所有CPU在時(shí)間軸的所有運(yùn)行任務(wù)。并且我們也可以看到系統(tǒng)所有的進(jìn)程以及進(jìn)程中所有的線程任務(wù)。

我們展開Perfetto UI的調(diào)試應(yīng)用里面的主線程:

可以看到線程中每個(gè)步驟的耗時(shí)。我們可以通過(guò)不斷的放大來(lái)查看每個(gè)時(shí)間段的系統(tǒng)調(diào)用。
優(yōu)化例子
優(yōu)化前:


可以看出在首頁(yè)inflate的過(guò)程中,有個(gè)一個(gè)“bg_simple_dict_blueriver.jpg”的圖標(biāo)耗時(shí)了29ms加載。分析其所在的代碼:
<ImageView
android:id="@+id/iv_simple_dict_bg"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/bg_simple_dict_blueriver"
android:scaleType="centerCrop"
android:visibility="gone"
/>
由于這個(gè)圖片只會(huì)在網(wǎng)絡(luò)不暢的時(shí)候作為placeholder存在,因此這里簡(jiǎn)單的做法可以將
android:src="@drawable/bg_simple_dict_blueriver"
修改為
tools:src="@drawable/bg_simple_dict_blueriver"
更好的辦法也可以將ImageView改為ViewStub引入,在有需要的時(shí)候再渲染出來(lái),節(jié)省布局渲染時(shí)間。
優(yōu)化后:

可以看出,在優(yōu)化后inflate的時(shí)間由原來(lái)的118ms降低到了103ms,并且在inflate過(guò)程中也沒(méi)有了bg_simple_dict_blueriver.jpg圖片加載的過(guò)程。
啟動(dòng)優(yōu)化
有了以上的Sample Java Methods以及Trace System Calls分析,我們可以得到從宏觀代碼層面以及微觀CPU執(zhí)行層面的啟動(dòng)任務(wù)耗時(shí)。
Proguard & R8


除了業(yè)務(wù)的懶加載處理之外,我們可以看到dex文件的加載時(shí)間占據(jù)了大部分的啟動(dòng)時(shí)間。dex的加載時(shí)間跟代碼量級(jí)有關(guān)。由于長(zhǎng)期的歷史引入了大量了第三方庫(kù)以及本身業(yè)務(wù)增長(zhǎng)帶來(lái)的代碼量增加,我們dex加載的速度也越來(lái)越慢。為了解決dex加載慢的問(wèn)題,我們可以通過(guò)兩個(gè)方面:首先是處理對(duì)dex加載有較大影響的加固過(guò)程,這個(gè)可以跟杭研進(jìn)行溝通處理。第二就是在代碼中加入代碼壓縮和混淆。
代碼壓縮和混淆可以使得dex文件變小,從而減少dex文件加載的時(shí)間。但是從零開始加入代碼壓縮和混淆是一個(gè)非常艱巨的過(guò)程,因?yàn)榇a壓縮和混淆后會(huì)導(dǎo)致很容易發(fā)生ClassNotFoundException以及NoSuchMethodError,并且會(huì)對(duì)諸如push、序列化等依賴類名以及屬性名的代碼失效。加入代碼壓縮和混淆需要額外的細(xì)心和較大的工作量。
在加入代碼壓縮和混淆的過(guò)程中,我們總結(jié)了以下的方法步驟:
本地代碼
- 檢查所有使用注解的代碼,加入proguard 規(guī)則
- 檢查所有JNI相關(guān)代碼,加入proguard 規(guī)則
- 檢查所有使用反射的代碼,加入proguard 規(guī)則
- 檢查所有序列化以及會(huì)使用Json轉(zhuǎn)換為Modle的代碼,加入proguard 規(guī)則
- 檢查所有根據(jù)類名來(lái)使用的代碼,例如Push等,加入proguard 規(guī)則
- 要求以后代碼重構(gòu)需要對(duì)Proguard進(jìn)行相應(yīng)改變
- 要求新增的代碼需要添加Proguard規(guī)則
三方代碼
- 判斷External Libraries中的三方庫(kù)引用是否是release依賴或者debug依賴,如果是的話繼續(xù)
- 判斷l(xiāng)ib庫(kù)是否為目前代碼所需要的,如果引用了沒(méi)有使用或者引用了目前代碼上所有使用的地方都已經(jīng)不再使用,則清理這個(gè)lib并清理相關(guān)沒(méi)有用到的代碼
- 若果lib庫(kù)為目前代碼所需要的,到該lib庫(kù)的官網(wǎng)查找相應(yīng)的proguard規(guī)則,并粘貼到proguard-rules.pro文件中
- 如果該lib官網(wǎng)庫(kù)沒(méi)有相應(yīng)proguard規(guī)則,則觀察lib庫(kù)是否有用到native代碼、annotation或者反射這種需要proguard處理的地方,有的話添加相應(yīng)規(guī)則
- 添加完proguard規(guī)則之后,找到目前項(xiàng)目中使用到這個(gè)庫(kù)的地方,嘗試一下是否會(huì)有崩潰出現(xiàn)
- 如果有崩潰出現(xiàn),根據(jù)崩潰提示增加相應(yīng)proguard規(guī)則





