Androdi熱修復之路 —— 理解Multidex 分包原理

一、使用

  • 使用配置
  1. 在build.gradle中添加支持Multidex
defaultConfig{
  multiDexEnabled true  
}
複製代碼
  1. 添加Multidex依賴
implementation 'com.android.support:multidex:1.0.3'
複製代碼
  1. 在Application中初始化Multidex
MultiDex.install(this);
複製代碼
  1. 反編譯打包後的APK查看生成的Dex中的類
    在這裏插入圖片描述
  2. 此時主classes.dex中的類
    在這裏插入圖片描述
  • 分包(配置主multiDex中的類)
  1. 在app目錄下,建立MultiDex的配置文件multidex-config.txt
  2. 在配置文件中聲明主Dex中的類
com/alex/kotlin/jniapplication/MainActivity.class
com/alex/kotlin/jniapplication/MainActivity2.class
複製代碼
  1. 在buildTypes中設置配置文件
multiDexKeepFile file('multidex-config.txt') 複製代碼
  • 再次打包APK,反編譯APK查看此時生成的Dex中的類
    在這裏插入圖片描述

二、multiDex源碼分析

  • MultiDex.install(this);
public static void install(Context context) {
    if (IS_VM_MULTIDEX_CAPABLE) {
    } else if (VERSION.SDK_INT < 4) {  //Multidex 最小支持版本SDK_INT 爲 4
        throw new RuntimeException("MultiDex installation failed. SDK " + VERSION.SDK_INT + " is unsupported. Min SDK version is " + 4 + ".");
    } else {
        try {
            ApplicationInfo applicationInfo = getApplicationInfo(context); //獲取程序的ApplicationInfo
            //分別傳入apk安裝文件和apk的信息文件
            doInstallation(context, new File(applicationInfo.sourceDir), new File(applicationInfo.dataDir), "secondary-dexes", "", true); 
        } catch (Exception var2) {}
    }
}
複製代碼
  1. 首先獲取程序的ApplicationInfo並建立程序的sourceDir 和 dataDir 文件,調用doInstallation()加載文件信息,其中sourceDir爲程序APK的安裝目錄,要被加載的dex文件就在此文件夾下,dataDir時解析apk目錄下dex文件後的緩存目錄,全部的dex爲難都會被寫入目錄下
  • doInstallation()
private static void doInstallation(Context mainContext, File sourceApk, File dataDir, String secondaryFolderName, String prefsKeyPrefix, boolean reinstallOnPatchRecoverableException) {
    synchronized(installedApk) {
        if (!installedApk.contains(sourceApk)) { // 判讀次APk是否被加載過,加載過會自動跳過
            installedApk.add(sourceApk); // 靜態集合保存被加載的apk
            ClassLoader loader;
            try {
                loader = mainContext.getClassLoader(); //獲取系統的ClassLoader實例
            } catch (RuntimeException var25) {}
                clearOldDexDir(mainContext); //清除舊的加載信息

                 //在dataDir文件夾下建立加載dex文件的緩存目錄 code_cache/secondary-dexes
                 //若是dataDir建立失敗會建立getFilesDir()爲緩存目錄
                File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName); 
                MultiDexExtractor extractor = new MultiDexExtractor(sourceApk, dexDir); 
                //建立MultiDexExtractor實例,保存源文件目錄和緩存目錄
                try {
                    List files = extractor.load(mainContext, prefsKeyPrefix, false); //(1)加載dex文件
                    try {
                        installSecondaryDexes(loader, dexDir, files); //(2)安裝dex文件
                    } catch (IOException var26) {
                        files = extractor.load(mainContext, prefsKeyPrefix, true); //出現異常會強制從新加載,參數true
                        installSecondaryDexes(loader, dexDir, files);
                    }
                } finally {
                    try {
                        extractor.close();
                    } catch (IOException var23) {
                        closeException = var23;
                    }
        }
    }
}
複製代碼

總結:java

  1. 首先根據installedApk判斷此APK文件夾是否加載過,若是加載過直接返回,未加載執行加載程序並緩存文件夾
  2. 獲取系統的ClassLoader實例
  3. 在dataDir文件夾下建立code_cache/secondary-dexes緩存目錄
  4. 建立MultiDexExtractor實例保存apk文件目錄和緩存目錄
  5. 執行MultiDexExtractor.load()加載文件夾下的dex文件
  6. 調用installSecondaryDexes()安裝加載的dex文件數組
  7. 若是安裝出現異常則強制從新加載和安裝
  • load(Context context, String prefsKeyPrefix, boolean forceReload)
List<? extends File> load(Context context, String prefsKeyPrefix, boolean forceReload) throws IOException {
        List files;
        // 判斷是否強制從新加載 或 Dex文件是否有改變
        if (!forceReload && !isModified(context, this.sourceApk, this.sourceCrc, prefsKeyPrefix)) {
            try {
                files = this.loadExistingExtractions(context, prefsKeyPrefix);//未強制和未改變的直接從緩存的dex中加載
            } catch (IOException var6) {
                files = this.performExtractions();
                putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files);
            }
        } else {
            files = this.performExtractions(); //強制從新加載
            putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files); //緩存信息
        }
        return files;
}
複製代碼

總結:android

  • 若是沒有強制從新加載、文件存在且沒有變化則從緩存的dex中加載數組

  • 未加載過的執行加載程序並緩存信息緩存

  • performExtractions()app

private List<MultiDexExtractor.ExtractedDex> performExtractions() throws IOException {
    String extractedFilePrefix = this.sourceApk.getName() + ".classes」; //設置文件名前綴
    this.clearDexDir();
    List<MultiDexExtractor.ExtractedDex> files = new ArrayList();
    ZipFile apk = new ZipFile(this.sourceApk); //將傳入的APK文件轉換爲Zip壓縮文件
    try {
        int secondaryNumber = 2; //排除主Dex從第二個dex中開始遍歷加載
        for(ZipEntry dexFile = apk.getEntry("classes" + secondaryNumber + ".dex"); dexFile != null; dexFile = apk.getEntry("classes" + secondaryNumber + ".dex")) { //循環獲取apk文件夾下的dex文件
            String fileName = extractedFilePrefix + secondaryNumber + ".zip」; //每一個dex文件生成對應的zip文件
            //對每一個dex文件都建立一個文件extractedFile並保存到集合中 
            MultiDexExtractor.ExtractedDex extractedFile = new MultiDexExtractor.ExtractedDex(this.dexDir, fileName);
            files.add(extractedFile);
            int numAttempts = 0; //統計加載次數,默認加載3次,超過3次還不成功則加載失敗
            boolean isExtractionSuccessful = false; //標記是否加載成功
            while(numAttempts < 3 && !isExtractionSuccessful) { //循環執行加載
                ++numAttempts;
                extract(apk, dexFile, extractedFile, extractedFilePrefix); //真正執行加載的方法
                try {
                    extractedFile.crc = getZipCrc(extractedFile);  //加載成功
                    isExtractionSuccessful = true;
                } catch (IOException var18) {
                }
            }
            ++secondaryNumber; //繼續加載下一個dex文件
        }
    } finally {
    }
    return files;
}
複製代碼

上述代碼執行邏輯:ide

  • 將傳入的APK文件轉換爲Zip壓縮文件,則每一個dex文件都是Zip下的子文件源碼分析

  • 排除主Dex從第二個dex中開始遍歷加載,並根據secondaryNumber獲取Zip下個每一個dex文件,而後在code_cache/secondary-dexes目錄下爲每一個dex建立zip文件gradle

  • 對每一個dex文件都建立一個文件extractedFile並保存到集合中ui

  • 執行extract()進行文件加載this

  • extract():執行文件的寫入,將apk中除了主dex以外的dex文件逐個讀取寫入到zip文件中

private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo, String extractedFilePrefix) throws IOException, FileNotFoundException {
    InputStream in = apk.getInputStream(dexFile); //從dexFile中獲取字節流
    ZipOutputStream out = null;
    //在緩存文件夾目錄中建立臨時文件夾 tmp-***.zip
    File tmp = File.createTempFile("tmp-" + extractedFilePrefix, ".zip", extractTo.getParentFile());
    try {
        out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp))); //建立ZipOutputStream輸出流
        try {
            ZipEntry classesDex = new ZipEntry("classes.dex");
            classesDex.setTime(dexFile.getTime());
            out.putNextEntry(classesDex); //設置Zip文件
            byte[] buffer = new byte[16384];  //讀取dexFile中的字節流寫入臨時文件tem中保存在緩存文件夾下
            for(int length = in.read(buffer); length != -1; length = in.read(buffer)) {
                out.write(buffer, 0, length); //將數據寫入名爲classes.dex的Zip文件
            }
            out.closeEntry();
        } finally {
            out.close();
        }
    } finally {
        closeQuietly(in);
        tmp.delete();
    }
}
複製代碼
  • installSecondaryDexes():執行dex文件的記載,在方法中會根據系統的版本進行區分
private static void installSecondaryDexes(ClassLoader loader, File dexDir, List<? extends File> files){
    if (!files.isEmpty()) { //文件是否爲null
        if (VERSION.SDK_INT >= 19) {
            MultiDex.V19.install(loader, files, dexDir);  // 根據SDK不一樣執行不一樣的install邏輯,從緩存文件中加載
        } else if (VERSION.SDK_INT >= 14) {
            MultiDex.V14.install(loader, files);
        } else {
            MultiDex.V4.install(loader, files);
        }
    }
}
複製代碼
  • install(ClassLoader loader, List<? extends File> additionalClassPathEntries, File optimizedDirectory)
static void install(ClassLoader loader, List<? extends File> additionalClassPathEntries, File optimizedDirectory)  {
    Field pathListField = MultiDex.findField(loader, "pathList」); //反射調用ClassLoader的pathList
    Object dexPathList = pathListField.get(loader);
    ArrayList<IOException> suppressedExceptions = new ArrayList();
    //將取出的zip文件包裝成Element對象並擴招dexElements
    MultiDex.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory, suppressedExceptions));
}

private static Object[] makeDexElements(Object dexPathList, ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions)  {
     //調用dexPathList中的makeDexElements方法傳入文件集合,解析zip文件集合中的dex文件保存爲Elements
    Method makeDexElements = MultiDex.findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class, ArrayList.class);
    //執行方法
    return (Object[])((Object[])makeDexElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions));
}
複製代碼

上面主要是使用ClassLoader加載文件,關於ClassLoader不熟悉的點擊Android熱修復之路(一)——ClassLoader查看,上述代碼的執行邏輯:

  1. 反射獲取ClassLoader的pathList
  2. 調用dexPathList中的makeDexElements方法傳入文件集合,解析zip文件集合中的dex文件保存爲Elements
  3. 將取出的zip文件包裝成Element對象擴展設置給ClassLoader的pathList

關於MutilDex的加載原理就到此結束了,簡單來講就是將全部的dex都加載在Element數組中,而後擴展只ClassLoader的pathList中,在ClassLoader查找類時就能夠找到了;

相關文章
相關標籤/搜索