Multidex(二)之 Dex 預載入優化

augfun發表於2020-11-22

一、前言

Multidex(一)之原始碼解析中我們介紹到MultiDex極有可能出現ANR(Application No Response)的問題,秒秒鐘卡死我們的應用,使用者肯定忍不了要怒解除安裝啊!作為追(被)求(逼)完(無)美(耐)的程式設計師哥哥,我們怎能作壁上觀?Google不做好的事情,我們就自己扛起來!那麼如何對MultiDex這個方案做優化讓它變成好同志呢?

本文就帶你實戰MultiDex的預載入優化。

二、分析

Multidex(一)之原始碼解析中分析過MultiDex第一次載入出現ANR的原因是因為提取Dex以及DexOpt這兩個過程都是耗時的操作,而且他們還都發生在主程式。稍等:主程式,ANR,腦袋裡好像閃現一道靈光,既然在主程式執行會產生ANR,那能不能換個程式執行呢?橘生淮南則為橘,生於淮北則為枳;換個程式說不定就有突破點。說幹就幹,憑藉程式設計師機智的大腦,分毫之間,一個優化方案的雛形已經瞭然於胸:App第一次啟動時單獨開一個額外優化的程式率先進行Dex提取以及DexOpt的操作,與此同時主程式在後臺等待,優化的程式執行完畢之後通知主程式繼續往下執行,主程式在執行MultiDex.install時發現已經是提前優化好了Dex,直接執行,非常快,毫秒級別,不會造成卡頓,愉快的往下繼續執行。

三、優化方案工作流程圖

四、程式碼實戰

在Application的attachBaseContext中執行優化方案;

@Override
protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    //只有主程式以及SDK版本5.0以下才走。
    if (isMainProcess(Application.this) && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
        if (!dexOptDone(base)) {
            preLoadDex(base);
        }
        long startTime = System.currentTimeMillis();
        MultiDex.install(this);
        LogUtil.i(TAG,"MainProcessCostTime:"+(System.currentTimeMillis() - startTime));
    }
}
 
/**
 * 當前版本是否進行過DexOpt操作。
 * @param context
 * @return
 */
private boolean dexOptDone(Context context) {
    SharedPreferences sp = context.getSharedPreferences(
            DeviceUtil.getVersionName(context), MODE_MULTI_PROCESS);
    return sp.getBoolean("dexoptdone", false);
}
 
/**
 * 在單獨程式中提前進行DexOpt的優化操作;主程式進入等待狀態。
 *
 * @param base
 */
public void preLoadDex(Context base) {
    Intent intent = new Intent(Application.this, PreLoadDexActivity.class);
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    base.startActivity(intent);
    while (!dexOptDone(base)) {
        try {
            //主執行緒開始等待;直到優化程式完成了DexOpt操作。
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

然後在PreLoadDexActivity中執行優化的操作,完成後修改標示;

@Override
public void onCreate(Bundle savedInstanceState) {
    requestWindowFeature(Window.FEATURE_NO_TITLE);
    super.onCreate(savedInstanceState);
    overridePendingTransition(0, 0);//取消掉系統預設的動畫。
    getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
    setContentView(R.layout.predexlayout);
 
    new Thread() {
        @Override
        public void run() {
            super.run();
            try {
                long time = System.currentTimeMillis();
                MultiDex.install(getApplication());
                LogUtil.i("lz", "PreLoadDexActivityCostTime:" + (System.currentTimeMillis() - time));
                SharedPreferences sp = getSharedPreferences(
                        DeviceUtil.getVersionName(PreLoadDexActivity.this), MODE_MULTI_PROCESS);
                sp.edit().putBoolean("dexdone", true).commit();
                killCurrentProcess();
            } catch (Exception e) {
                LogUtil.e("loadDex", e.getLocalizedMessage());
                killCurrentProcess();
            }
        }
    }.start();
}

在AndroidManifest中配置:

<activity android:name=".PreLoadDexActivity"
    android:process=":preloaddex"
    android:alwaysRetainTaskState= "false"
    android:theme="@style/PreLoadStyle"
    android:launchMode= "singleTask"
    android:excludeFromRecents= "true"
    android:screenOrientation= "portrait"
    />
執行結果如下:

可以通過Log看到,在優化程式中Dex的提取以及Dexopt的操作耗時近4秒,而在主程式的第二次執行則耗時16毫秒,耗時發生在優化程式中的執行緒中,主程式實際執行MultiDex.install的時候耗時極其短暫;再也不會出現ANR的困擾了。

  • 第一次開啟App,會出現PreLoadDexActivity,略顯突兀,可以再應用的閃屏頁加上這段邏輯,根據標示判斷究竟執行正常邏輯還是優化的邏輯。
  • 關於SharedPreferences程式間不安全的問題:此處的使用只是單向的讀寫,因而不會有這個場景。

五、問題

1、為什麼執行優化操作的時候判斷只有在主程式以及SDK版本5.0以下才執行呢?
如果App是多程式架構的話,Application會執行多次,這個優化過程無需執行多次;而在SDK版本5.0及以上,預設使用ART虛擬機器,與Dalvik的區別在於安裝時已經將全部的Class.dex轉換為了oat檔案,優化過程在安裝時已經完成;因此無需執行。

2、為什麼主程式此時不會ANR?
回憶下ANR的發生場景:Service、BroadCastReceiver、ContentProvider的TimeOut;輸入事件的TimeOut等。當出現ANR時,都會最終呼叫到AMS的appNotResponding()方法
因為主程式此時已經進入後臺,不響應Android螢幕事件。同時也不存在以上發生ANR的場景,因此主程式在後臺Sleep,不會產生ANR。

3、在優化的程式中只是開啟了一個執行緒提前做了MultiDex的工作,那為什麼不直接在主程式中開啟一個子執行緒做同樣工作呢?
Good Question,不愧是善於思考的程式猿!在主程式中直接開啟一個子執行緒確實是可以避免ANR的問題,但是有沒有想到,此時主程式中呼叫到的類,可能會因為SecondaryDex的優化尚未完成或者沒有被加入到ClassLoader中而導致畫面太美不敢看的ClassNotFoundException。

相關文章