一、前言
在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。
那是不是就宣判了這個想法的死刑呢?No,No,No,程式猿就是為了解決挑戰而生的,非同步載入確實是個正常又合理的想法,那這個想法怎麼落地呢?歡迎關注下一篇文章。
歡迎關注微信公眾號:定期分享Java、Android乾貨!