Multidex記錄三:原始碼解析
為什麼要用記錄呢,因為我從開始接觸Android時我們的專案就在65535的邊緣。不久Google就出了multidex的解決方案。我們也已經接入multidex好多年,但我自己還沒有接入,知其然而不知其所以然。出了問題只能自己找原始碼來分析。前兩篇文章 ofollow,noindex">Multidex記錄一:介紹和使用 和 Multidex記錄二:缺陷&解決 分別講述了怎麼接入和接入時遇到的問題,本博文只是對multidex原始碼學習過程中的分析和理解的記錄。
關於 Multidex
的相關知識點前兩章已經講的差不多了,這篇文章只分析 Multidex
的安裝。
流程圖

原始碼分析
我們先來看看 MultiDex
的安裝日誌:
I/MultiDex: VM with version 1.6.0 does not have multidex support Installing application MultiDexExtractor.load(/data/app/com.xxx.xxx-1.apk, false, ) I/MultiDex: Blocking on lock /data/data/com.xxx.xxx/code_cache/secondary-dexes/MultiDex.lock /data/data/com.xxx.xxx/code_cache/secondary-dexes/MultiDex.lock locked Detected that extraction must be performed. I/MultiDex: Extraction is needed for file /data/data/com.xxx.xxx/code_cache/secondary-dexes/com.xxx.xxx-1.apk.classes2.zip Extracting /data/data/com.xxx.xxx/code_cache/secondary-dexes/tmp-com.xxx.xxx-1.apk.classes1415547735.zip I/MultiDex: Renaming to /data/data/com.xxx.xxx/code_cache/secondary-dexes/com.xxx.xxx-1.apk.classes2.zip Extraction succeeded - length /data/data/com.xxx.xxx/code_cache/secondary-dexes/com.xxx.xxx-1.apk.classes2.zip: 4238720 - crc: 2971858359 Extraction is needed for file /data/data/com.xxx.xxx/code_cache/secondary-dexes/com.xxx.xxx-1.apk.classes3.zip Extracting /data/data/com.xxx.xxx/code_cache/secondary-dexes/tmp-com.xxx.xxx-1.apk.classes-1615165740.zip I/MultiDex: Renaming to /data/data/com.xxx.xxx/code_cache/secondary-dexes/com.xxx.xxx-1.apk.classes3.zip Extraction succeeded - length /data/data/com.xxx.xxx/code_cache/secondary-dexes/com.xxx.xxx-1.apk.classes3.zip: 3106018 - crc: 3138243730 Extraction is needed for file /data/data/com.xxx.xxx/code_cache/secondary-dexes/com.xxx.xxx-1.apk.classes4.zip Extracting /data/data/com.xxx.xxx/code_cache/secondary-dexes/tmp-com.xxx.xxx-1.apk.classes-469912688.zip I/MultiDex: Renaming to /data/data/com.xxx.xxx/code_cache/secondary-dexes/com.xxx.xxx-1.apk.classes4.zip Extraction succeeded - length /data/data/com.xxx.xxx/code_cache/secondary-dexes/com.xxx.xxx-1.apk.classes4.zip: 2163715 - crc: 1148318293 load found 3 secondary dex files I/MultiDex: install done
第二次啟動時的日誌:
I/MultiDex: VM with version 1.6.0 does not have multidex support Installing application MultiDexExtractor.load(/data/app/com.xxx.xxx-1.apk, false, ) Blocking on lock /data/data/com.xxx.xxx/code_cache/secondary-dexes/MultiDex.lock /data/data/com.xxx.xxx/code_cache/secondary-dexes/MultiDex.lock locked I/MultiDex: loading existing secondary dex files load found 3 secondary dex files install done
初始化資訊
public final class MultiDex { static final String TAG = "MultiDex"; //老版本dex檔案存放路徑 private static final String OLD_SECONDARY_FOLDER_NAME = "secondary-dexes"; //dex檔案存放路徑 code_cache/secondary-dexes private static final String SECONDARY_FOLDER_NAME; //Multidex最高支援的版本,大於20Android系統已支援 private static final int MAX_SUPPORTED_SDK_VERSION = 20; //Multidex最低支援的版本 private static final int MIN_SDK_VERSION = 4; //vm的版本資訊 private static final int VM_WITH_MULTIDEX_VERSION_MAJOR = 2; private static final int VM_WITH_MULTIDEX_VERSION_MINOR = 1; //apk路徑 private static final Set<String> installedApk; //是否支援Multidex private static final boolean IS_VM_MULTIDEX_CAPABLE; static { //SECONDARY_FOLDER_NAME=code_cache/secondary-dexes SECONDARY_FOLDER_NAME = "code_cache" + File.separator + "secondary-dexes"; installedApk = new HashSet(); //VM是否已經支援自動Multidex IS_VM_MULTIDEX_CAPABLE = isVMMultidexCapable(System.getProperty("java.vm.version")); }
Multidex安裝
public static void install(Context context) { //VM是否已經支援自動Multidex if (IS_VM_MULTIDEX_CAPABLE) { Log.i("MultiDex", "VM has multidex support, MultiDex support library is disabled."); } else if (VERSION.SDK_INT < 4) { //Multidex最低支援的版本 throw new RuntimeException("Multi dex installation failed. SDK " + VERSION.SDK_INT + " is unsupported. Min SDK version is " + 4 + "."); } else { try { /***部分程式碼省略***/ //多執行緒鎖 synchronized(installedApk) { //apkPath = data/data/com.xxx.xxx/ String apkPath = applicationInfo.sourceDir; if (installedApk.contains(apkPath)) { return; } installedApk.add(apkPath); /***部分程式碼省略***/ //清除 /data/data/com.xxx.xxx/files/secondary-dexes 目錄下的檔案 try { clearOldDexDir(context); } catch (Throwable var8) { Log.w("MultiDex", "Something went wrong when trying to clear old MultiDex extraction, continuing without cleaning.", var8); } //data/data/com.xxx.xxx/code_cache/secondary-dexes File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME); //解壓apk,獲得dex的zip檔案列表 List<File> files = MultiDexExtractor.load(context, applicationInfo, dexDir, false); //校驗zip檔案 if (checkValidZipFiles(files)) { //安裝dex檔案 installSecondaryDexes(loader, dexDir, files); } else { //校驗失敗,重新執行解壓(解壓失敗直接丟擲異常)和安裝 Log.w("MultiDex", "Files were not valid zip files.Forcing a reload."); files = MultiDexExtractor.load(context, applicationInfo, dexDir, true); if (!checkValidZipFiles(files)) { throw new RuntimeException("Zip files were not valid."); } installSecondaryDexes(loader, dexDir, files); } } } /***部分程式碼省略***/ } }
Multidex獲取dex檔案
載入dex檔案
/** * @param context * @param applicationInfo * @param dexDir /data/data/com.xxx.xxx/code_cache/secondary-dexes/ * @param forceReload 是否強制重新載入 * @return 包含dex的zip檔案列表 * @throws IOException *if an error occurs when writing the file. */ static List<File> load(Context context, ApplicationInfo applicationInfo, File dexDir, boolean forceReload) throws IOException { //data/data/com.xxx.xxx/ File sourceApk = new File(applicationInfo.sourceDir); //apk的迴圈冗餘校驗碼 long currentCrc = getZipCrc(sourceApk); List files; //是否強制執行reload和是否已經解壓過apk if (!forceReload && !isModified(context, sourceApk, currentCrc)) { try { //是否已存在zip檔案 files = loadExistingExtractions(context, sourceApk, dexDir); } catch (IOException var9) { files = performExtractions(sourceApk, dexDir); putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1); } } else { //獲取apk中的classes2.dex並且壓縮為zip檔案 files = performExtractions(sourceApk, dexDir); //儲存當前apk的資訊,作為下次有效快取的明證 putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1); } return files; } /** * @param context * @param timeStamp apk的最後一次修改時間 * @param crc apk的迴圈冗餘校驗碼 * @param totalDexNumber apk中一共有幾個dex */ private static void putStoredApkInfo(Context context, long timeStamp, long crc, int totalDexNumber) { SharedPreferences prefs = getMultiDexPreferences(context); Editor edit = prefs.edit(); edit.putLong("timestamp", timeStamp); edit.putLong("crc", crc); edit.putInt("dex.number", totalDexNumber); apply(edit); }
獲取已經存在的dex的壓縮包
/** * @param context * @param dexDir /data/data/com.xxx.xxx/code_cache/secondary-dexes/ * @param sourceApk data/app/com.xxx.xxx-1.apk * @return 包含dex的zip檔案列表 * @throws IOException *if an error occurs when writing the file. */ private static List<File> loadExistingExtractions(Context context, File sourceApk, File dexDir) throws IOException { //extractedFilePrefix = com.xxx.xxx.apk.classes String extractedFilePrefix = sourceApk.getName() + ".classes"; //totalDexNumber=apk中dex的數量 int totalDexNumber = getMultiDexPreferences(context).getInt("dex.number", 1); List<File> files = new ArrayList(totalDexNumber); //主dex已經載入過了,載入class2.dex,class3.dex...... for(int secondaryNumber = 2; secondaryNumber <= totalDexNumber; ++secondaryNumber) { //fileName = com.xxx.xxx.apk.classes2.zip String fileName = extractedFilePrefix + secondaryNumber + ".zip"; //extractedFile = data/data/com.xxx.xxx/code_cache/secondary-dexes/com.wuba.bangjob-1.apk.classes2.zip File extractedFile = new File(dexDir, fileName); if (!extractedFile.isFile()) { throw new IOException("Missing extracted secondary dex file '" + extractedFile.getPath() + "'"); } files.add(extractedFile); //校驗zip檔案是否完整 if (!verifyZipFile(extractedFile)) { Log.i("MultiDex", "Invalid zip file: " + extractedFile); throw new IOException("Invalid ZIP file."); } } return files; }
生成dex的壓縮zip檔案
/** * @param sourceApk data/app/com.xxx.xxx-1.apk * @param dexDir /data/data/com.xxx.xxx/code_cache/secondary-dexes/ * @return 包含dex的zip檔案列表 * @throws IOException *if an error occurs when writing the file. */ private static List<File> performExtractions(File sourceApk, File dexDir) throws IOException { //extractedFilePrefix = com.xxx.xxx.apk.classes String extractedFilePrefix = sourceApk.getName() + ".classes"; //刪除data/data/com.xxx.xxx/code_cache/secondary-dexes/目錄下的檔案 prepareDexDir(dexDir, extractedFilePrefix); List<File> files = new ArrayList(); ZipFile apk = new ZipFile(sourceApk); try { //從class2.dex開始 int secondaryNumber = 2; for(ZipEntry dexFile = apk.getEntry("classes" + secondaryNumber + ".dex"); dexFile != null; dexFile = apk.getEntry("classes" + secondaryNumber + ".dex")) { //fileName = com.xxx.xxx.apk.classes2.zip String fileName = extractedFilePrefix + secondaryNumber + ".zip"; File extractedFile = new File(dexDir, fileName); files.add(extractedFile); //最多三次嘗試生成dex的zip檔案 int numAttempts = 0; //標識是否生成dex的zip檔案 boolean isExtractionSuccessful = false; while(numAttempts < 3 && !isExtractionSuccessful) { ++numAttempts; extract(apk, dexFile, extractedFile, extractedFilePrefix); //校驗壓縮檔案是否完整,否則刪除重來 isExtractionSuccessful = verifyZipFile(extractedFile); if (!isExtractionSuccessful) { extractedFile.delete(); if (extractedFile.exists()) { Log.w("MultiDex", "Failed to delete corrupted secondary dex '" + extractedFile.getPath() + "'"); } } } if (!isExtractionSuccessful) { throw new IOException("Could not create zip file " + extractedFile.getAbsolutePath() + " for secondary dex (" + secondaryNumber + ")"); } ++secondaryNumber; } }/***部分程式碼省略***/ return files; }
將classes2.dex放入zip檔案中
/** * @param apk apk的壓縮包 * @param dexFile apk中的classes2.dex檔案 * @param extractTo /data/data/com.xxx.xxx/code_cache/secondary-dexes/com.xxx.xxx.apk.classes2.zip * @param extractedFilePrefixcom.wuba.bangjob-1.apk.classes * @throws IOException *if an error occurs when writing the file. */ private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo, String extractedFilePrefix) throws IOException, FileNotFoundException { InputStream in = apk.getInputStream(dexFile); ZipOutputStream out = null; ///data/data/com.wuba.bangjob/code_cache/secondary-dexes/com.xxx.xxx.apk.classes1415547735.zip File tmp = File.createTempFile(extractedFilePrefix, ".zip", extractTo.getParentFile()); try { out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp))); try { //將classes2.dex進行壓縮內容名稱為classes.dex ZipEntry classesDex = new ZipEntry("classes.dex"); classesDex.setTime(dexFile.getTime()); out.putNextEntry(classesDex); byte[] buffer = new byte[16384]; for(int length = in.read(buffer); length != -1; length = in.read(buffer)) { out.write(buffer, 0, length); } out.closeEntry(); } finally { out.close(); } //重新命名檔案為com.xxx.xxx.apk.classes2.zip if (!tmp.renameTo(extractTo)) { throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() + "\" to \"" + extractTo.getAbsolutePath() + "\""); } }/***部分程式碼省略***/ }
dex檔案的裝載
將含有載入含有dex的壓縮包進行夾雜,相關知識點參考: Android類載入之PathClassLoader和DexClassLoader 。
為什麼需要做版本的區分,就是因為版本見類載入的實現是有些差異的。
/** * @param loader * @param dexDir /data/data/xxx.xxx.xxx/code_cache/secondary-dexes/ * @param files dex的壓縮zip檔案 */ private static void installSecondaryDexes(ClassLoader loader, File dexDir, List<File> files) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException { if (!files.isEmpty()) { if (VERSION.SDK_INT >= 19) {//KitKat(19) MultiDex.V19.install(loader, files, dexDir); } else if (VERSION.SDK_INT >= 14) {//IceCreamSandwich(14,15),JellyBean(16,17,18) MultiDex.V14.install(loader, files, dexDir); } else { MultiDex.V4.install(loader, files); } } }
Android類載入之PathClassLoader和DexClassLoader 這篇文章將的是 VERSION.SDK_INT >= 19
那麼我們先看看第一個case:
private static final class V19 { private V19() { } /** * @param loader PathClassLoader * @param additionalClassPathEntries dex壓縮包路徑 * @param optimizedDirectory opt之後的dex檔案目錄 */ private static void install(ClassLoader loader, List<File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException { //PathClassLoader的DexPathList型別變數pathList Field pathListField = MultiDex.findField(loader, "pathList"); Object dexPathList = pathListField.get(loader); ArrayList<IOException> suppressedExceptions = new ArrayList(); //進行dex的opt併合並dex的Element MultiDex.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory, suppressedExceptions)); //處理異常 if (suppressedExceptions.size() > 0) { Iterator i$ = suppressedExceptions.iterator(); while(i$.hasNext()) { IOException e = (IOException)i$.next(); Log.w("MultiDex", "Exception in makeDexElement", e); } //一直感覺Google原始碼這塊有些問題,loader應該改為dexPathList。因為dexElementsSuppressedExceptions變數是屬於DexPathList的成員 Field suppressedExceptionsField = MultiDex.findField(loader, "dexElementsSuppressedExceptions"); IOException[] dexElementsSuppressedExceptions = (IOException[])((IOException[])suppressedExceptionsField.get(loader)); if (dexElementsSuppressedExceptions == null) { dexElementsSuppressedExceptions = (IOException[])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(loader, dexElementsSuppressedExceptions); } } //呼叫DexPathList的makeDexElements方法進行dex的opt private static Object[] makeDexElements(Object dexPathList, ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { Method makeDexElements = MultiDex.findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class, ArrayList.class); return (Object[])((Object[])makeDexElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions)); } }
19 > VERSION.SDK_INT >= 14
的處理支援比 VERSION.SDK_INT >= 19
少了異常處理。是因為DexPathList的 makeDexElements
方法有了修改。
/** * VERSION.SDK_INT >= 19 */ private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions); /** * 19 > VERSION.SDK_INT >= 14 */ private static Element[] makeDexElements(ArrayList<File> files,File optimizedDirectory);
VERSION_SDK_INT < 14
的情況和前面的都不相同是因為 PathClassLoader
的是實現就不相同。。。。
/** * VERSION.SDK_INT == 13 */ public class PathClassLoader extends ClassLoader { private final String path; private final String libPath; /* * Parallel arrays for jar/apk files. * * (could stuff these into an object and have a single array; * improves clarity but adds overhead) */ private final String[] mPaths; private final File[] mFiles; private final ZipFile[] mZips; private final DexFile[] mDexs; /***部分程式碼省略***/ }
apk解壓結果預覽

文章到這裡就全部講述完啦,若有其他需要交流的可以留言哦~!~!
想閱讀作者的更多文章,可以檢視我個人部落格 和公共號:
