MultiDex工作原理分析和優化方案
動態加載技術(插件化)系列已經坑了有一段時間了,不過UP主我并沒有放棄治療哈,相信在不就的未來就可以看到“系統Api Hook模式”和插件化框架Frontia的更新了。今天要講的是動態加載技術的親戚 —— MultiDex。他們的核心原理之一都是dex文件的加載。
MultiDex是Google為了解決“65535方法數超標”以及“INSTALL_FAILED_DEXOPT”問題而開發的一個Support庫,具體如何使用MultiDex現在市面已經有一大堆教程(可以參考給 App 啟用 MultiDex 功能),這里不再贅述。這篇日志主要是配合源碼分析MultiDex的工作原理,以及提供一些MultiDex優化的方案。
Dex的工作機制
等等,這個章節講的不是MultiDex嗎,怎么變成Dex了?沒錯哈,沒有Dex,哪來的MultiDex。在Android中,對Dex文件操作對應的類叫做DexFile。在CLASSLOADER 的工作機制中,我們說到:
對于 Java 程序來說,編寫程序就是編寫類,運行程序也就是運行類(編譯得到的class文件),其中起到關鍵作用的就是類加載器 ClassLoader。
Android程序的每一個Class都是由ClassLoader#loadClass方法加載進內存的,更準確來說,一個ClassLoader實例會有一個或者多個DexFile實例,調用了ClassLoader#loadClass之后,ClassLoader會通過類名,在自己的DexFile數組里面查找有沒有那個DexFile對象里面存在這個類,如果都沒有就拋ClassNotFound異常。ClassLoader通過調用DexFile的一個叫defineClass的Native方法去加載指定的類,這點與JVM略有不同,后者是直接調用ClassLoader#defineCLass方法,反正最后實際加載類的方法都叫defineClass就沒錯了?。
創建DexFile對象
首先來看看造DexFile對象的構方法。
public final class DexFile {
private int mCookie;
private final String mFileName;
...
public DexFile(File file) throws IOException {
this(file.getPath());
}
public DexFile(String fileName) throws IOException {
mCookie = openDexFile(fileName, null, 0);
mFileName = fileName;
guard.open("close");
}
private DexFile(String sourceName, String outputName, int flags) throws IOException {
mCookie = openDexFile(sourceName, outputName, flags);
mFileName = sourceName;
guard.open("close");
}
static public DexFile loadDex(String sourcePathName, String outputPathName,
int flags) throws IOException {
return new DexFile(sourcePathName, outputPathName, flags);
}
public Class loadClass(String name, ClassLoader loader) {
String slashName = name.replace('.', '/');
return loadClassBinaryName(slashName, loader);
}
public Class loadClassBinaryName(String name, ClassLoader loader) {
return defineClass(name, loader, mCookie);
}
private native static Class defineClass(String name, ClassLoader loader, int cookie);
native private static int openDexFile(String sourceName, String outputName,
int flags) throws IOException;
native private static int openDexFile(byte[] fileContents)
...
}
通過以前分析過的源碼,我們知道ClassLoader主要是通過DexFile.loadDex這個靜態方法來創建它需要的DexFile實例的,這里創建DexFile的時候,保存了Dex文件的文件路徑mFileName,同時調用了openDexFile的Native方法打開Dex文件并返回了一個mCookie的整型變量(我不知道這個干啥用的,我猜它是一個C++用的資源句柄,用于Native層訪問具體的Dex文件)。在Native層的openDexFile方法里,主要做了檢查當前創建來的Dex文件是否是有效的Dex文件,還是是一個帶有Dex文件的壓縮包,還是一個無效的Dex文件。
加載Dex文件里的類
加載類的時候,ClassLoader又是通過DexFile#loadClass這個方法來完成的,這個方法里調用了defineClass這個Native方法,看來DexFile才是加載Class的具體API,加載Dex文件和加載具體Class都是通過Native方法完成,ClassLoader有點名不副實啊。
MultiDex的工作機制
當一個Dex文件太肥的時候(方法數目太多、文件太大),在打包Apk文件的時候就會出問題,就算打包的時候不出問題,在Android 5.0以下設備上安裝或運行Apk也會出問題(具體原因可以參考給 App 啟用 MultiDex 功能)。既然一個Dex文件不行的話,那就把這個碩大的Dex文件拆分成若干個小的Dex文件,剛好一個ClassLoader可以有多個DexFile,這就是MultiDex的基本設計思路。
工作流程
MultiDex的工作流程具體分為兩個部分,一個部分是打包構建Apk的時候,將Dex文件拆分成若干個小的Dex文件,這個Android Studio已經幫我們做了(設置 “multiDexEnabled true”),另一部分就是在啟動Apk的時候,同時加載多個Dex文件(具體是加載Dex文件優化后的Odex文件,不過文件名還是.dex),這一部分工作從Android 5.0開始系統已經幫我們做了,但是在Android 5.0以前還是需要通過MultiDex Support庫來支持(MultiDex.install(Context))。
所以我們需要關心的是第二部分,這個過程的簡單示意流程圖如下。
(圖中紅色部分為耗時比較大的地方)
源碼分析
現在官方已經部署的MultiDex Support版本是com.android.support:multidex:1.0.1,但是現在倉庫的master分支已經有了許多新的提交(其中最明顯的區別是加入了FileLock來控制多進程同步問題),所以這里分析的源碼都是最新的master分支上的。
MultiDex Support的入口是MultiDex.install(Context),先從這里入手吧。(這次我把具體的分析都寫在代碼的注釋了,這樣看是不是更簡潔明了些?)
public static void install(Context context) {
Log.i(TAG, "install");
// 1. 判讀是否需要執行MultiDex。
if (IS_VM_MULTIDEX_CAPABLE) {
Log.i(TAG, "VM has multidex support, MultiDex support library is disabled.");
return;
}
if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) {
throw new RuntimeException("Multi dex installation failed. SDK " + Build.VERSION.SDK_INT
+ " is unsupported. Min SDK version is " + MIN_SDK_VERSION + ".");
}
try {
ApplicationInfo applicationInfo = getApplicationInfo(context);
if (applicationInfo == null) {
// Looks like running on a test Context, so just return without patching.
return;
}
// 2. 如果這個方法已經調用過一次,就不能再調用了。
synchronized (installedApk) {
String apkPath = applicationInfo.sourceDir;
if (installedApk.contains(apkPath)) {
return;
}
installedApk.add(apkPath);
// 3. 如果當前Android版本已經自身支持了MultiDex,依然可以執行MultiDex操作,
// 但是會有警告。
if (Build.VERSION.SDK_INT > MAX_SUPPORTED_SDK_VERSION) {
Log.w(TAG, "MultiDex is not guaranteed to work in SDK version "
+ Build.VERSION.SDK_INT + ": SDK version higher than "
+ MAX_SUPPORTED_SDK_VERSION + " should be backed by "
+ "runtime with built-in multidex capabilty but it's not the "
+ "case here: java.vm.version=\""
+ System.getProperty("java.vm.version") + "\"");
}
// 4. 獲取當前的ClassLoader實例,后面要做的工作,就是把其他dex文件加載后,
// 把其DexFile對象添加到這個ClassLoader實例里就完事了。
ClassLoader loader;
try {
loader = context.getClassLoader();
} catch (RuntimeException e) {
Log.w(TAG, "Failure while trying to obtain Context class loader. " +
"Must be running in test mode. Skip patching.", e);
return;
}
if (loader == null) {
Log.e(TAG,
"Context class loader is null. Must be running in test mode. "
+ "Skip patching.");
return;
}
try {
// 5. 清除舊的dex文件,注意這里不是清除上次加載的dex文件緩存。
// 獲取dex緩存目錄是,會優先獲取/data/data/<package>/code-cache作為緩存目錄。
// 如果獲取失敗,則使用/data/data/<package>/files/code-cache目錄。
// 這里清除的是第二個目錄。
clearOldDexDir(context);
} catch (Throwable t) {
Log.w(TAG, "Something went wrong when trying to clear old MultiDex extraction, "
+ "continuing without cleaning.", t);
}
// 6. 獲取緩存目錄(/data/data/<package>/code-cache)。
File dexDir = getDexDir(context, applicationInfo);
// 7. 加載緩存文件(如果有)。
List<File> files = MultiDexExtractor.load(context, applicationInfo, dexDir, false);
// 8. 檢查緩存的dex是否安全
if (checkValidZipFiles(files)) {
// 9. 安裝緩存的dex
installSecondaryDexes(loader, dexDir, files);
} else {
// 9. 從apk壓縮包里面提取dex文件
Log.w(TAG, "Files were not valid zip files. Forcing a reload.");
files = MultiDexExtractor.load(context, applicationInfo, dexDir, true);
if (checkValidZipFiles(files)) {
// 10. 安裝提取的dex
installSecondaryDexes(loader, dexDir, files);
} else {
throw new RuntimeException("Zip files were not valid.");
}
}
}
} catch (Exception e) {
Log.e(TAG, "Multidex installation failure", e);
throw new RuntimeException("Multi dex installation failed (" + e.getMessage() + ").");
}
Log.i(TAG, "install done");
}
具體代碼的分析已經在上面代碼的注釋里給出了,從這里我們也可以看出,整個MultiDex.install(Context)的過程中,關鍵的步驟就是MultiDexExtractor#load方法和MultiDex#installSecondaryDexes方法。
(這部分是題外話)其中有個MultiDex#clearOldDexDir(Context)方法,這個方法的作用是刪除/data/data/<package>/files/code-cache,一開始我以為這個方法是刪除上一次執行MultiDex后的緩存文件,不過這明顯不對,不可能每次MultiDex都重新解壓dex文件一邊,這樣每次啟動會很耗時,只有第一次冷啟動的時候才需要解壓dex文件。后來我又想是不是以前舊版的MultiDex曾經把緩存文件放在這個目錄里,現在新版本只是清除以前舊版的遺留文件?但是我找遍了整個MultiDex Repo的提交也沒有見過類似的舊版本代碼。后面我仔細看MultiDex#getDexDir這個方法才發現,原來MultiDex在獲取dex緩存目錄是,會優先獲取/data/data/<package>/code-cache作為緩存目錄,如果獲取失敗,則使用/data/data/<package>/files/code-cache目錄,而后者的緩存文件會在每次App重新啟動的時候被清除。感覺MultiDex獲取緩存目錄的邏輯不是很嚴謹,而獲取緩存目錄失敗也是MultiDex工作工程中少數有重試機制的地方,看來MultiDex真的是一個臨時的兼容方案,Google也許并不打算認真處理這些歷史的黑鍋。
接下來再看看MultiDexExtractor#load這個方法。
static List<File> load(Context context, ApplicationInfo applicationInfo, File dexDir,
boolean forceReload) throws IOException {
Log.i(TAG, "MultiDexExtractor.load(" + applicationInfo.sourceDir + ", " + forceReload + ")");
final File sourceApk = new File(applicationInfo.sourceDir);
// 1. 獲取當前Apk文件的crc值。
long currentCrc = getZipCrc(sourceApk);
// Validity check and extraction must be done only while the lock file has been taken.
File lockFile = new File(dexDir, LOCK_FILENAME);
RandomAccessFile lockRaf = new RandomAccessFile(lockFile, "rw");
FileChannel lockChannel = null;
FileLock cacheLock = null;
List<File> files;
IOException releaseLockException = null;
try {
lockChannel = lockRaf.getChannel();
Log.i(TAG, "Blocking on lock " + lockFile.getPath());
// 2. 加上文件鎖,防止多進程沖突。
cacheLock = lockChannel.lock();
Log.i(TAG, lockFile.getPath() + " locked");
// 3. 先判斷是否強制重新解壓,這里第一次會優先使用已解壓過的dex文件,如果加載失敗就強制重新解壓。
// 此外,通過crc和文件修改時間,判斷如果Apk文件已經被修改(覆蓋安裝),就會跳過緩存重新解壓dex文件。
if (!forceReload && !isModified(context, sourceApk, currentCrc)) {
try {
// 4. 加載緩存的dex文件
files = loadExistingExtractions(context, sourceApk, dexDir);
} catch (IOException ioe) {
Log.w(TAG, "Failed to reload existing extracted secondary dex files,"
+ " falling back to fresh extraction", ioe);
// 5. 加載失敗的話重新解壓,并保存解壓出來的dex文件的信息。
files = performExtractions(sourceApk, dexDir);
putStoredApkInfo(context,
getTimeStamp(sourceApk), currentCrc, files.size() + 1);
}
} else {
// 4. 重新解壓,并保存解壓出來的dex文件的信息。
Log.i(TAG, "Detected that extraction must be performed.");
files = performExtractions(sourceApk, dexDir);
putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
}
} finally {
if (cacheLock != null) {
try {
cacheLock.release();
} catch (IOException e) {
Log.e(TAG, "Failed to release lock on " + lockFile.getPath());
// Exception while releasing the lock is bad, we want to report it, but not at
// the price of overriding any already pending exception.
releaseLockException = e;
}
}
if (lockChannel != null) {
closeQuietly(lockChannel);
}
closeQuietly(lockRaf);
}
if (releaseLockException != null) {
throw releaseLockException;
}
Log.i(TAG, "load found " + files.size() + " secondary dex files");
return files;
}
這個過程主要是獲取可以安裝的dex文件列表,可以是上次解壓出來的緩存文件,也可以是重新從Apk包里面提取出來的。需要注意的時,如果是重新解壓,這里會有明顯的耗時,而且解壓出來的dex文件,會被壓縮成.zip壓縮包,壓縮的過程也會有明顯的耗時(這里壓縮dex文件可能是問了節省空間)。
如果dex文件是重新解壓出來的,則會保存dex文件的信息,包括解壓的apk文件的crc值、修改時間以及dex文件的數目,以便下一次啟動直接使用已經解壓過的dex緩存文件,而不是每一次都重新解壓。
需要特別提到的是,里面的FileLock是最新的master分支里面新加進去的功能,現在最新的1.0.1版本里面是沒有的。
無論是通過使用緩存的dex文件,還是重新從apk中解壓dex文件,獲取dex文件列表后,下一步就是安裝(或者說加載)這些dex文件了。最后的工作在MultiDex#installSecondaryDexes這個方法里面。
private static void installSecondaryDexes(ClassLoader loader, File dexDir, List<File> files)
throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
InvocationTargetException, NoSuchMethodException, IOException {
if (!files.isEmpty()) {
if (Build.VERSION.SDK_INT >= 19) {
V19.install(loader, files, dexDir);
} else if (Build.VERSION.SDK_INT >= 14) {
V14.install(loader, files, dexDir);
} else {
V4.install(loader, files);
}
}
}
因為在不同的SDK版本上,ClassLoader(更準確來說是DexClassLoader)加載dex文件的方式有所不同,所以這里做了V4/V14/V19的兼容(Magic Code)。
Build.VERSION.SDK_INT < 14
/**
* Installer for platform versions 4 to 13.
*/
private static final class V4 {
private static void install(ClassLoader loader, List<File> additionalClassPathEntries)
throws IllegalArgumentException, IllegalAccessException,
NoSuchFieldException, IOException {
int extraSize = additionalClassPathEntries.size();
Field pathField = findField(loader, "path");
StringBuilder path = new StringBuilder((String) pathField.get(loader));
String[] extraPaths = new String[extraSize];
File[] extraFiles = new File[extraSize];
ZipFile[] extraZips = new ZipFile[extraSize];
DexFile[] extraDexs = new DexFile[extraSize];
for (ListIterator<File> iterator = additionalClassPathEntries.listIterator();
iterator.hasNext();) {
File additionalEntry = iterator.next();
String entryPath = additionalEntry.getAbsolutePath();
path.append(':').append(entryPath);
int index = iterator.previousIndex();
extraPaths[index] = entryPath;
extraFiles[index] = additionalEntry;
extraZips[index] = new ZipFile(additionalEntry);
extraDexs[index] = DexFile.loadDex(entryPath, entryPath + ".dex", 0);
}
// 這個版本是最簡單的。
// 只需要創建DexFile對象后,使用反射的方法分別擴展ClassLoader實例的以下字段即可。
pathField.set(loader, path.toString());
expandFieldArray(loader, "mPaths", extraPaths);
expandFieldArray(loader, "mFiles", extraFiles);
expandFieldArray(loader, "mZips", extraZips);
expandFieldArray(loader, "mDexs", extraDexs);
}
}
14 <= Build.VERSION.SDK_INT < 19
/**
* Installer for platform versions 14, 15, 16, 17 and 18.
*/
private static final class V14 {
private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
File optimizedDirectory)
throws IllegalArgumentException, IllegalAccessException,
NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
// 擴展ClassLoader實例的"pathList"字段。
Field pathListField = findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
new ArrayList<File>(additionalClassPathEntries), optimizedDirectory));
}
private static Object[] makeDexElements(
Object dexPathList, ArrayList<File> files, File optimizedDirectory)
throws IllegalAccessException, InvocationTargetException,
NoSuchMethodException {
Method makeDexElements =
findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class);
return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory);
}
}
從API14開始,DexClassLoader會使用一個DexpDexPathList類來封裝DexFile數組。
final class DexPathList {
private static final String DEX_SUFFIX = ".dex";
private static final String JAR_SUFFIX = ".jar";
private static final String ZIP_SUFFIX = ".zip";
private static final String APK_SUFFIX = ".apk";
private static Element[] makeDexElements(ArrayList<File> files,
File optimizedDirectory) {
ArrayList<Element> elements = new ArrayList<Element>();
for (File file : files) {
ZipFile zip = null;
DexFile dex = null;
String name = file.getName();
if (name.endsWith(DEX_SUFFIX)) {
// Raw dex file (not inside a zip/jar).
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ex) {
System.logE("Unable to load dex file: " + file, ex);
}
} else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
|| name.endsWith(ZIP_SUFFIX)) {
try {
zip = new ZipFile(file);
} catch (IOException ex) {
System.logE("Unable to open zip file: " + file, ex);
}
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ignored) {
}
} else {
System.logW("Unknown file type for: " + file);
}
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, zip, dex));
}
}
return elements.toArray(new Element[elements.size()]);
}
private static DexFile loadDexFile(File file, File optimizedDirectory)
throws IOException {
if (optimizedDirectory == null) {
return new DexFile(file);
} else {
String optimizedPath = optimizedPathFor(file, optimizedDirectory);
return DexFile.loadDex(file.getPath(), optimizedPath, 0);
}
}
}
通過調用DexPathList#makeDexElements方法,可以加載我們上面解壓得到的dex文件,從代碼也可以看出,DexPathList#makeDexElements其實也是通過調用DexFile#loadDex來加載dex文件并創建DexFile對象的。V14中,通過反射調用DexPathList#makeDexElements方法加載我們需要的dex文件,在把加載得到的數組擴展到ClassLoader實例的"pathList"字段,從而完成dex文件的安裝。
從DexPathList的代碼中我們也可以看出,ClassLoader是支持直接加載.dex/.zip/.jar/.apk的dex文件包的(我記得以前在哪篇日志中好像提到過類似的問題…)。
19 <= Build.VERSION.SDK_INT
/**
* Installer for platform versions 19.
*/
private static final class V19 {
private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
File optimizedDirectory)
throws IllegalArgumentException, IllegalAccessException,
NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
Field pathListField = findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
suppressedExceptions));
if (suppressedExceptions.size() > 0) {
for (IOException e : suppressedExceptions) {
Log.w(TAG, "Exception in makeDexElement", e);
}
Field suppressedExceptionsField =
findField(dexPathList, "dexElementsSuppressedExceptions");
IOException[] dexElementsSuppressedExceptions =
(IOException[]) suppressedExceptionsField.get(dexPathList);
if (dexElementsSuppressedExceptions == null) {
dexElementsSuppressedExceptions =
suppressedExceptions.toArray(
new IOException[suppressedExceptions.size()]);
} else {
IOException[] combined =
new IOException[suppressedExceptions.size() +
dexElementsSuppressedExceptions.length];
suppressedExceptions.toArray(combined);
System.arraycopy(dexElementsSuppressedExceptions, 0, combined,
suppressedExceptions.size(), dexElementsSuppressedExceptions.length);
dexElementsSuppressedExceptions = combined;
}
suppressedExceptionsField.set(dexPathList, dexElementsSuppressedExceptions);
}
}
private static Object[] makeDexElements(
Object dexPathList, ArrayList<File> files, File optimizedDirectory,
ArrayList<IOException> suppressedExceptions)
throws IllegalAccessException, InvocationTargetException,
NoSuchMethodException {
Method makeDexElements =
findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class,
ArrayList.class);
return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory,
suppressedExceptions);
}
}
V19與V14差別不大,只不過DexPathList#makeDexElements方法多了一個ArrayList<IOException>參數,如果在執行DexPathList#makeDexElements方法的過程中出現異常,后面使用反射的方式把這些異常記錄進DexPathList的dexElementsSuppressedExceptions字段里面。
無論是V4/V14還是V19,在創建DexFile對象的時候,都需要通過DexFile的Native方法openDexFile來打開dex文件,其具體細節暫不討論(涉及到dex的文件結構,很煩,有興趣請閱讀dalvik_system_DexFile.cpp),這個過程的主要目的是給當前的dex文件做Optimize優化處理并生成相同文件名的odex文件,App實際加載類的時候,都是通過odex文件進行的。因為每個設備對odex格式的要求都不一樣,所以這個優化的操作只能放在安裝Apk的時候處理,主dex的優化我們已經在安裝apk的時候搞定了,其余的dex就是在MultiDex#installSecondaryDexes里面優化的,而后者也是MultiDex過程中,另外一個耗時比較多的操作。(在MultiDex中,提取出來的dex文件被壓縮成.zip文件,又優化后的odex文件則被保存為.dex文件。)
到這里,MultiDex的工作流程就結束了。怎么樣,是不是覺得和以前談到動態加載技術(插件化)的時候說的很像?沒錯,誰叫它們的核心都是dex文件呢。Java老師第一節課就說“類就是編程”,搞定類你就能搞定整個世界啊!
優化方案
MultiDex有個比較蛋疼的問題,就是會產生明顯的卡頓現象,通過上面的分析,我們知道具體的卡頓產生在解壓dex文件以及優化dex兩個步驟。不過好在,在Application#attachBaseContext(Context)中,UI線程的阻塞是不會引發ANR的,只不過這段長時間的卡頓(白屏)還是會影響用戶體驗。
目前,優化方案能想到的有兩種。
PreMultiDex方案
大致思路是,在安裝一個新的apk的時候,先在Worker線程里做好MultiDex的解壓和Optimize工作,安裝apk并啟動后,直接使用之前Optimize產生的odex文件,這樣就可以避免第一次啟動時候的Optimize工作。
安裝dex的時候,核心是創建DexFile對象并使用其Native方法對dex文件進行opt處理,同時生產一個與dex文件(.zip)同名的已經opt過的dex文件(.dex)。如果安裝dex的時候,這個opt過的dex文件已經存在,則跳過這個過程,這會節省許多耗時。所以優化的思路就是,下載Apk完成的時候,預先解壓dex文件,并預先觸發安裝dex文件以生產opt過的dex文件。這樣覆蓋安裝Apk并啟動的時候,如果MultiDex能命中解壓好的dex和odex文件,則能避開耗時最大的兩個操作。
不過這個方案的缺點也是明顯的,第一次安裝的apk沒有作用,而且事先需要使用內置的apk更新功能把新版本的apk文件下載下來后,才能做PreMultiDex工作。
異步MultiDex方案
這種方案也是目前比較流行的Dex手動分包方案,啟動App的時候,先顯示一個簡單的Splash閃屏界面,然后啟動Worker線程執行MultiDex#install(Context)工作,就可以避免UI線程阻塞。不過要確保啟動以及啟動MultiDex#install(Context)所需要的類都在主dex里面(手動分包),而且需要處理好進程同步問題。
參考資料:
來自:http://mobile.51cto.com/android-524919.htm