應(yīng)用啟動(dòng)類型
- 冷啟動(dòng)
場(chǎng)景:開機(jī)后第一次啟動(dòng)應(yīng)用 或者 應(yīng)用被殺死后再次啟動(dòng)
生命周期:Process.start->Application創(chuàng)建->attachBaseContext->onCreate->onStart->onResume->Activity生命周期
啟動(dòng)速度:在幾種啟動(dòng)類型中最慢,也是我們優(yōu)化啟動(dòng)速度最大的攔路虎
- 溫啟動(dòng)
場(chǎng)景:應(yīng)用已經(jīng)啟動(dòng),返回鍵退出
生命周期:onCreate->onStart->onResume->Activity生命周期
啟動(dòng)速度:較快
- 熱啟動(dòng)
場(chǎng)景:Home鍵最小化應(yīng)用
生命周期:onResume->Activity生命周期
啟動(dòng)速度:快
從上面的總結(jié)可以看出,在應(yīng)用的啟動(dòng)過程中,冷啟動(dòng)是最慢最耗時(shí)的,系統(tǒng)以及應(yīng)用本身都有大量的工作需要處理,所以,冷啟動(dòng)對(duì)于應(yīng)用的啟動(dòng)速度是最具挑戰(zhàn)以及最有必要進(jìn)行優(yōu)化的。
冷啟動(dòng)流程
冷啟動(dòng)指的是應(yīng)用程序從進(jìn)程在系統(tǒng)不存在,到系統(tǒng)創(chuàng)建應(yīng)用運(yùn)行進(jìn)程空間的過程。冷啟動(dòng)通常會(huì)發(fā)生在一下兩種情況:
設(shè)備啟動(dòng)以來首次啟動(dòng)應(yīng)用程序
系統(tǒng)殺死應(yīng)用程序之后再次啟動(dòng)應(yīng)用程序
在冷啟動(dòng)的最開始,系統(tǒng)需要負(fù)責(zé)做三件事:
加載以及啟動(dòng)app
app啟動(dòng)之后立刻顯示一個(gè)空白的預(yù)覽窗口
創(chuàng)建app進(jìn)程
一旦系統(tǒng)完成創(chuàng)建app進(jìn)程后,app進(jìn)程將要接著負(fù)責(zé)完成下面的工作:
創(chuàng)建Application對(duì)象
創(chuàng)建并且啟動(dòng)主線程ActivityThread
創(chuàng)建啟動(dòng)第一個(gè)Activity
Inflating views
布局屏幕
執(zhí)行第一次繪制
一旦app進(jìn)程完完成了第一次繪制工作,系統(tǒng)進(jìn)程就會(huì)用main activity替換前面顯示的預(yù)覽窗口,這個(gè)時(shí)候,用戶就可以正式開始與app進(jìn)行交互了。
從冷啟動(dòng)的流程看,我們無法干預(yù)app進(jìn)程創(chuàng)建等系統(tǒng)操作,我們能夠干預(yù)的有:
預(yù)覽窗口
Application生命周期回調(diào)
Activity生命周期回調(diào)
優(yōu)化分析測(cè)量工具
對(duì)研發(fā)人員來說,啟動(dòng)速度是我們的“門面”,它清清楚楚可以被所有人看到,我們都希望自己應(yīng)用的啟動(dòng)速度可以秒殺所有競(jìng)爭(zhēng)對(duì)手。
“工欲善其事必先利其器”,我們需要先找到一款適合做啟動(dòng)優(yōu)化分析的工具或者方式。
- adb shell am start -W [packageName]/[ packageName. AppstartActivity]
在統(tǒng)計(jì) app 啟動(dòng)時(shí)間時(shí),系統(tǒng)為我們提供了 adb 命令,可以輸出啟動(dòng)時(shí)間。系統(tǒng)在繪制完成后,ActivityManagerService 會(huì)回調(diào)該方法,但是能夠方便我們通過腳本多次啟動(dòng)測(cè)量 TotalTime,對(duì)比版本間啟動(dòng)時(shí)間差異。但是統(tǒng)計(jì)時(shí)間不如 Systrace 準(zhǔn)確。
- 代碼埋點(diǎn)
通過代碼埋點(diǎn)來準(zhǔn)確獲取記錄每個(gè)方法的執(zhí)行時(shí)間,知道哪些地方耗時(shí),然后再有針對(duì)性地優(yōu)化。例如通過在 app 啟動(dòng)生命周期中,關(guān)鍵位置加入時(shí)間點(diǎn)記錄,達(dá)到測(cè)量目的;又例如可以在 Application 的 attachBaseContext方法中記錄開始時(shí)間,然后在啟動(dòng)的第一個(gè) Activity 的 onWindowFocusChanged方法記錄結(jié)束時(shí)間。
但是從用戶點(diǎn)擊 app Icon 到 Application 被創(chuàng)建,再到 Activity 的渲染,中間還是有很多步驟的,比如冷啟動(dòng)的進(jìn)程創(chuàng)建過程,而這個(gè)時(shí)間用此版本是沒辦法統(tǒng)計(jì)了,必須得承受這點(diǎn)數(shù)據(jù)的不準(zhǔn)確性。
- Nanoscope
Nanoscope 非常真實(shí),不過暫時(shí)只支持 Nexus 6 和 x86 模擬器。
- Simpleperf
Simpleperf 的火焰圖并不適合做啟動(dòng)流程分析。
- TraceView
通過 TraceView 主要可以得到兩種數(shù)據(jù):?jiǎn)未螆?zhí)行耗時(shí)的方法 以及 執(zhí)行次數(shù)多的方法。但是TraceView 性能耗損太大,不能比較正確反映真實(shí)情況。
- Systrace
Systrace 能夠追蹤關(guān)鍵系統(tǒng)調(diào)用的耗時(shí)情況,如系統(tǒng)的 IO 操作、內(nèi)核工作隊(duì)列、CPU 負(fù)載、Surface 渲染、GC 事件以及 Android 各個(gè)子系統(tǒng)的運(yùn)行狀況等。但是不支持應(yīng)用程序代碼的耗時(shí)分析。
綜上所述,這幾種方式都各有各的優(yōu)點(diǎn)以及缺點(diǎn),我們都要掌握。
但是有沒有一種比較折中比較理想的方案呢?有的。
- “Systrace + 函數(shù)插樁”
除了能夠看到例如 GC、System Server、CPU 調(diào)度等系統(tǒng)調(diào)用的耗時(shí),還能夠通過 Android 工程編譯的過程中,在指定的方法前后,自動(dòng)化插入插樁函數(shù),統(tǒng)計(jì)方法執(zhí)行時(shí)間。通過插樁,我們可以看到應(yīng)用主線程和其他線程的函數(shù)調(diào)用流程。它的實(shí)現(xiàn)原理非常簡(jiǎn)單,就是將下面的兩個(gè)函數(shù) 通過用ASM框架修改字節(jié)碼的方式 分別插入到每個(gè)方法的入口和出口。
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n109" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">class TraceMethod {
public static void i() {
Trace.beginSection();
}
public static void o() {
Trace.endSection();
}
}</pre>
當(dāng)然這里面有非常多的細(xì)節(jié)需要考慮,比如怎么樣降低插樁對(duì)性能的影響、哪些函數(shù)需要被排除掉。函數(shù)插樁后的效果如下:
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n111" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">class Test {
public void test() {
TraceMethod.i();
// 原來的工作
TraceMethod.o();
}
}</pre>
只有準(zhǔn)確的數(shù)據(jù)評(píng)估才能指引優(yōu)化的方向,這一步是非常重要的。沒有充分評(píng)估或者評(píng)估使用了錯(cuò)誤的方法,最終得到了錯(cuò)誤的方向,會(huì)導(dǎo)致最后發(fā)現(xiàn)根本達(dá)不到預(yù)期的優(yōu)化效果。
啟動(dòng)優(yōu)化方法
在拿到整個(gè)啟動(dòng)流程的全景圖之后,我們可以清楚地看到這段時(shí)間內(nèi)系統(tǒng)、應(yīng)用各個(gè)進(jìn)程和線程的運(yùn)行情況,現(xiàn)在我們要開始真正開始“干活”了。
具體的優(yōu)化方式,我把它們分為預(yù)覽窗口優(yōu)化、業(yè)務(wù)梳理、業(yè)務(wù)優(yōu)化、多進(jìn)程優(yōu)化、線程優(yōu)化、GC 優(yōu)化和系統(tǒng)調(diào)用優(yōu)化。業(yè)務(wù)梳理、業(yè)務(wù)優(yōu)化、線程優(yōu)化、GC 優(yōu)化、系統(tǒng)調(diào)用優(yōu)化和布局優(yōu)化。
預(yù)覽窗口優(yōu)化
當(dāng)用戶點(diǎn)擊應(yīng)用桌面圖標(biāo)啟動(dòng)應(yīng)用的時(shí)候,利用提前展示出來的 Window,快速展示出一個(gè)界面,用戶只需要很短的時(shí)間就可以看到“預(yù)覽頁”,這種完全“跟手”的感覺在高端機(jī)上體驗(yàn)非常好,但對(duì)于中低端機(jī),會(huì)把總的的閃屏?xí)r間變得更長(zhǎng)。
如果點(diǎn)擊圖標(biāo)沒有響應(yīng),用戶主觀上會(huì)認(rèn)為是手機(jī)系統(tǒng)響應(yīng)比較慢。所以比較推薦的做法是,只在 Android 6.0 或者 Android 7.0 以上才啟用“預(yù)覽窗口”方案,讓手機(jī)性能好的用戶可以有更好的體驗(yàn)。
要實(shí)現(xiàn)預(yù)覽窗口的顯示,只需要在利用 activity 的windowBackground主題屬性提供一個(gè)簡(jiǎn)單的自定義 drawable 給啟動(dòng)的 activity,如下:
Layout XML file:
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n123" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><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/product_logo_144dp"
android:gravity="center"/>
</item>
</layer-list></pre>
Manifest file:
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n125" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><activity ...
android:theme="@style/AppTheme.Launcher" /></pre>
這樣一個(gè) activity 啟動(dòng)的時(shí)候,就會(huì)先顯示一個(gè)預(yù)覽窗口,給用戶快速響應(yīng)的體驗(yàn)。當(dāng) activity想要恢復(fù)原來 theme,可以通過在調(diào)用super.onCreate() 和setContentView()之前調(diào)用 setTheme(R.style.AppTheme),如下:
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n127" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public class MyMainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
// Make sure this is before calling super.onCreate
setTheme(R.style.Theme_MyApp);
super.onCreate(savedInstanceState);
// ...
}
}</pre>
業(yè)務(wù)梳理
不要一股腦把全部初始化工作放在 Application 中做,需要梳理清楚當(dāng)前啟動(dòng)過程正在運(yùn)行的每一個(gè)模塊,哪些是一定需要的、哪些可以砍掉、哪些可以懶加載。但是需要注意的是,懶加載要防止集中化,否則容易出現(xiàn)首頁顯示后用戶無法操作的情形。總的來說,用以下四個(gè)維度分整理啟動(dòng)的各個(gè)點(diǎn):
必要且耗時(shí):?jiǎn)?dòng)初始化,考慮用線程來初始化。
必要不耗時(shí):首頁繪制。
非必要但耗時(shí):數(shù)據(jù)上報(bào)、插件初始化。
非必要不耗時(shí):不用想,這塊直接去掉,在需要用的時(shí)再加載。
把數(shù)據(jù)整理出來后,按需實(shí)現(xiàn)加載邏輯,采取分步加載、異步加載、延期加載策略,如下圖所示:
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n140" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"></pre>
一句話概述,要提高應(yīng)用的啟動(dòng)速度,核心思想是在啟動(dòng)過程中少做事情,越少越好。
業(yè)務(wù)優(yōu)化
通過梳理之后,剩下的都是啟動(dòng)過程一定要用的模塊。這個(gè)時(shí)候,我們只能硬著頭皮去做進(jìn)一步的優(yōu)化。優(yōu)化前期需要“抓大放小”,先看看主線程究竟慢在哪里。最理想是通過算法進(jìn)行優(yōu)化,例如一個(gè)數(shù)據(jù)解密操作需要 1 秒,通過算法優(yōu)化之后變成 10 毫秒。退而求其次,我們要考慮這些任務(wù)是不是可以通過異步線程預(yù)加載實(shí)現(xiàn),但需要注意的是過多的線程預(yù)加載會(huì)讓我們的邏輯變得更加復(fù)雜。
業(yè)務(wù)優(yōu)化做到后面,會(huì)發(fā)現(xiàn)一些架構(gòu)和歷史包袱會(huì)拖累我們前進(jìn)的步伐。比較常見的是一些事件會(huì)被各個(gè)業(yè)務(wù)模塊監(jiān)聽,大量的回調(diào)導(dǎo)致很多工作集中執(zhí)行,部分框架初始化“太厚”,例如一些插件化框架,啟動(dòng)過程各種反射、各種 Hook,整個(gè)耗時(shí)至少幾百毫秒。還有一些歷史包袱非常沉重,而且“牽一發(fā)動(dòng)全身”,改動(dòng)風(fēng)險(xiǎn)比較大。但是我想說,如果有合適的時(shí)機(jī),我們依然需要勇敢去償還這些“歷史債務(wù)”。
多進(jìn)程優(yōu)化
Android app 是支持多進(jìn)程的,在 Manifest 中只要在組件聲明中加入android:process屬性就可以讓組件在啟動(dòng)時(shí)運(yùn)行在不同的進(jìn)程中。舉個(gè)例子: 對(duì)于多進(jìn)程 app ,可能擁有主進(jìn)程,插件進(jìn)程以及下載進(jìn)程,但開發(fā)者只能在 Manifest 中聲明一個(gè) Application 組件,如果對(duì)應(yīng)不同進(jìn)程的組件啟動(dòng)時(shí),系統(tǒng)會(huì)創(chuàng)建三個(gè)進(jìn)程,創(chuàng)建三個(gè) Application 對(duì)象,同時(shí)attachBaseContext、onCreate等生命周期回調(diào)方法也會(huì)被調(diào)用三次。
但是每個(gè)進(jìn)程需要初始化的內(nèi)容肯定是不一樣的,所以,為了防止資源的浪費(fèi),我們需要在Application 中區(qū)分進(jìn)程,對(duì)應(yīng)進(jìn)程只初始化對(duì)應(yīng)的內(nèi)容。
線程優(yōu)化
線程優(yōu)化分兩方面:
第一,耗時(shí)任務(wù)異步化。子線程處理耗時(shí)任務(wù),主線程做的事情越少,越早進(jìn)入Acitivity繪制階段,界面越早展現(xiàn)。例如不在主線程做如 IO 、網(wǎng)絡(luò)等耗時(shí)操作。但是要注意,子線程不能阻塞主線程。
第二,線程池管理線程,控制線程的數(shù)量。線程數(shù)量太多會(huì)相互競(jìng)爭(zhēng) CPU 資源,導(dǎo)致分給主線程的時(shí)間片減少,從而導(dǎo)致啟動(dòng)速度變慢。線程切換的數(shù)據(jù)我們可以通過卡頓優(yōu)化中學(xué)到的 sched 文件查看,這里特別需要注意 nr_involuntary_switches 被動(dòng)切換的次數(shù)。
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n152" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">proc/[pid]/sched:
nr_voluntary_switches:主動(dòng)上下文切換次數(shù),因?yàn)榫€程無法獲取所需資源導(dǎo)致上下文切換,最普遍的是 IO。
nr_involuntary_switches:被動(dòng)上下文切換次數(shù),線程被系統(tǒng)強(qiáng)制調(diào)度導(dǎo)致上下文切換,例如大量線程在搶占 CPU。 </pre>
第三,避免主線程與子線程之間的鎖阻塞等待。有一次我們把主線程內(nèi)的一個(gè)耗時(shí)任務(wù)放到線程中并發(fā)執(zhí)行,但是發(fā)現(xiàn)這樣做根本沒起作用。仔細(xì)檢查后發(fā)現(xiàn)線程內(nèi)部會(huì)持有一個(gè)鎖,主線程很快就有其他任務(wù)因?yàn)檫@個(gè)鎖而等待。通過 Systrace 可以看到鎖等待的事件,我們需要排查這些等待是否可以優(yōu)化,特別是防止主線程出現(xiàn)長(zhǎng)時(shí)間的空轉(zhuǎn)。
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n154" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">
GC優(yōu)化
在啟動(dòng)過程,要盡量減少 GC 的次數(shù),避免造成主線程長(zhǎng)時(shí)間的卡頓,特別是對(duì) Dalvik 來說,我們可以通過 Systrace 單獨(dú)查看整個(gè)啟動(dòng)過程 GC 的時(shí)間。
啟動(dòng)過程避免進(jìn)行大量的字符串操作,特別是序列化跟反序列化過程。一些頻繁創(chuàng)建的對(duì)象,例如網(wǎng)絡(luò)庫和圖片庫中的 Byte 數(shù)組、Buffer 可以復(fù)用。如果一些模塊實(shí)在需要頻繁創(chuàng)建對(duì)象,可以考慮移到 Native 實(shí)現(xiàn)。
Java 對(duì)象的逃逸也很容易引起 GC 問題,我們?cè)趯懘a的時(shí)候比較容易忽略這個(gè)點(diǎn)。我們應(yīng)該保證對(duì)象生命周期盡量的短,在棧上就進(jìn)行銷毀。
系統(tǒng)調(diào)用優(yōu)化
部分系統(tǒng)的API使用是阻塞性的,文件很小可能無法感知,當(dāng)文件過大,或者使用頻繁時(shí),可能造成阻塞。例如:SharedPreference.Editor 的提交操作建議使用異步的 apply,而不是阻塞的 commit。
通過 systrace 的 System Service 類型,我們可以看到啟動(dòng)過程 System Server 的CPU 工作情況。在啟動(dòng)過程,我們盡量不要做系統(tǒng)調(diào)用,例如 PackageManagerService 操作、Binder 調(diào)用等待。
在啟動(dòng)過程也不要過早地拉起應(yīng)用的其他進(jìn)程,System Server 和新的進(jìn)程都會(huì)競(jìng)爭(zhēng) CPU 資源。特別是系統(tǒng)內(nèi)存不足的時(shí)候,當(dāng)我們拉起一個(gè)新的進(jìn)程,可能會(huì)成為“壓死駱駝的最后一根稻草”。它可能會(huì)觸發(fā)系統(tǒng)的 low memorykiller 機(jī)制,導(dǎo)致系統(tǒng)殺死和拉起(保活)大量的進(jìn)程,從而影響前臺(tái)進(jìn)程的 CPU。舉個(gè)例子,之前一個(gè)程序在啟動(dòng)過程會(huì)拉起下載和視頻播放進(jìn)程,改為按需拉起后,線上啟動(dòng)時(shí)間提高了 3%,對(duì)于 1GB 以下的低端機(jī)優(yōu)化,整個(gè)啟動(dòng)時(shí)間可以優(yōu)化 5%~8%,效果還是非常明顯的。
布局優(yōu)化
布局越復(fù)雜,測(cè)量布局繪制的時(shí)間就越長(zhǎng)。主要做到以下幾點(diǎn):
布局的層級(jí)越少,加載速度越快。
一個(gè)控件的屬性越少,解析越快,刪除控件中的無用屬性。
使用<ViewStub/>標(biāo)簽加載一些不常用的布局,做到使用時(shí)在加載。
使用<merge/>標(biāo)簽減少布局的嵌套層次。
盡可能少用wrap_content,wrap_content會(huì)增加布局measure時(shí)的計(jì)算成本,已知寬高為固定值時(shí),不用wrap_content。
啟動(dòng)優(yōu)化進(jìn)階方法
還有什么方法可以做進(jìn)一步優(yōu)化嗎?
數(shù)據(jù)重排
如果我們?cè)趩?dòng)的過程中需要讀一個(gè)文件 test.io 的 1KB 數(shù)據(jù),而我們的 buffer 不小心寫成 1byte,那么總共要讀取 1000 次。系統(tǒng)是否會(huì)真的發(fā)起 1000 次磁盤 IO 呢?
事實(shí)上 1000 次讀操作只是我們發(fā)起的次數(shù),并不是真正的磁盤 I/O 次數(shù)。你可以參考下面 Linux 文件 I/O流程。
Linux 文件系統(tǒng)從磁盤讀文件的時(shí)候,會(huì)以 block 為單位去磁盤讀取,一般 block 大小是 4KB。也就是說一次磁盤讀寫大小至少是 4KB,然后會(huì)把 4KB 數(shù)據(jù)放到頁緩存 Page Cache 中。如果下次讀取文件數(shù)據(jù)已經(jīng)在頁緩存中,那就不會(huì)發(fā)生真實(shí)的磁盤 I/O,而是直接從頁緩存中讀取,大大提升了讀的速度。所以上面的例子,我們雖然讀了 1000 次,但事實(shí)上只會(huì)發(fā)生一次磁盤 I/O,其他的數(shù)據(jù)都會(huì)在頁緩存中得到。
Dex 文件用的到的類和安裝包 APK 里面各種資源文件一般都比較小,但是讀取非常頻繁。我們可以利用系統(tǒng)這個(gè)機(jī)制將它們按照讀取順序重新排列,減少真實(shí)的磁盤 I/O 次數(shù)。
在啟動(dòng)優(yōu)化中,數(shù)據(jù)的重排主要有兩方面:類重排 以及 資源文件重排。
類重排
類重排的實(shí)現(xiàn)通過 ReDex的 Interdex調(diào)整類在 Dex 中的排列順序。
根據(jù)interdex官方介紹的原理,我們可以知道要實(shí)現(xiàn)這個(gè)優(yōu)化需要解決三個(gè)問題:
- 如何獲取啟動(dòng)時(shí)加載類的序列?
redex中的方案是dump出程序啟動(dòng)時(shí)的hprof文件,再?gòu)闹蟹治龀黾虞d的類,比較麻煩。這里我們采用的方案是hook住ClassLoader.findClass方法,在系統(tǒng)加載類時(shí)日志打印出類名,這樣分析日志就可以得到啟動(dòng)時(shí)加載的類序列了。
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n196" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">class GetClassLoader extends PathClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 將類名 name 記錄到文件
writeToFile(name, "coldstart_classes.txt");
return super.findClass(name);
}
}</pre>
- 如何把需要的類放到主dex中?
redex的做法應(yīng)該是解析出所有dex中的類,再按配置的加載類序列,從主dex開始重新生成各個(gè)dex,所以會(huì)打亂原有的dex分布。而在手q中,分dex規(guī)則是編譯腳本中維護(hù)的,因此我們可以修改分包邏輯,將需要的類放到主dex。
- 如何調(diào)整主dex中類的順序?
開源就是好。Android編譯時(shí)把.class轉(zhuǎn)換成.dex是依靠dx.bat,這個(gè)工具實(shí)際執(zhí)行的是sdk中的dx.jar。我們可以修改dx的源碼,替換這個(gè)jar包,就可以執(zhí)行自定義的dx邏輯了。
資源文件重排
Facebook 在比較早的時(shí)候就使用“資源熱圖”來實(shí)現(xiàn)資源文件的重排,最近支付寶在《通過安裝包重排布優(yōu)化 Android 端啟動(dòng)性能》中也詳細(xì)講述了資源重排的原理和落地方法。
類的加載
加載類的過程有一個(gè) verify class 的步驟,它需要需要校驗(yàn)方法的每一個(gè)指令,是一個(gè)比較耗時(shí)的操作。
verify步驟可以看這篇文章:微信 Android 熱補(bǔ)丁實(shí)踐演進(jìn)之路
我們可以通過 Hook 來去掉 verify 這個(gè)步驟,這對(duì)啟動(dòng)速度有幾十毫秒的優(yōu)化。不過我想說,其實(shí)最大的優(yōu)化場(chǎng)景在于首次和覆蓋安裝時(shí)。以 Dalvik 平臺(tái)為例,一個(gè) 2MB 的 Dex 正常需要 350 毫秒,將 classVerifyMode 設(shè)為 VERIFY_MODE_NONE 后,只需要150 毫秒,節(jié)省超過 50% 的時(shí)間。
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n212" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">// Dalvik Globals.h
gDvm.classVerifyMode = VERIFY_MODE_NONE;
// Art runtime.cc
verify_ = verifier::VerifyMode::kNone;</pre>
但是 ART 平臺(tái)要復(fù)雜很多,Hook 需要兼容幾個(gè)版本。而且在安裝時(shí)大部分 Dex 已經(jīng)優(yōu)化好了,去掉 ART 平臺(tái)的 verify 只會(huì)對(duì)動(dòng)態(tài)加載的 Dex 帶來一些好處。Atlas 中的 dalvik_hack-3.0.0.5.jar可以通過下面的方法去掉 verify,但是當(dāng)前沒有支持 ART 平臺(tái)。
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n214" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">AndroidRuntime runtime = AndroidRuntime.getInstance();
runtime.init(context);
runtime.setVerificationEnabled(false);</pre>
這個(gè)黑科技可以大大降低首次啟動(dòng)的速度,代價(jià)是對(duì)后續(xù)運(yùn)行會(huì)產(chǎn)生輕微的影響。同時(shí)也要考慮兼容性問題,暫時(shí)不建議在 ART 平臺(tái)使用。
黑科技
?;?/strong>
講到黑科技,你可能第一個(gè)想到的就是保活。?;羁梢詼p少 Application 創(chuàng)建跟初始化的時(shí)間,讓冷啟動(dòng)變成溫啟動(dòng)。不過在 Target 26 之后,?;畹拇_變得越來越難。對(duì)于大廠來說,可能需要尋求廠商合作的機(jī)會(huì)。
插件化和熱修復(fù)
它們真的那么好嗎?事實(shí)上大部分的框架在設(shè)計(jì)上都存在大量的 Hook 和私有 API 調(diào)用,帶來的缺點(diǎn)主要有兩個(gè):
穩(wěn)定性。雖然大家都號(hào)稱兼容 100% 的機(jī)型,由于廠商的兼容性、安裝失敗、dex2oat 失敗等原因,還是會(huì)有那么一些代碼和資源的異常。Android P 推出的 non-sdk-interface 調(diào)用限制,以后適配只會(huì)越來越難,成本越來高。
性能。Android Runtime 每個(gè)版本都有很多的優(yōu)化,因?yàn)椴寮蜔嵝迯?fù)用到的一些黑科技,導(dǎo)致底層 Runtime 的優(yōu)化我們是享受不到的。Tinker 框架在加載補(bǔ)丁后,應(yīng)用啟動(dòng)速度會(huì)降低 5%~10%。
總的來說,對(duì)于黑科技我們需要慎重,當(dāng)你足夠了解它們內(nèi)部的機(jī)制以后,可以選擇性的使用。
總結(jié)
以上就是本人學(xué)習(xí)過程中對(duì)啟動(dòng)優(yōu)化相關(guān)內(nèi)容的總結(jié),今天的文章就到這里,感謝您的閱讀。