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

在早起的Android開發過程中,隨着項目的增加開發者可能會遇到65535的問題,也就是程序中的方法達到上限,針對此問題官方給出的解決方案是Multidex,使用Multidex將程序中的代碼打包成多個dex文件,從而讓系統的避免方法上限的問題;

1、使用

  • 使用配置
  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中的類
    在這裏插入圖片描述

2、multiDex源碼分析

在上面的使用過程中,體驗了將Android一個項目打包到連個dex的過程,且可以設置配置文件制定主dex的類文件,因爲主dex可能會影響到程序的啓動問題,也是在啓動時就加載到內存中的,那其他的dex是如何工作的呢?下面就從源碼角度看看MultiDex如何完成其他dex加載的;

  • 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;
            loader = mainContext.getClassLoader(); // 獲取系統的ClassLoader實例
            clearOldDexDir(mainContext); // 清除file/下的臨時文件信息,
         //在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;
                    }
        }
    }
}

總結:

  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;
}

總結:

  • 如果沒有強制重新加載、文件存在且沒有變化則從緩存的dex中加載

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

  • performExtractions()

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;
}

上述代碼執行邏輯:

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

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

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

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

  • 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查找類時就可以找到了;

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章