類加載機(jī)制系列3——MultiDex原理解析

1 MultiDex的由來

Android中由于一個(gè)dex文件最多存儲(chǔ)65536個(gè)方法,也就是一個(gè)short類型的范圍,所以隨著應(yīng)用的類不斷增加,當(dāng)一個(gè)dex文件突破這個(gè)方法數(shù)的時(shí)候就會(huì)報(bào)出異常。雖然可以通過混淆等方式來減少無用的方法,但是隨著APP功能的增多,突破方法數(shù)限制還是不可避免的。因此在Android5.0時(shí),Android推出了官方的解決方案:MultiDex。打包的時(shí)候,把一個(gè)應(yīng)用分成多個(gè)dex,例如:classes.dex、classes2.dex、classes3.dex...,加載的時(shí)候把這些dex都追加到DexPathList對(duì)應(yīng)的數(shù)組中,這樣就解決了方法數(shù)的限制。

5.0后的系統(tǒng)都內(nèi)置了加載多個(gè)dex文件的功能,而在5.0之前,系統(tǒng)只可以加載一個(gè)主dex,其它的dex就需要采用一定的手段來加載。這也就是我們今天要講的MultiDex。

MultiDex存放在android.support.multidex包下。

2 MultiDex的使用

Gradle構(gòu)建環(huán)境下,在主應(yīng)用的build.gradle文件夾添加如下配置:

defaultConfig {
    ...
    multiDexEnabled true
    ...
}

dependencies {
    compile 'com.android.support:multidex:1.0.1'
    ...
}

現(xiàn)在最新的multidex版本是1.0.2。

在AndroidManifest.xml中的app節(jié)點(diǎn)下,使用MultiDexApplication作為應(yīng)用入口。

package android.support.multidex;
...
public class MultiDexApplication extends Application {
  @Override
  protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    MultiDex.install(this);
  }
}

當(dāng)然了,大部分情況下,我們都會(huì)自定義一個(gè)自己的Application對(duì)應(yīng)用做一些初始化。這種情況下,可以在我們自定義的Application中的attachBaseContext()方法中調(diào)用MultiDex.install()方法。

# 自定義的Applicaiton中
  @Override
  protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    MultiDex.install(this);
  }

需要注意的是:MultiDex.install()方法的調(diào)用時(shí)機(jī)要盡可能的早,防止加載后面的dex文件中的類時(shí)報(bào)ClassNotFoundException。

3 MultiDex源碼分析

分析MultiDex的的入口就是它的靜態(tài)方法install()。
這個(gè)方法的作用就是把從應(yīng)用的APK文件中的dex添加到應(yīng)用的類加載器PathClassLoader中的DexPathList的Emlement數(shù)組中。

public static void install(Context context) {
    Log.i(TAG, "install");
    //判斷Android系統(tǒng)是否已經(jīng)支持了MultiDex,如果支持了就不需要再去安裝了,直接返回
    if (IS_VM_MULTIDEX_CAPABLE) {
        Log.i(TAG, "VM has multidex support, MultiDex support library is disabled.");
        return;
    }

    // 如果Android系統(tǒng)低于MultiDex最低支持的版本就拋出異常
    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 {
        // 獲取應(yīng)用信息
        ApplicationInfo applicationInfo = getApplicationInfo(context);
        // 如果應(yīng)用信息為空就返回,比如說運(yùn)行在一個(gè)測(cè)試的Context下。
        if (applicationInfo == null) {
            // Looks like running on a test Context, so just return without patching.
            return;
        }
        // 同步方法
        synchronized (installedApk) {
            // 獲取已經(jīng)安裝的APK的全路徑
            String apkPath = applicationInfo.sourceDir;
            if (installedApk.contains(apkPath)) {
                return;
            }
            // 把路徑添加到已經(jīng)安裝的APK路徑中
            installedApk.add(apkPath);
            // 如果編譯版本大于最大支持版本,報(bào)一個(gè)警告
            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") + "\"");
            }
            /* The patched class loader is expected to be a descendant of
             * dalvik.system.BaseDexClassLoader. We modify its
             * dalvik.system.DexPathList pathList field to append additional DEX
             * file entries.
             */
            ClassLoader loader;
            try {
                // 獲取ClassLoader,實(shí)際上是PathClassLoader
                loader = context.getClassLoader();
            } catch (RuntimeException e) {
                /* Ignore those exceptions so that we don't break tests relying on Context like
                 * a android.test.mock.MockContext or a android.content.ContextWrapper with a
                 * null base Context.
                 */
                Log.w(TAG, "Failure while trying to obtain Context class loader. " +
                        "Must be running in test mode. Skip patching.", e);
                return;
            }
            // 在某些測(cè)試環(huán)境下ClassLoader為null
            if (loader == null) {
                // Note, the context class loader is null when running Robolectric tests.
                Log.e(TAG,
                        "Context class loader is null. Must be running in test mode. "
                                + "Skip patching.");
                return;
            }
            try {
                // 清除老的緩存的Dex目錄,來源的緩存目錄是"/data/user/0/${packageName}/files/secondary-dexes"
                clearOldDexDir(context);
            } catch (Throwable t) {
                Log.w(TAG, "Something went wrong when trying to clear old MultiDex extraction, "
                        + "continuing without cleaning.", t);
            }
            // 新建一個(gè)存放dex的目錄,路徑是"/data/user/0/${packageName}/code_cache/secondary-dexes",用來存放優(yōu)化后的dex文件
            File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME);
            // 使用MultiDexExtractor這個(gè)工具類把APK中的dex抽取到dexDir目錄中,返回的files集合有可能為空,表示沒有secondaryDex
            // 不強(qiáng)制重新加載,也就是說如果已經(jīng)抽取過了,可以直接從緩存目錄中拿來使用,這么做速度比較快
            List<File> files = MultiDexExtractor.load(context, applicationInfo, dexDir, false);
            if (checkValidZipFiles(files)) {
                // 如果抽取的文件是有效的,就安裝secondaryDex
                installSecondaryDexes(loader, dexDir, files);
            } else {
                Log.w(TAG, "Files were not valid zip files. Forcing a reload.");
                // Try again, but this time force a reload of the zip file.
                // 如果抽取出的文件是無效的,那么就強(qiáng)制重新加載,這么做的話速度就慢了一點(diǎn),有一些IO開銷
                files = MultiDexExtractor.load(context, applicationInfo, dexDir, true);
                if (checkValidZipFiles(files)) {
                    // 強(qiáng)制加載后,如果文件有效就安裝,否則就拋出異常
                    installSecondaryDexes(loader, dexDir, files);
                } else {
                    // Second time didn't work, give up
                    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");
}

關(guān)于dex文件抽取邏輯和校驗(yàn)邏輯我們先不管,我們看一下MultiDex是如何安裝secondaryDex文件的。
由于不同版本的Android系統(tǒng),類加載機(jī)制有一些不同,所以分為了V19、V14和V4等三種情況下的安裝。V19、V14和V4都是MultiDex的private的靜態(tài)內(nèi)部類。V19支持Andorid19版本(20是只支持可穿戴設(shè)備的),V14支持14,、15、16、17 和 18版本,V4支持從4到13的版本。

# android.support.multidex.MultiDex
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);
        }
    }
}

我們來看一下V19的源碼

/**
 * 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 {
        /* The patched class loader is expected to be a descendant of
         * dalvik.system.BaseDexClassLoader. We modify its
         * dalvik.system.DexPathList pathList field to append additional DEX
         * file entries.
         */
        // 傳遞的loader是PathClassLoader,findFidld()方法是遍歷loader及其父類找到pathList字段
        // 實(shí)際上就是找到BaseClassLoader中的DexPathList
        Field pathListField = findField(loader, "pathList");
        // 獲取PathClassLoader綁定的DexPathList對(duì)象
        Object dexPathList = pathListField.get(loader);
        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        // 擴(kuò)展DexPathList對(duì)象的Element數(shù)組,數(shù)組名是dexElements
        // makeDexElements()方法的作用就是調(diào)用DexPathList的makeDexElements()方法來創(chuàng)建dex元素
        expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
                new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
                suppressedExceptions));
        // 后面就是添加一些IO異常信息,因?yàn)檎{(diào)用DexPathList的makeDexElements會(huì)有一些IO操作,相應(yīng)的可能就會(huì)有一些異常情況
        if (suppressedExceptions.size() > 0) {
            for (IOException e : suppressedExceptions) {
                Log.w(TAG, "Exception in makeDexElement", e);
            }
            Field suppressedExceptionsField =
                    findField(loader, "dexElementsSuppressedExceptions");
            IOException[] dexElementsSuppressedExceptions =
                    (IOException[]) suppressedExceptionsField.get(loader);
            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(loader, dexElementsSuppressedExceptions);
        }
    }
    /**
     * A wrapper around
     * {@code private static final dalvik.system.DexPathList#makeDexElements}.
     */
    // 通過反射的方式調(diào)用DexPathList#makeDexElements()方法
    // dexPathList 就是一個(gè)DexPathList對(duì)象
    private static Object[] makeDexElements(
            Object dexPathList, ArrayList<File> files, File optimizedDirectory,
            ArrayList<IOException> suppressedExceptions)
                    throws IllegalAccessException, InvocationTargetException,
                    NoSuchMethodException {
        // 獲取DexPathList的makeDexElements()方法
        Method makeDexElements =
                findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class,
                        ArrayList.class);
        // 調(diào)用makeDexElements()方法,根據(jù)外界傳遞的包含dex文件的源文件和優(yōu)化后的緩存目錄返回一個(gè)Element[]數(shù)組
        return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory,
                suppressedExceptions);
    }
}

MultiDex的expandFieldArray()方法作用是擴(kuò)展一個(gè)對(duì)象中的數(shù)組中的元素。實(shí)際上就是一個(gè)工具方法。簡(jiǎn)單看一下源碼:

# android.support.multidex.MultiDex
private static void expandFieldArray(Object instance, String fieldName,
        Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException,
        IllegalAccessException {
    Field jlrField = findField(instance, fieldName);
    Object[] original = (Object[]) jlrField.get(instance);
    Object[] combined = (Object[]) Array.newInstance(
            original.getClass().getComponentType(), original.length + extraElements.length);
    System.arraycopy(original, 0, combined, 0, original.length);
    System.arraycopy(extraElements, 0, combined, original.length, extraElements.length);
    jlrField.set(instance, combined);
}

V19的install()方法調(diào)用完畢之后,就把APK文件中的主dex文件之外的dex文件追加到PathClassLoader(也就是BaseClassLoader)中DexPathListde Element[]數(shù)組中。這樣在加載一個(gè)類的時(shí)候就會(huì)遍歷所有的dex文件,保證了打包的類都能夠正常加載。

至于V14和V4中的install()方法,主要的思想都是一致的,在細(xì)節(jié)上有一些不同,有興趣的可以自行查看相關(guān)源碼。

小結(jié)一下:
MultiDex的install()方法實(shí)際上是先抽取出APK文件中的.dex文件,然后利用反射把這個(gè).dex文件生成對(duì)應(yīng)的數(shù)組,最后把這些dex路徑追加到PathClassLoader加載dex的路徑中,從而保證了APK中所有.dex文件中類都能夠被正確的加載。

分析完了,MultiDex加載secondartDex的邏輯,我們?cè)賮砜匆幌聫腁PK文件中抽取出.dex文件的邏輯。
看一下MultiDexExtractor的load()方法:

# android.support.multidex.MultiDexExtractor
static List<File> load(Context context, ApplicationInfo applicationInfo, File dexDir,
        boolean forceReload) throws IOException {
    Log.i(TAG, "MultiDexExtractor.load(" + applicationInfo.sourceDir + ", " + forceReload + ")");
    // sourceDir 路徑為"/data/app/}${packageName}-1/base.apk"
    final File sourceApk = new File(applicationInfo.sourceDir);
    // 獲取APK文件的CRC(循環(huán)冗余校驗(yàn))
    long currentCrc = getZipCrc(sourceApk);

    List<File> files;
    // 如果不需要重新加載并且文件沒有被修改過
    // isModified()方法是根據(jù)SharedPreference中存放的APK文件上一次修改的時(shí)間戳和currentCrc來判斷是否修改過文件
    if (!forceReload && !isModified(context, sourceApk, currentCrc)) {
        try {
            // 從緩存目錄中加載已經(jīng)抽取過的文件
            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);
            // 如果從緩存中加載失敗就需要沖APK文件中去加載,這個(gè)過程時(shí)間會(huì)長(zhǎng)一點(diǎn)
            files = performExtractions(sourceApk, dexDir);
            // 把抽取信息保存到SharedPreferences中,方便下次使用
            putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);

        }
    } else {
        // 如果強(qiáng)制加載或者APK文件已經(jīng)修改過就重新抽取dex文件
        Log.i(TAG, "Detected that extraction must be performed.");
        files = performExtractions(sourceApk, dexDir);
        putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
    }

    Log.i(TAG, "load found " + files.size() + " secondary dex files");
    return files;
}

根據(jù)前后順序的話,App第一次運(yùn)行的時(shí)候需要從APK沖抽取dex文件,我們先來看一下MultiDexExtractor的performExtractions()方法:

# android.support.multidex.MultiDexExtractor
private static List<File> performExtractions(File sourceApk, File dexDir)
        throws IOException {
    // 抽取出的dex文件名前綴是"${apkName}.classes"
    final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;

    // Ensure that whatever deletions happen in prepareDexDir only happen if the zip that
    // contains a secondary dex file in there is not consistent with the latest apk.  Otherwise,
    // multi-process race conditions can cause a crash loop where one process deletes the zip
    // while another had created it.
    // 由于這個(gè)dexDir緩存目錄可能不止一個(gè)APK在使用,在抽取一個(gè)APK之前如果有緩存過的與APK相關(guān)的dex文件就需要先刪除掉,如果dexDir目錄不存在就需要?jiǎng)?chuàng)建
    prepareDexDir(dexDir, extractedFilePrefix);

    List<File> files = new ArrayList<File>();

    final ZipFile apk = new ZipFile(sourceApk);
    try {

        int secondaryNumber = 2;
        // 獲取"classes${secondaryNumber}.dex"格式的文件
        ZipEntry dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
        // 如果dexFile不為null就一直遍歷
        while (dexFile != null) {
            // 抽取后的文件名是"${apkName}.classes${secondaryNumber}.zip"
            String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
            // 創(chuàng)建文件
            File extractedFile = new File(dexDir, fileName);
            // 添加到集合中
            files.add(extractedFile);

            Log.i(TAG, "Extraction is needed for file " + extractedFile);
            // 抽取過程中存在失敗的可能,可以多次嘗試,使用isExtractionSuccessful作為是否成功的標(biāo)志
            int numAttempts = 0;
            boolean isExtractionSuccessful = false;
            while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) {
                numAttempts++;

                // Create a zip file (extractedFile) containing only the secondary dex file
                // (dexFile) from the apk.
                // 抽出去apk中對(duì)應(yīng)序號(hào)的dex文件,存放到extractedFile這個(gè)zip文件中,只包含它一個(gè)
                // extract方法就是一個(gè)IO操作
                extract(apk, dexFile, extractedFile, extractedFilePrefix);

                // Verify that the extracted file is indeed a zip file.   
                // 判斷是夠抽取成功
                isExtractionSuccessful = verifyZipFile(extractedFile);

                // Log the sha1 of the extracted zip file
                Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "success" : "failed") +
                        " - length " + extractedFile.getAbsolutePath() + ": " +
                        extractedFile.length());
                if (!isExtractionSuccessful) {
                    // Delete the extracted file
                    extractedFile.delete();
                    if (extractedFile.exists()) {
                        Log.w(TAG, "Failed to delete corrupted secondary dex '" +
                                extractedFile.getPath() + "'");
                    }
                }
            }
            if (!isExtractionSuccessful) {
                throw new IOException("Could not create zip file " +
                        extractedFile.getAbsolutePath() + " for secondary dex (" +
                        secondaryNumber + ")");
            }
            // 繼續(xù)下一個(gè)dex的抽取
            secondaryNumber++;
            dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
        }
    } finally {
        try {
            apk.close();
        } catch (IOException e) {
            Log.w(TAG, "Failed to close resource", e);
        }
    }

    return files;
}

當(dāng)MultiDexExtractor的performExtractions()方法調(diào)用完畢的時(shí)候就把APK中所有的dex文件抽取出來,并以一定文件名格式的zip文件保存在緩存目錄中。然后再把一些關(guān)鍵的信息通過調(diào)用putStoredApkInfo(Context context, long timeStamp, long crc, int totalDexNumber)方法保存到SP中。

當(dāng)APK之后再啟動(dòng)的時(shí)候就會(huì)從緩存目錄中去加載已經(jīng)抽取過的dex文件。我們接著來看一下MultiDexExtractor的loadExistingExtractions()方法:

# android.support.multidex.MultiDexExtractor
private static List<File> loadExistingExtractions(Context context, File sourceApk, File dexDir)
        throws IOException {
    Log.i(TAG, "loading existing secondary dex files");
    // 抽取出的dex文件名前綴是"${apkName}.classes"
    final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
    // 從SharedPreferences中獲取.dex文件的總數(shù)量,調(diào)用這個(gè)方法的前提是已經(jīng)抽取過dex文件,所以SP中是有值的
    int totalDexNumber = getMultiDexPreferences(context).getInt(KEY_DEX_NUMBER, 1);
    final List<File> files = new ArrayList<File>(totalDexNumber);

    // 從第2個(gè)dex開始遍歷,這是因?yàn)橹鱠ex由Android系統(tǒng)自動(dòng)加載的,從第2個(gè)開始即可
    for (int secondaryNumber = 2; secondaryNumber <= totalDexNumber; secondaryNumber++) {
        // 文件名,格式是"${apkName}.classes${secondaryNumber}.zip"
        String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
        // 根據(jù)緩存目錄和文件名得到抽取后的文件
        File extractedFile = new File(dexDir, fileName);
        // 如果是一個(gè)文件就保存到抽取出的文件列表中
        if (extractedFile.isFile()) {
            files.add(extractedFile);
            if (!verifyZipFile(extractedFile)) {
                Log.i(TAG, "Invalid zip file: " + extractedFile);
                throw new IOException("Invalid ZIP file.");
            }
        } else {
            throw new IOException("Missing extracted secondary dex file '" +
                    extractedFile.getPath() + "'");
        }
    }

    return files;
}

4 總結(jié)

分析到這,MultiDex安裝多個(gè)dex的原理應(yīng)該介紹清楚了,無非就是通過一定的方式把dex文件抽取出來,然后把這些dex文件追加到DexPathList的Element[]數(shù)組的后面,這個(gè)過程要盡可能的早,所以一般是在Application的attachBaseContext()方法中。
一些熱修復(fù)技術(shù),就是通過一定的方式把修復(fù)后的dex插入到DexPathList的Element[]數(shù)組前面,實(shí)現(xiàn)了修復(fù)后的class搶先加載。

參考

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容