MultiDex(三)之非同步載入優化

貌似許亞軍發表於2016-12-26

一、前言

在上一篇文章《Multidex(二)之Dex預載入優化》中我們提到主程式中直接開啟一個子執行緒執行MultiDex的工作確實可以避免ANR的問題,然而此時主程式中呼叫到的類,可能會因為SecondaryDex的優化尚未完成或者沒有被加入到ClassLoader中而導致畫面太美不敢看的ClassNotFoundException。如何是好?明知山有虎,偏往虎山行!

本文就帶你實戰MultiDex的非同步載入優化。

二、分析

既然我們要做的是非同步載入優化,那毋庸置疑MultiDex.install是要放在子執行緒了,這步簡單。接下來我們分析下ClassNotFoundException的原因,在非主Dex沒有被優化、載入到ClassLoader之前引用到了其中的Class,肯定找不到秒秒鐘異常給你看。那問題就轉換成了下面這兩個:

  • 如何保證程式的入口類以及入口類的引用類都在主Dex?
  • 以及在非主Dex類載入的時候如何進行判斷干預?

問題一:在保證主Dex不被撐爆的前提下,我們可以定義一個Task對Gradle打包的流程進行自定義,將程式的入口類以及入口類的引用類都放到主Dex中。
問題二:在非主Dex類載入的時候進行校驗,當非同步優化還沒完成的時候返回或者載入一個Loading的介面提示使用者等待。如何對一個具體的類進行校驗呢?看起來比較複雜。換個思路,我們把基類都放到主Dex中,保證非主Dex載入的時候都是呼叫四大元件,然後進行Hook。這樣非主Dex被呼叫的時候都是通過四大元件來呼叫的,而基礎類都在主Dex已經提前被載入,可以放心呼叫。

三、上程式碼,Show The Code

Application非同步執行Multidex.install;

public static boolean dexLoadDone;//標示非主Dex有沒有被載入成功

@Override
protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    hookInstrumentation();
    new Thread(){
        @Override
        public void run() {
            super.run();
            //子執行緒執行,完成後修改標示
            MultiDex.install(ChrApplication.this);
            dexLoadDone = true;
        }
    }.start();
}複製程式碼

干預打包流程,保證入口類以及入口類的引用類都放在主Dex中。

MultiDex(三)之非同步載入優化
build.gradle截圖

如何對四大元件進行Hook?此處分析Activity為例。
startActivty的時候最終都會呼叫到Instrumentation.execStartActivity方法
ContextImpl.java

  @Override
  public void startActivity(Intent intent, Bundle options) {
      warnIfCallingFromSystemProcess();
      if ((intent.getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0) {
          throw new AndroidRuntimeException(
                  "Calling startActivity() from outside of an Activity "
                  + " context requires the FLAG_ACTIVITY_NEW_TASK flag."
                  + " Is this really what you want?");
      }
      mMainThread.getInstrumentation().execStartActivity(
              getOuterContext(), mMainThread.getApplicationThread(), null,
              (Activity) null, intent, -1, options);
  }複製程式碼


Activity.java

  public void startActivityForResult(Intent intent, int requestCode, @Nullable Bundle options) {
    if (mParent == null) {
        Instrumentation.ActivityResult ar =
            mInstrumentation.execStartActivity(
                this, mMainThread.getApplicationThread(), mToken, this,
                intent, requestCode, options);
        if (ar != null) {
            mMainThread.sendActivityResult(
                mToken, mEmbeddedID, requestCode, ar.getResultCode(),
                ar.getResultData());
        }
        if (requestCode >= 0) {
            // If this start is requesting a result, we can avoid making
            // the activity visible until the result is received.  Setting
            // this code during onCreate(Bundle savedInstanceState) or onResume() will keep the
            // activity hidden during this time, to avoid flickering.
            // This can only be done when a result is requested because
            // that guarantees we will get information back when the
            // activity is finished, no matter what happens to it.
            mStartedActivity = true;
        }

        cancelInputsAndStartExitTransition(options);
        // TODO Consider clearing/flushing other event sources and events for child windows.
    } else {
        if (options != null) {
            mParent.startActivityFromChild(this, intent, requestCode, options);
        } else {
            // Note we want to go through this method for compatibility with
            // existing applications that may have overridden it.
            mParent.startActivityFromChild(this, intent, requestCode);
        }
    }
}複製程式碼

追蹤Instrumentation物件的來路,是在ActivityThread裡的performLaunchActivity方法。那我們就對Instrumentation進行Hook。

  /** 
   ** 最好在Application中調。
   */
  public void hookInstrumentation() {
    try {
        Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
        Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
        currentActivityThreadMethod.setAccessible(true);
        //通過currentActivityThread這個靜態的方法獲取到ActivityThread的例項物件。
        Object currentActivityThread = currentActivityThreadMethod.invoke(null);

        // 拿到原始的 mInstrumentation欄位
        Field mInstrumentationField = activityThreadClass.getDeclaredField("mInstrumentation");
        mInstrumentationField.setAccessible(true);
        Instrumentation mInstrumentation = (Instrumentation) mInstrumentationField.get(currentActivityThread);

        // 建立代理物件
        Instrumentation chrInstrumentation = new ChrInstrumentation(mInstrumentation);
        // 替換
        mInstrumentationField.set(currentActivityThread, chrInstrumentation);
    } catch (Exception e) {
        Log.i("lz", "hookInstrumentation Exception");
    }
}

public class ChrInstrumentation  extends Instrumentation{
// ActivityThread中原始物件,反射需要
private Instrumentation mBase;

public ChrInstrumentation(Instrumentation base) {
    mBase = base;
}

@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
    if(!ChrApplication.dexLoadDone){
        //沒有完成MultiDex的載入,就替換顯示Activity。
        className = LoadDexActivity.class.getName();
        Log.i("lz","未完成,重定向");
    }
    return super.newActivity(cl, className, intent);
}

public ActivityResult execStartActivity(
        Context who, IBinder contextThread, IBinder token, Activity target,
        Intent intent, int requestCode, Bundle options) {
    try {
        //newActivity與execStartActivity兩個方法只需要覆寫一個即可。
        if (!ChrApplication.dexLoadDone) {
            intent = new Intent(MyApp.myApp, ThirdActivity.class);
        }
        //這個方法是@hide的,反射呼叫。
        Method execStartActivity = Instrumentation.class.getDeclaredMethod(
                "execStartActivity",
                Context.class, IBinder.class, IBinder.class, Activity.class,
                Intent.class, int.class, Bundle.class);
        execStartActivity.setAccessible(true);
        return (ActivityResult) execStartActivity.invoke(mBase, who,
                contextThread, token, target, intent, requestCode, options);
    } catch (Exception e) {
        Log.i("lz", e.toString());
        return null;
    }
}}複製程式碼

此處重寫了Instrumentation的兩個方法,newActivity與execStartActivity,這兩個方法都可以實現我們的需求,具體用哪一個呢?推薦newActivity,此處建立Activity物件,執行順序靠前。

四、看效果

反編譯檢視方法數以及keep_in_main_dex.txt裡的檔案是否在主Dex中。

MultiDex(三)之非同步載入優化
單Dex方法數不超過設定的48k

在子執行緒中Sleep若干秒,快速點選非主Dex中的Activity,製造現場。可以看到列印出來的Log以及,顯示的Activity也是LoadDexActivity,實現了攔截。

五、問題

1、這個方案的缺點?
通過以上分析及程式碼實踐,可以看到,這個方案雖然實現了Dex在主程式的子執行緒中的載入,但是改動量極大;

  • 需要對Gradle打包的過程進行定製,將入口類以及入口類的引用類都放的keep_in_main_dex.txt,這個過程需要用指令碼實現,並且經常性的更新、維護;
  • 不同版本Gradle的Api可能會有變化,因此需要額外處理;
  • 本文分析的是對Activity的Hook,而原則上四大元件都需要Hook,時間成本上肯定更長;
  • 還有其他類如記錄真實跳轉Activity,在非同步處理完畢之後跳往真實Activity的邏輯。

2、推薦方案?
鑑於問題1中描述的缺點,所以更推薦上一篇文章《Multidex(二)之Dex預載入優化》的方案,使用方便簡單。


以上就是MultiDex系列文章的全部三篇,對MultiDex的原理及優化方案進行了分析。

歡迎關注微信公眾號:定期分享Java、Android乾貨!

MultiDex(三)之非同步載入優化
歡迎關注

相關文章