MultiDex工作原理分析和優(yōu)化方案
動(dòng)態(tài)加載技術(shù)(插件化)系列已經(jīng)坑了有一段時(shí)間了,不過(guò)UP主我并沒(méi)有放棄治療哈,相信在不就的未來(lái)就可以看到“系統(tǒng)Api Hook模式”和插件化框架Frontia的更新了。今天要講的是動(dòng)態(tài)加載技術(shù)的親戚 —— MultiDex。他們的核心原理之一都是dex文件的加載。
MultiDex是Google為了解決“65535方法數(shù)超標(biāo)”以及“INSTALL_FAILED_DEXOPT”問(wèn)題而開(kāi)發(fā)的一個(gè)Support庫(kù),具體如何使用MultiDex現(xiàn)在市面已經(jīng)有一大堆教程(可以參考給 App 啟用 MultiDex 功能),這里不再贅述。這篇日志主要是配合源碼分析MultiDex的工作原理,以及提供一些MultiDex優(yōu)化的方案。
Dex的工作機(jī)制
等等,這個(gè)章節(jié)講的不是MultiDex嗎,怎么變成Dex了?沒(méi)錯(cuò)哈,沒(méi)有Dex,哪來(lái)的MultiDex。在Android中,對(duì)Dex文件操作對(duì)應(yīng)的類(lèi)叫做DexFile。在CLASSLOADER 的工作機(jī)制中,我們說(shuō)到:
對(duì)于 Java 程序來(lái)說(shuō),編寫(xiě)程序就是編寫(xiě)類(lèi),運(yùn)行程序也就是運(yùn)行類(lèi)(編譯得到的class文件),其中起到關(guān)鍵作用的就是類(lèi)加載器 ClassLoader。
Android程序的每一個(gè)Class都是由ClassLoader#loadClass方法加載進(jìn)內(nèi)存的,更準(zhǔn)確來(lái)說(shuō),一個(gè)ClassLoader實(shí)例會(huì)有一個(gè)或者多個(gè)DexFile實(shí)例,調(diào)用了ClassLoader#loadClass之后,ClassLoader會(huì)通過(guò)類(lèi)名,在自己的DexFile數(shù)組里面查找有沒(méi)有那個(gè)DexFile對(duì)象里面存在這個(gè)類(lèi),如果都沒(méi)有就拋ClassNotFound異常。ClassLoader通過(guò)調(diào)用DexFile的一個(gè)叫defineClass的Native方法去加載指定的類(lèi),這點(diǎn)與JVM略有不同,后者是直接調(diào)用ClassLoader#defineCLass方法,反正最后實(shí)際加載類(lèi)的方法都叫defineClass就沒(méi)錯(cuò)了🌝。
創(chuàng)建DexFile對(duì)象
首先來(lái)看看造DexFile對(duì)象的構(gòu)方法。
- 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)
- ...
- }
通過(guò)以前分析過(guò)的源碼,我們知道ClassLoader主要是通過(guò)DexFile.loadDex這個(gè)靜態(tài)方法來(lái)創(chuàng)建它需要的DexFile實(shí)例的,這里創(chuàng)建DexFile的時(shí)候,保存了Dex文件的文件路徑mFileName,同時(shí)調(diào)用了openDexFile的Native方法打開(kāi)Dex文件并返回了一個(gè)mCookie的整型變量(我不知道這個(gè)干啥用的,我猜它是一個(gè)C++用的資源句柄,用于Native層訪問(wèn)具體的Dex文件)。在Native層的openDexFile方法里,主要做了檢查當(dāng)前創(chuàng)建來(lái)的Dex文件是否是有效的Dex文件,還是是一個(gè)帶有Dex文件的壓縮包,還是一個(gè)無(wú)效的Dex文件。
加載Dex文件里的類(lèi)
加載類(lèi)的時(shí)候,ClassLoader又是通過(guò)DexFile#loadClass這個(gè)方法來(lái)完成的,這個(gè)方法里調(diào)用了defineClass這個(gè)Native方法,看來(lái)DexFile才是加載Class的具體API,加載Dex文件和加載具體Class都是通過(guò)Native方法完成,ClassLoader有點(diǎn)名不副實(shí)啊。
MultiDex的工作機(jī)制
當(dāng)一個(gè)Dex文件太肥的時(shí)候(方法數(shù)目太多、文件太大),在打包Apk文件的時(shí)候就會(huì)出問(wèn)題,就算打包的時(shí)候不出問(wèn)題,在Android 5.0以下設(shè)備上安裝或運(yùn)行Apk也會(huì)出問(wèn)題(具體原因可以參考給 App 啟用 MultiDex 功能)。既然一個(gè)Dex文件不行的話,那就把這個(gè)碩大的Dex文件拆分成若干個(gè)小的Dex文件,剛好一個(gè)ClassLoader可以有多個(gè)DexFile,這就是MultiDex的基本設(shè)計(jì)思路。
工作流程
MultiDex的工作流程具體分為兩個(gè)部分,一個(gè)部分是打包構(gòu)建Apk的時(shí)候,將Dex文件拆分成若干個(gè)小的Dex文件,這個(gè)Android Studio已經(jīng)幫我們做了(設(shè)置 “multiDexEnabled true”),另一部分就是在啟動(dòng)Apk的時(shí)候,同時(shí)加載多個(gè)Dex文件(具體是加載Dex文件優(yōu)化后的Odex文件,不過(guò)文件名還是.dex),這一部分工作從Android 5.0開(kāi)始系統(tǒng)已經(jīng)幫我們做了,但是在Android 5.0以前還是需要通過(guò)MultiDex Support庫(kù)來(lái)支持(MultiDex.install(Context))。
所以我們需要關(guān)心的是第二部分,這個(gè)過(guò)程的簡(jiǎn)單示意流程圖如下。
(圖中紅色部分為耗時(shí)比較大的地方)
源碼分析
現(xiàn)在官方已經(jīng)部署的MultiDex Support版本是com.android.support:multidex:1.0.1,但是現(xiàn)在倉(cāng)庫(kù)的master分支已經(jīng)有了許多新的提交(其中最明顯的區(qū)別是加入了FileLock來(lái)控制多進(jìn)程同步問(wèn)題),所以這里分析的源碼都是最新的master分支上的。
MultiDex Support的入口是MultiDex.install(Context),先從這里入手吧。(這次我把具體的分析都寫(xiě)在代碼的注釋了,這樣看是不是更簡(jiǎn)潔明了些?)
- public static void install(Context context) {
- Log.i(TAG, "install");
- // 1. 判讀是否需要執(zhí)行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. 如果這個(gè)方法已經(jīng)調(diào)用過(guò)一次,就不能再調(diào)用了。
- synchronized (installedApk) {
- String apkPath = applicationInfo.sourceDir;
- if (installedApk.contains(apkPath)) {
- return;
- }
- installedApk.add(apkPath);
- // 3. 如果當(dāng)前Android版本已經(jīng)自身支持了MultiDex,依然可以執(zhí)行MultiDex操作,
- // 但是會(huì)有警告。
- 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. 獲取當(dāng)前的ClassLoader實(shí)例,后面要做的工作,就是把其他dex文件加載后,
- // 把其DexFile對(duì)象添加到這個(gè)ClassLoader實(shí)例里就完事了。
- 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緩存目錄是,會(huì)優(yōu)先獲取/data/data/<package>/code-cache作為緩存目錄。
- // 如果獲取失敗,則使用/data/data/<package>/files/code-cache目錄。
- // 這里清除的是第二個(gè)目錄。
- 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");
- }
具體代碼的分析已經(jīng)在上面代碼的注釋里給出了,從這里我們也可以看出,整個(gè)MultiDex.install(Context)的過(guò)程中,關(guān)鍵的步驟就是MultiDexExtractor#load方法和MultiDex#installSecondaryDexes方法。
(這部分是題外話)其中有個(gè)MultiDex#clearOldDexDir(Context)方法,這個(gè)方法的作用是刪除/data/data/<package>/files/code-cache,一開(kāi)始我以為這個(gè)方法是刪除上一次執(zhí)行MultiDex后的緩存文件,不過(guò)這明顯不對(duì),不可能每次MultiDex都重新解壓dex文件一邊,這樣每次啟動(dòng)會(huì)很耗時(shí),只有第一次冷啟動(dòng)的時(shí)候才需要解壓dex文件。后來(lái)我又想是不是以前舊版的MultiDex曾經(jīng)把緩存文件放在這個(gè)目錄里,現(xiàn)在新版本只是清除以前舊版的遺留文件?但是我找遍了整個(gè)MultiDex Repo的提交也沒(méi)有見(jiàn)過(guò)類(lèi)似的舊版本代碼。后面我仔細(xì)看MultiDex#getDexDir這個(gè)方法才發(fā)現(xiàn),原來(lái)MultiDex在獲取dex緩存目錄是,會(huì)優(yōu)先獲取/data/data/<package>/code-cache作為緩存目錄,如果獲取失敗,則使用/data/data/<package>/files/code-cache目錄,而后者的緩存文件會(huì)在每次App重新啟動(dòng)的時(shí)候被清除。感覺(jué)MultiDex獲取緩存目錄的邏輯不是很?chē)?yán)謹(jǐn),而獲取緩存目錄失敗也是MultiDex工作工程中少數(shù)有重試機(jī)制的地方,看來(lái)MultiDex真的是一個(gè)臨時(shí)的兼容方案,Google也許并不打算認(rèn)真處理這些歷史的黑鍋。
接下來(lái)再看看MultiDexExtractor#load這個(gè)方法。
- 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. 獲取當(dāng)前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. 加上文件鎖,防止多進(jìn)程沖突。
- cacheLock = lockChannel.lock();
- Log.i(TAG, lockFile.getPath() + " locked");
- // 3. 先判斷是否強(qiáng)制重新解壓,這里第一次會(huì)優(yōu)先使用已解壓過(guò)的dex文件,如果加載失敗就強(qiáng)制重新解壓。
- // 此外,通過(guò)crc和文件修改時(shí)間,判斷如果Apk文件已經(jīng)被修改(覆蓋安裝),就會(huì)跳過(guò)緩存重新解壓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. 加載失敗的話重新解壓,并保存解壓出來(lái)的dex文件的信息。
- files = performExtractions(sourceApk, dexDir);
- putStoredApkInfo(context,
- getTimeStamp(sourceApk), currentCrc, files.size() + 1);
- }
- } else {
- // 4. 重新解壓,并保存解壓出來(lái)的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;
- }
這個(gè)過(guò)程主要是獲取可以安裝的dex文件列表,可以是上次解壓出來(lái)的緩存文件,也可以是重新從Apk包里面提取出來(lái)的。需要注意的時(shí),如果是重新解壓,這里會(huì)有明顯的耗時(shí),而且解壓出來(lái)的dex文件,會(huì)被壓縮成.zip壓縮包,壓縮的過(guò)程也會(huì)有明顯的耗時(shí)(這里壓縮dex文件可能是問(wèn)了節(jié)省空間)。
如果dex文件是重新解壓出來(lái)的,則會(huì)保存dex文件的信息,包括解壓的apk文件的crc值、修改時(shí)間以及dex文件的數(shù)目,以便下一次啟動(dòng)直接使用已經(jīng)解壓過(guò)的dex緩存文件,而不是每一次都重新解壓。
需要特別提到的是,里面的FileLock是最新的master分支里面新加進(jìn)去的功能,現(xiàn)在最新的1.0.1版本里面是沒(méi)有的。
無(wú)論是通過(guò)使用緩存的dex文件,還是重新從apk中解壓dex文件,獲取dex文件列表后,下一步就是安裝(或者說(shuō)加載)這些dex文件了。最后的工作在MultiDex#installSecondaryDexes這個(gè)方法里面。
- 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);
- }
- }
- }
因?yàn)樵诓煌腟DK版本上,ClassLoader(更準(zhǔn)確來(lái)說(shuō)是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);
- }
- // 這個(gè)版本是最簡(jiǎn)單的。
- // 只需要?jiǎng)?chuàng)建DexFile對(duì)象后,使用反射的方法分別擴(kuò)展ClassLoader實(shí)例的以下字段即可。
- 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 {
- // 擴(kuò)展ClassLoader實(shí)例的"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開(kāi)始,DexClassLoader會(huì)使用一個(gè)DexpDexPathList類(lèi)來(lái)封裝DexFile數(shù)組。
- 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);
- }
- }
- }
通過(guò)調(diào)用DexPathList#makeDexElements方法,可以加載我們上面解壓得到的dex文件,從代碼也可以看出,DexPathList#makeDexElements其實(shí)也是通過(guò)調(diào)用DexFile#loadDex來(lái)加載dex文件并創(chuàng)建DexFile對(duì)象的。V14中,通過(guò)反射調(diào)用DexPathList#makeDexElements方法加載我們需要的dex文件,在把加載得到的數(shù)組擴(kuò)展到ClassLoader實(shí)例的"pathList"字段,從而完成dex文件的安裝。
從DexPathList的代碼中我們也可以看出,ClassLoader是支持直接加載.dex/.zip/.jar/.apk的dex文件包的(我記得以前在哪篇日志中好像提到過(guò)類(lèi)似的問(wèn)題…)。
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差別不大,只不過(guò)DexPathList#makeDexElements方法多了一個(gè)ArrayList<IOException>參數(shù),如果在執(zhí)行DexPathList#makeDexElements方法的過(guò)程中出現(xiàn)異常,后面使用反射的方式把這些異常記錄進(jìn)DexPathList的dexElementsSuppressedExceptions字段里面。
無(wú)論是V4/V14還是V19,在創(chuàng)建DexFile對(duì)象的時(shí)候,都需要通過(guò)DexFile的Native方法openDexFile來(lái)打開(kāi)dex文件,其具體細(xì)節(jié)暫不討論(涉及到dex的文件結(jié)構(gòu),很煩,有興趣請(qǐng)閱讀dalvik_system_DexFile.cpp),這個(gè)過(guò)程的主要目的是給當(dāng)前的dex文件做Optimize優(yōu)化處理并生成相同文件名的odex文件,App實(shí)際加載類(lèi)的時(shí)候,都是通過(guò)odex文件進(jìn)行的。因?yàn)槊總€(gè)設(shè)備對(duì)odex格式的要求都不一樣,所以這個(gè)優(yōu)化的操作只能放在安裝Apk的時(shí)候處理,主dex的優(yōu)化我們已經(jīng)在安裝apk的時(shí)候搞定了,其余的dex就是在MultiDex#installSecondaryDexes里面優(yōu)化的,而后者也是MultiDex過(guò)程中,另外一個(gè)耗時(shí)比較多的操作。(在MultiDex中,提取出來(lái)的dex文件被壓縮成.zip文件,又優(yōu)化后的odex文件則被保存為.dex文件。)
到這里,MultiDex的工作流程就結(jié)束了。怎么樣,是不是覺(jué)得和以前談到動(dòng)態(tài)加載技術(shù)(插件化)的時(shí)候說(shuō)的很像?沒(méi)錯(cuò),誰(shuí)叫它們的核心都是dex文件呢。Java老師第一節(jié)課就說(shuō)“類(lèi)就是編程”,搞定類(lèi)你就能搞定整個(gè)世界啊!
優(yōu)化方案
MultiDex有個(gè)比較蛋疼的問(wèn)題,就是會(huì)產(chǎn)生明顯的卡頓現(xiàn)象,通過(guò)上面的分析,我們知道具體的卡頓產(chǎn)生在解壓dex文件以及優(yōu)化dex兩個(gè)步驟。不過(guò)好在,在Application#attachBaseContext(Context)中,UI線程的阻塞是不會(huì)引發(fā)ANR的,只不過(guò)這段長(zhǎng)時(shí)間的卡頓(白屏)還是會(huì)影響用戶體驗(yàn)。
目前,優(yōu)化方案能想到的有兩種。
PreMultiDex方案
大致思路是,在安裝一個(gè)新的apk的時(shí)候,先在Worker線程里做好MultiDex的解壓和Optimize工作,安裝apk并啟動(dòng)后,直接使用之前Optimize產(chǎn)生的odex文件,這樣就可以避免第一次啟動(dòng)時(shí)候的Optimize工作。
安裝dex的時(shí)候,核心是創(chuàng)建DexFile對(duì)象并使用其N(xiāo)ative方法對(duì)dex文件進(jìn)行opt處理,同時(shí)生產(chǎn)一個(gè)與dex文件(.zip)同名的已經(jīng)opt過(guò)的dex文件(.dex)。如果安裝dex的時(shí)候,這個(gè)opt過(guò)的dex文件已經(jīng)存在,則跳過(guò)這個(gè)過(guò)程,這會(huì)節(jié)省許多耗時(shí)。所以優(yōu)化的思路就是,下載Apk完成的時(shí)候,預(yù)先解壓dex文件,并預(yù)先觸發(fā)安裝dex文件以生產(chǎn)opt過(guò)的dex文件。這樣覆蓋安裝Apk并啟動(dòng)的時(shí)候,如果MultiDex能命中解壓好的dex和odex文件,則能避開(kāi)耗時(shí)最大的兩個(gè)操作。
不過(guò)這個(gè)方案的缺點(diǎn)也是明顯的,第一次安裝的apk沒(méi)有作用,而且事先需要使用內(nèi)置的apk更新功能把新版本的apk文件下載下來(lái)后,才能做PreMultiDex工作。
異步MultiDex方案
這種方案也是目前比較流行的Dex手動(dòng)分包方案,啟動(dòng)App的時(shí)候,先顯示一個(gè)簡(jiǎn)單的Splash閃屏界面,然后啟動(dòng)Worker線程執(zhí)行MultiDex#install(Context)工作,就可以避免UI線程阻塞。不過(guò)要確保啟動(dòng)以及啟動(dòng)MultiDex#install(Context)所需要的類(lèi)都在主dex里面(手動(dòng)分包),而且需要處理好進(jìn)程同步問(wèn)題。
參考資料:
著作信息:
本文章出自 Kaede 的博客,原創(chuàng)文章若無(wú)特別說(shuō)明,均遵循 CC BY-NC 4.0 知識(shí)共享許可協(xié)議4.0(署名-非商用-相同方式共享),可以隨意摘抄轉(zhuǎn)載,但必須標(biāo)明署名及原地址。