探究Android的冷啟動優化
本文依據平臺如下
- 機型: 魅藍Note(高通615真八核/2G/1080P/4.4)
- 效果:1.1s -> 0.7s(實際使用者看到的假介面時間更短)
1. 啟動過程概述
在應用層,普通APP啟動過程大致如下:
- 載入Application
- 靜態程式碼段/建構函式
- onCreate方法
- 載入主Activity
- 靜態程式碼段/建構函式
- 訊息佇列第一次迴圈: onCreate,通過setContentview解析、載入xml
- 訊息佇列第二次迴圈: 被動地呼叫Choreographerd中的FrameDisplayEventReceiver的run()進行進行實際繪製
為了提高使用者感知,我希望在主執行緒中執行的順序如下(注意本流程不適用於外掛化的App):
- 儘快顯示DecoView(Main Thread)(顯示Theme中定義的ActionBar、背景等)
- 儘快顯示xml中的靜態View(Main Thread)(顯示xml中的佈局)
- 載入第三方黑盒SDK(Main Thread)
- 進行網路、圖片等框架的構造(Main Thread)
- 通過框架進行業務請求(Gson/OkHttp等, Worker Thread),並更新View
不建議在Application中初始化耗時任務,它將直接導致白屏
2. 使用者感知優化
本部分可以提高上文1,2,3的使用者體驗
2.1. 載入偽背景(0.1~0.2s)
DecoView的優先順序比setContentView
繪製一個App啟動的草圖,如下,一個是Toolbar
,一個是背景
<layer-list
xmlns:android="http://schemas.android.com/apk/res/android"
android:opacity="opaque"
>
<item android:gravity="top">
<shape android:shape="rectangle">
<solid android:color="#c8ececec"/>
</shape>
</item>
<item
android:top="75dp"
android:gravity="top">
<shape android:shape="rectangle">
<solid android:color="@color/primary"/>
</shape>
</item>
</layer-list>
設定windowBackground
<style name="ColdStartTheme" parent="APPTheme">
<item name="android:windowBackground">@drawable/cold_start_bg</item>
</style>
在啟動時先載入了偽背景,然後才載入了真正的View元素
偽背景載入 → 繪製完成View最終可以讓使用者覺得“提高”了0.1~0.2s的速度
上述方案均不能很好處理狀態列,如果你使用Translucent,慎用
2.2. XML佈局優化
此部分適用於解析、處理、繪製靜態xml時的優化
xml佈局優化是老生常談的話題了,本質是減少無謂的繪製,網上面試寶典很多,這裡就也不介紹了。解決方法如下:
- 使用Include,Merge,viewStub簡化佈局
- 使用相對佈局,layer-list降低樹的層級
- 使用gone標籤可以跳過繪製
- 被遮擋的view避免重複繪製
3. 延後啟動耗時框架
本部分不能壓縮總時間,只是將耗時操作移動到後面而已,可以讓白屏時間減少0.2~0.3s(取決於框架數量)。
3.1. 實現方法
在onCreate()的最後,加入post操作,即可實現在繪製XmlView完成後再進行非UI的耗時操作
getWindow().getDecorView().post(new Runnable() {
@Override public void run() {
//載入Applicaiton中的框架 40+ms
GlobalContext.startThirdFrameWork();
//構建網路框架 120ms
repo = SquareUtils.getRetrofit(URL).create(GithubService.class);
//進行ssl庫的初始化請求 40+ms
onRefresh();
}
});
3.2. 實現原理
在XML被inflate後,需要通過mDecoView.addView(xmlView)
進行新增。
addview最終呼叫ViewRootImpl
的方法scheduleTraversals()
,進行了訊息佇列的優先獨佔操作
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
接著呼叫doTraversal()
釋放
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
SyncBarrier擁有訊息佇列的獨佔性,當使用SyncBarrier
時,後面的訊息將被阻塞,這樣在主執行緒中就有更多的CPU時間可以分給WMS進行繪圖了。在View繪製完成後,解除SyncBarrier後才會呼叫我們在上文Post的耗時框架載入任務,這樣就實現了延遲載入。
4. 多執行緒初始化
此部分真的可以壓縮啟動時間,但是對SDK執行緒安全有一定的要求,在黑盒SDK下容易出現問題
下文複用了OkHttp中的單例Worker執行緒池,節省了0.16s的啟動時間
SquareUtils.getDispatcher().executorService().execute(new Runnable() {
@Override public void run() {
Log.d(TAG, "run: " + System.currentTimeMillis());
//42ms
GlobalContext.startThirdFrameWork();
//120ms
repo = SquareUtils.getRetrofit(DanbooruAPI.KONACHAN).create(DanbooruAPI.class);
runOnUiThread(new Runnable() {
@Override public void run() {
//40ms
onRefresh();
}
});
}
});
最後,你就能比較充分利用你的真八核手機
主執行緒: 解析xml ----------addView()--------| → 更新介面
執行緒池: 初始化框架 --post(請求網路)---wait()--|
5. 混淆
經過測試,混淆在一定程度上可以提高速度,屬於免費的效能提升,但是不是非常明顯,大概只有100ms
混淆後要記得測試
6. 總結
通過上述方法,可以壓榨0.3~0.6s的時間,讓使用者能夠更快的啟動APP
附錄. Retrofit框架載入時間分析
Retrofit 在知乎上有人這樣回答的,大意是動態代理 == 反射 == 慢
,這就是典型的半桶水,不懂裝懂。
通過對每個方法進行統計後,結果卻是這樣的:
retrofit構造(128ms)
- 構造OkHttp:121ms, 其中javax.ssl構建耗時117ms,呼叫的是一個SSL遍歷native操作,這個基本無法避免;快取檔案初始化1ms
- 構造GsonFactory 4ms: 主要是classloader載入的時間
- 其他 3ms
retrofit訪問網路前介面的拼裝(42ms)
- RxJava框架: 12ms
- 動態代理: 1ms
- Gson庫: 27ms,主要進行反射操作
- 其他: 2ms
隨著SSL的普及,javax.ssl必然會被載入,這個100ms的時間在native中黑盒執行,很難避免,只能等手機ROM去優化嘍;剩下的就是Gson的時間比較久,這個時間還是可以接受的。
從上面也可以看出,與動態代理相關的時間,並沒有想象中那麼慢,不要看到反射就覺得慢,網路I/O請求與之後拼裝的時間加起來,比動態代理要多的多