1. 程式人生 > >探究Android的冷啟動優化

探究Android的冷啟動優化

本文依據平臺如下

  • 機型: 魅藍Note(高通615真八核/2G/1080P/4.4)
  • 效果:1.1s -> 0.7s(實際使用者看到的假介面時間更短)

1. 啟動過程概述

在應用層,普通APP啟動過程大致如下:

  1. 載入Application
    • 靜態程式碼段/建構函式
    • onCreate方法
  2. 載入主Activity
    • 靜態程式碼段/建構函式
    • 訊息佇列第一次迴圈: onCreate,通過setContentview解析、載入xml
    • 訊息佇列第二次迴圈: 被動地呼叫Choreographerd中的FrameDisplayEventReceiver的run()進行進行實際繪製

為了提高使用者感知,我希望在主執行緒中執行的順序如下(注意本流程不適用於外掛化的App):

  1. 儘快顯示DecoView(Main Thread)(顯示Theme中定義的ActionBar、背景等)
  2. 儘快顯示xml中的靜態View(Main Thread)(顯示xml中的佈局)
  3. 載入第三方黑盒SDK(Main Thread)
  4. 進行網路、圖片等框架的構造(Main Thread)
  5. 通過框架進行業務請求(Gson/OkHttp等, Worker Thread),並更新View

不建議在Application中初始化耗時任務,它將直接導致白屏

2. 使用者感知優化

本部分可以提高上文1,2,3的使用者體驗

2.1. 載入偽背景(0.1~0.2s)

DecoView的優先順序比setContentView

優先順序更高,所以可以讓DecoView顯示一個偽啟動背景介面,而不是白屏黑屏或者沒介面甩鍋給手機廠商,讓使用者感受到App正在載入是一個好的選擇。

繪製一個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佈局優化是老生常談的話題了,本質是減少無謂的繪製,網上面試寶典很多,這裡就也不介紹了。解決方法如下:

  1. 使用Include,Merge,viewStub簡化佈局
  2. 使用相對佈局,layer-list降低樹的層級
  3. 使用gone標籤可以跳過繪製
  4. 被遮擋的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請求與之後拼裝的時間加起來,比動態代理要多的多