Android-Multidex安装流程解析
Android-Multidex安裝流程解析
關于為什么需要引入Multidex支持以及如何配置Multidex可參考官網,本篇不做闡述,本篇著重分析Multidex1.0.2源碼進行分析
大家都知道配置Multidex都需要在Application中的添加這樣代碼
public class MyApplication extends SomeOtherApplication {@Overrideprotected void attachBaseContext(Context base) {super.attachBaseContext(context);Multidex.install(this);} }其關鍵代碼是Multidex類的install方法,我們來看看內部如何實現
public final class MultiDex {static final String TAG = "MultiDex";private static final Set<File> installedApk = new HashSet();private static final boolean IS_VM_MULTIDEX_CAPABLE = isVMMultidexCapable(System.getProperty("java.vm.version"));...private MultiDex() {}public static void install(Context context) {Log.i("MultiDex", "Installing application");//1、判斷虛擬機是否支持MultiDexif(IS_VM_MULTIDEX_CAPABLE) {Log.i("MultiDex", "VM has multidex support, MultiDex support library is disabled.");} else if(VERSION.SDK_INT < 4) {//2、Android SDK小于4不支持Multidexthrow new RuntimeException("MultiDex installation failed. SDK " + VERSION.SDK_INT + " is unsupported. Min SDK version is " + 4 + ".");} else {try {ApplicationInfo applicationInfo = getApplicationInfo(context);if(applicationInfo == null) {Log.i("MultiDex", "No ApplicationInfo available, i.e. running on a test Context: MultiDex support library is disabled.");return;}//3、關鍵代碼doInstallation(context, new File(applicationInfo.sourceDir), new File(applicationInfo.dataDir), "secondary-dexes", "");} catch (Exception var2) {Log.e("MultiDex", "MultiDex installation failure", var2);throw new RuntimeException("MultiDex installation failed (" + var2.getMessage() + ").");}Log.i("MultiDex", "install done");}}static boolean isVMMultidexCapable(String versionString) {boolean isMultidexCapable = false;if(versionString != null) {Matcher matcher = Pattern.compile("(\\d+)\\.(\\d+)(\\.\\d+)?").matcher(versionString);if(matcher.matches()) {try {int major = Integer.parseInt(matcher.group(1));int minor = Integer.parseInt(matcher.group(2));isMultidexCapable = major > 2 || major == 2 && minor >= 1;} catch (NumberFormatException var5) {;}}}Log.i("MultiDex", "VM with version " + versionString + (isMultidexCapable?" has multidex support":" does not have multidex support"));return isMultidexCapable;} }小結:Multidex.install方法可分為如下3步
1、判斷VM是否原生支持Multidex,如果支持就不進行Multidex.install方法
2、如果Android SDK小于4,直接拋出異常提示不支持Multidex
3、也是最關鍵的代碼即doInstallation方法
繼續看doInstallation方法
private static void doInstallation(Context mainContext, File sourceApk, File dataDir, String secondaryFolderName, String prefsKeyPrefix) throws IOException, IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException {//installedApk是個全局靜態變量Set集合Set var5 = installedApk;synchronized(installedApk) {if(!installedApk.contains(sourceApk)) {//1、如果集合中不存在將souceApk文件加入其中installedApk.add(sourceApk);//Android5.0以及以上版本使用ART機制運行,后者原生支持從 APK 文件加載多個 DEX 文件if(VERSION.SDK_INT > 20) {Log.w("MultiDex", "MultiDex is not guaranteed to work in SDK version " + VERSION.SDK_INT + ": SDK version higher than " + 20 + " 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") + "\"");}ClassLoader loader;try {//此處獲取的類型為PathClassLoader,后面安裝非主dex會使用到loader = mainContext.getClassLoader();} catch (RuntimeException var11) {Log.w("MultiDex", "Failure while trying to obtain Context class loader. Must be running in test mode. Skip patching.", var11);return;}if(loader == null) {Log.e("MultiDex", "Context class loader is null. Must be running in test mode. Skip patching.");} else {try {//2、刪除secondar-dexs目錄(/data/data/app_package_name/files/secondary-dexes)clearOldDexDir(mainContext);} catch (Throwable var10) {Log.w("MultiDex", "Something went wrong when trying to clear old MultiDex extraction, continuing without cleaning.", var10);}//3、創建"secondar-dexs"目錄,用于存儲提取的zipFile dexDir = getDexDir(mainContext, dataDir, secondaryFolderName);//4、從副dex中提取zip并返回zip列表List<? extends File> files = MultiDexExtractor.load(mainContext, sourceApk, dexDir, prefsKeyPrefix, false);//5、將提取出所有zip添加到BaseDexClassLoader中pathList屬性installSecondaryDexes(loader, dexDir, files);}}}}//刪除舊dex目錄 private static void clearOldDexDir(Context context) throws Exception {File dexDir = new File(context.getFilesDir(), "secondary-dexes");if(dexDir.isDirectory()) {Log.i("MultiDex", "Clearing old secondary dex dir (" + dexDir.getPath() + ").");File[] files = dexDir.listFiles();if(files == null) {Log.w("MultiDex", "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");return;}File[] var3 = files;int var4 = files.length;for(int var5 = 0; var5 < var4; ++var5) {File oldFile = var3[var5];Log.i("MultiDex", "Trying to delete old file " + oldFile.getPath() + " of size " + oldFile.length());if(!oldFile.delete()) {Log.w("MultiDex", "Failed to delete old file " + oldFile.getPath());} else {Log.i("MultiDex", "Deleted old file " + oldFile.getPath());}}if(!dexDir.delete()) {Log.w("MultiDex", "Failed to delete secondary dex dir " + dexDir.getPath());} else {Log.i("MultiDex", "Deleted old secondary dex dir " + dexDir.getPath());}}}private static File getDexDir(Context context, File dataDir, String secondaryFolderName) throws IOException {//默認使用/data/data/app_package_name/code_cache/作為存放提取文件(zip)的父目錄File cache = new File(dataDir, "code_cache");try {mkdirChecked(cache);} catch (IOException var5) {cache = new File(context.getFilesDir(), "code_cache");mkdirChecked(cache);}//在code_cache目錄下新建secondary-dexes子目錄,以存放zip文件File dexDir = new File(cache, secondaryFolderName);mkdirChecked(dexDir);return dexDir;}private static void mkdirChecked(File dir) throws IOException {dir.mkdir();if(!dir.isDirectory()) {File parent = dir.getParentFile();if(parent == null) {Log.e("MultiDex", "Failed to create dir " + dir.getPath() + ". Parent file is null.");} else {Log.e("MultiDex", "Failed to create dir " + dir.getPath() + ". parent file is a dir " + parent.isDirectory() + ", a file " + parent.isFile() + ", exists " + parent.exists() + ", readable " + parent.canRead() + ", writable " + parent.canWrite());}throw new IOException("Failed to create directory " + dir.getPath());}}我們小結下Multidex.doInstallation方法基本的四大步驟
1. 判斷sourceApk文件是否存在installedApk集合中,不存在則將其添加到集合中,否則不做處理
2. 刪除舊副dex目錄(/data/data/app_package_name/files/secondary-dexes)
3. 提取zip并返回zip的列表
4. 修改pathList
前二步好理解直接看源碼即可,我們重點來看第三步,第四步
提取zip并返回zip的列表
代碼如下
//提取的zip文件存放的目錄,正常情況下目錄為/data/data/you_package_name/code_cache/secondary-dexes File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName);List<? extends File> files = MultiDexExtractor.load(mainContext, sourceApk, dexDir, prefsKeyPrefix, false);我們繼續看MultiDexExtractor.load方法
static List<? extends File> load(Context context, File sourceApk, File dexDir, String prefsKeyPrefix, boolean forceReload) throws IOException {Log.i("MultiDex", "MultiDexExtractor.load(" + sourceApk.getPath() + ", " + forceReload + ", " + prefsKeyPrefix + ")");//獲取sourceApk循環冗余校驗碼long currentCrc = getZipCrc(sourceApk);// 使用文件鎖來保證進程間安裝multidex正常File lockFile = new File(dexDir, "MultiDex.lock");RandomAccessFile lockRaf = new RandomAccessFile(lockFile, "rw");FileChannel lockChannel = null;FileLock cacheLock = null;IOException releaseLockException = null;List files;try {lockChannel = lockRaf.getChannel();Log.i("MultiDex", "Blocking on lock " + lockFile.getPath());//1、使用文件鎖,阻塞當前線程直到獲取鎖為止cacheLock = lockChannel.lock();Log.i("MultiDex", lockFile.getPath() + " locked");//2、如果不是強制重新加載且sourceApk沒有修改則直接加載已存在的文件if(!forceReload && !isModified(context, sourceApk, currentCrc, prefsKeyPrefix)) {try {//加載之前提取過的zip文件files = loadExistingExtractions(context, sourceApk, dexDir, prefsKeyPrefix);} catch (IOException var21) {Log.w("MultiDex", "Failed to reload existing extracted secondary dex files, falling back to fresh extraction", var21);//異常則執行步驟3files = performExtractions(sourceApk, dexDir);putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), currentCrc, files);}} else {//3、執行提取操作Log.i("MultiDex", "Detected that extraction must be performed.");files = performExtractions(sourceApk, dexDir);//保存提取出的文件相關屬性(apk的crc、timeStamp(**lastModified**())以及所有副dex的crc、timeStamp)putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), currentCrc, files);}} finally {//4、執行收尾操作,譬如文件鎖的釋放等...if(cacheLock != null) {try {cacheLock.release();} catch (IOException var20) {Log.e("MultiDex", "Failed to release lock on " + lockFile.getPath());releaseLockException = var20;}}if(lockChannel != null) {closeQuietly(lockChannel);}closeQuietly(lockRaf);}//5、判斷釋放鎖是否出現異常,如果有直接拋出標志提取或加載文件失敗,否則返回提取文件列表if(releaseLockException != null) {throw releaseLockException;} else {Log.i("MultiDex", "load found " + files.size() + " secondary dex files");return files;}}小結下其實分為5個小步驟
1. 使用文件鎖來保證加載提取文件操作/提取文件操作的安全性
2. 加載之前提取的zip文件
3. 提取文件操作
4. 文件資源釋放(執行收尾操作,譬如文件鎖的釋放等)
5. 根據釋放文件鎖是否有異常決定是拋出異常還是正常返回文件列表
上述步驟關鍵步驟為2、3步;我們按app首次安裝流程來分析,先看第三步提取文件操作
提取文件操作
廢話不多說,直接上代碼
private static List<MultiDexExtractor.ExtractedDex> performExtractions(File sourceApk, File dexDir) throws IOException {//定義提取zip文件的名稱前綴(fileName.apk.classes)String extractedFilePrefix = sourceApk.getName() + ".classes";//在dexDir目錄清理舊文件prepareDexDir(dexDir, extractedFilePrefix);//創建數組用于存放提取的zip文件和方法返回List<MultiDexExtractor.ExtractedDex> files = new ArrayList();/***解析apk壓縮包*/ZipFile apk = new ZipFile(sourceApk);try {int secondaryNumber = 2;//從apk文件中嘗試尋找"classes2.dex","chasses3.dex"..."classesN.dex"直到找到所有為止for(ZipEntry dexFile = apk.getEntry("classes" + secondaryNumber + ".dex"); dexFile != null; dexFile = apk.getEntry("classes" + secondaryNumber + ".dex")) {//每找到一個副dex,立馬創建一個相對應名為"fileName.apk.classesN.zip"的提取文件String fileName = extractedFilePrefix + secondaryNumber + ".zip";MultiDexExtractor.ExtractedDex extractedFile = new MultiDexExtractor.ExtractedDex(dexDir, fileName);files.add(extractedFile);Log.i("MultiDex", "Extraction is needed for file " + extractedFile);//每次提取時有三次重試機會int numAttempts = 0;boolean isExtractionSuccessful = false;while(numAttempts < 3 && !isExtractionSuccessful) {++numAttempts;//執行提取操作extract(apk, dexFile, extractedFile, extractedFilePrefix);try {//從提取文件中讀取crc校驗碼,如果讀取失敗視為提取失敗extractedFile.crc = getZipCrc(extractedFile);isExtractionSuccessful = true;} catch (IOException var19) {isExtractionSuccessful = false;Log.w("MultiDex", "Failed to read crc from " + extractedFile.getAbsolutePath(), var19);}Log.i("MultiDex", "Extraction " + (isExtractionSuccessful?"succeeded":"failed") + " - length " + extractedFile.getAbsolutePath() + ": " + extractedFile.length() + " - crc: " + extractedFile.crc);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;}} finally {//釋放資源try {apk.close();} catch (IOException var18) {Log.w("MultiDex", "Failed to close resource", var18);}}return files;}小結下上面方法做了哪些事
1. 清理dexDir舊文件
2. 創建數組用于保存提取文件
3. 從apk源包中尋找到所有副dex,每找到一個副dex嘗試提取(有三次重試機會)并將提取到文件存放到數組中
4. 釋放資源并返回數組
其實真正到提取還是第三步中的extract方法
extract其實內部非常簡單
大體就是將副dex數據拷貝一份生成一個classes.dex文件,并將該文件壓縮成zip
這個zip就是我們需要的提取文件
extract就不在細說了,大家有興趣自己看
extract(apk, dexFile, extractedFile, extractedFilePrefix);apk: zipFile
dexFile:apk中副dex(classesN.dex)
extractTo:提取文件(fileName.apk.classesN.zip)
extractedFilePrefix:提取文件前綴(fileName.apk.classes)
至此提取文件的流程已分析完畢
我們再回過頭來看看MultiDexExtractor.load方法的第二步加載之前提取的zip文件
加載之前提取的zip文件
因為提取文件操作后還有一個保存文件信息的操作(見下面代碼)
該操作就是為了加載zip文件服務,因為加載zip需要知道有幾個zip需要加載,加載的zip的路徑等
源碼如下很簡單不解釋了
//保存提取文件相關信息 private static void putStoredApkInfo(Context context, String keyPrefix, long timeStamp, long crc, List<MultiDexExtractor.ExtractedDex> extractedDexes) {SharedPreferences prefs = getMultiDexPreferences(context);Editor edit = prefs.edit();edit.putLong(keyPrefix + "timestamp", timeStamp);edit.putLong(keyPrefix + "crc", crc);edit.putInt(keyPrefix + "dex.number", extractedDexes.size() + 1);int extractedDexId = 2;for(Iterator var10 = extractedDexes.iterator(); var10.hasNext(); ++extractedDexId) {MultiDexExtractor.ExtractedDex dex = (MultiDexExtractor.ExtractedDex)var10.next();edit.putLong(keyPrefix + "dex.crc." + extractedDexId, dex.crc);edit.putLong(keyPrefix + "dex.time." + extractedDexId, dex.lastModified());}edit.commit();}//加載提取文件 private static List<MultiDexExtractor.ExtractedDex> loadExistingExtractions(Context context, File sourceApk, File dexDir, String prefsKeyPrefix) throws IOException {Log.i("MultiDex", "loading existing secondary dex files");String extractedFilePrefix = sourceApk.getName() + ".classes";SharedPreferences multiDexPreferences = getMultiDexPreferences(context);int totalDexNumber = multiDexPreferences.getInt(prefsKeyPrefix + "dex.number", 1);List<MultiDexExtractor.ExtractedDex> files = new ArrayList(totalDexNumber - 1);for(int secondaryNumber = 2; secondaryNumber <= totalDexNumber; ++secondaryNumber) {String fileName = extractedFilePrefix + secondaryNumber + ".zip";MultiDexExtractor.ExtractedDex extractedFile = new MultiDexExtractor.ExtractedDex(dexDir, fileName);if(!extractedFile.isFile()) {throw new IOException("Missing extracted secondary dex file '" + extractedFile.getPath() + "'");}extractedFile.crc = getZipCrc(extractedFile);long expectedCrc = multiDexPreferences.getLong(prefsKeyPrefix + "dex.crc." + secondaryNumber, -1L);long expectedModTime = multiDexPreferences.getLong(prefsKeyPrefix + "dex.time." + secondaryNumber, -1L);long lastModified = extractedFile.lastModified();if(expectedModTime != lastModified || expectedCrc != extractedFile.crc) {throw new IOException("Invalid extracted dex: " + extractedFile + " (key \"" + prefsKeyPrefix + "\"), expected modification time: " + expectedModTime + ", modification time: " + lastModified + ", expected crc: " + expectedCrc + ", file crc: " + extractedFile.crc);}files.add(extractedFile);}return files;}我們在回過頭來看看Multidex.doInstallation的最后一大步
修改pathList
修改pathList其實就是修改BaseDexClassLoader中pathList屬性,我們以V19版本源碼為例進行分析(V14、V4代碼基本差不多)
private static void installSecondaryDexes(ClassLoader loader, File dexDir, List<? extends File> files) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {if(!files.isEmpty()) {if(VERSION.SDK_INT >= 19) {MultiDex.V19.install(loader, files, dexDir);} else if(VERSION.SDK_INT >= 14) {MultiDex.V14.install(loader, files, dexDir);} else {MultiDex.V4.install(loader, files);}}} private static final class V19 {private V19() {}//修改BaseDexClassLoader中pathList的dexElements、dexElementsSuppressedExceptions數組private static void install(ClassLoader loader, List<? extends File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException {Field pathListField = MultiDex.findField(loader, "pathList");//1、通過反射獲取pathList變量Object dexPathList = pathListField.get(loader);ArrayList<IOException> suppressedExceptions = new ArrayList();//2、修改dexElements數組//構造一個新數組將原數組添加其中,在將所有提取zip追加到新數組中;//suppressedExceptions則是用來存在在加載副dex過程中出現到異常MultiDex.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory, suppressedExceptions));if(suppressedExceptions.size() > 0) {Iterator var6 = suppressedExceptions.iterator();while(var6.hasNext()) {IOException e = (IOException)var6.next();Log.w("MultiDex", "Exception in makeDexElement", e);}Field suppressedExceptionsField = MultiDex.findField(dexPathList, "dexElementsSuppressedExceptions");//反射來后去DexPathList中dexElementsSuppressedExceptions數組IOException[] dexElementsSuppressedExceptions = (IOException[])((IOException[])suppressedExceptionsField.get(dexPathList));//判斷dexElementsSuppressedExceptions為空直接將其修改為suppressedExceptionsif(dexElementsSuppressedExceptions == null) {dexElementsSuppressedExceptions = (IOException[])suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);} else {//如果dexElementsSuppressedExceptions不空則將創建數組,并先將加載提取文件到異常放到新建數組前面//在將原有dexElementsSuppressedExceptions數據追加其后,并通過反射將dexElementsSuppressedExceptions設置為新建數組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);}}//調用DexPathList類中的makeDexElements方法private static Object[] makeDexElements(Object dexPathList, ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {Method makeDexElements = MultiDex.findMethod(dexPathList, "makeDexElements", new Class[]{ArrayList.class, File.class, ArrayList.class});return (Object[])((Object[])makeDexElements.invoke(dexPathList, new Object[]{files, optimizedDirectory, suppressedExceptions}));}}至此Multidex安裝流程已全部完畢,說了這么多,其實最關鍵的就二步(提取、修改pathList)大家可以參照下面圖理解
總結
以上是生活随笔為你收集整理的Android-Multidex安装流程解析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 迁移至Android3.0遇到一些问题
- 下一篇: android sina oauth2.