一、前言
在上一篇文章《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中。
如何對四大元件進行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);
}複製程式碼
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中。
在子執行緒中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乾貨!