美文网首页Android
类加载机制系列3——MultiDex原理解析

类加载机制系列3——MultiDex原理解析

作者: sososeen09 | 来源:发表于2017-12-20 11:06 被阅读172次

    1 MultiDex的由来

    Android中由于一个dex文件最多存储65536个方法,也就是一个short类型的范围,所以随着应用的类不断增加,当一个dex文件突破这个方法数的时候就会报出异常。虽然可以通过混淆等方式来减少无用的方法,但是随着APP功能的增多,突破方法数限制还是不可避免的。因此在Android5.0时,Android推出了官方的解决方案:MultiDex。打包的时候,把一个应用分成多个dex,例如:classes.dex、classes2.dex、classes3.dex...,加载的时候把这些dex都追加到DexPathList对应的数组中,这样就解决了方法数的限制。

    5.0后的系统都内置了加载多个dex文件的功能,而在5.0之前,系统只可以加载一个主dex,其它的dex就需要采用一定的手段来加载。这也就是我们今天要讲的MultiDex。

    MultiDex存放在android.support.multidex包下

    2 MultiDex的使用

    Gradle构建环境下,在主应用的build.gradle文件夹添加如下配置:

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

    现在最新的multidex版本是1.0.2。

    在AndroidManifest.xml中的app节点下,使用MultiDexApplication作为应用入口。

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

    当然了,大部分情况下,我们都会自定义一个自己的Application对应用做一些初始化。这种情况下,可以在我们自定义的Application中的attachBaseContext()方法中调用MultiDex.install()方法。

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

    需要注意的是:MultiDex.install()方法的调用时机要尽可能的早,防止加载后面的dex文件中的类时报ClassNotFoundException。

    3 MultiDex源码分析

    分析MultiDex的的入口就是它的静态方法install()。
    这个方法的作用就是把从应用的APK文件中的dex添加到应用的类加载器PathClassLoader中的DexPathList的Emlement数组中。

    public static void install(Context context) {
        Log.i(TAG, "install");
        //判断Android系统是否已经支持了MultiDex,如果支持了就不需要再去安装了,直接返回
        if (IS_VM_MULTIDEX_CAPABLE) {
            Log.i(TAG, "VM has multidex support, MultiDex support library is disabled.");
            return;
        }
    
        // 如果Android系统低于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 {
            // 获取应用信息
            ApplicationInfo applicationInfo = getApplicationInfo(context);
            // 如果应用信息为空就返回,比如说运行在一个测试的Context下。
            if (applicationInfo == null) {
                // Looks like running on a test Context, so just return without patching.
                return;
            }
            // 同步方法
            synchronized (installedApk) {
                // 获取已经安装的APK的全路径
                String apkPath = applicationInfo.sourceDir;
                if (installedApk.contains(apkPath)) {
                    return;
                }
                // 把路径添加到已经安装的APK路径中
                installedApk.add(apkPath);
                // 如果编译版本大于最大支持版本,报一个警告
                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,实际上是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;
                }
                // 在某些测试环境下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);
                }
                // 新建一个存放dex的目录,路径是"/data/user/0/${packageName}/code_cache/secondary-dexes",用来存放优化后的dex文件
                File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME);
                // 使用MultiDexExtractor这个工具类把APK中的dex抽取到dexDir目录中,返回的files集合有可能为空,表示没有secondaryDex
                // 不强制重新加载,也就是说如果已经抽取过了,可以直接从缓存目录中拿来使用,这么做速度比较快
                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.
                    // 如果抽取出的文件是无效的,那么就强制重新加载,这么做的话速度就慢了一点,有一些IO开销
                    files = MultiDexExtractor.load(context, applicationInfo, dexDir, true);
                    if (checkValidZipFiles(files)) {
                        // 强制加载后,如果文件有效就安装,否则就抛出异常
                        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");
    }
    

    关于dex文件抽取逻辑和校验逻辑我们先不管,我们看一下MultiDex是如何安装secondaryDex文件的。
    由于不同版本的Android系统,类加载机制有一些不同,所以分为了V19、V14和V4等三种情况下的安装。V19、V14和V4都是MultiDex的private的静态内部类。V19支持Andorid19版本(20是只支持可穿戴设备的),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字段
            // 实际上就是找到BaseClassLoader中的DexPathList
            Field pathListField = findField(loader, "pathList");
            // 获取PathClassLoader绑定的DexPathList对象
            Object dexPathList = pathListField.get(loader);
            ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
            // 扩展DexPathList对象的Element数组,数组名是dexElements
            // makeDexElements()方法的作用就是调用DexPathList的makeDexElements()方法来创建dex元素
            expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
                    new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
                    suppressedExceptions));
            // 后面就是添加一些IO异常信息,因为调用DexPathList的makeDexElements会有一些IO操作,相应的可能就会有一些异常情况
            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}.
         */
        // 通过反射的方式调用DexPathList#makeDexElements()方法
        // dexPathList 就是一个DexPathList对象
        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);
            // 调用makeDexElements()方法,根据外界传递的包含dex文件的源文件和优化后的缓存目录返回一个Element[]数组
            return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory,
                    suppressedExceptions);
        }
    }
    

    MultiDex的expandFieldArray()方法作用是扩展一个对象中的数组中的元素。实际上就是一个工具方法。简单看一下源码:

    # 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()方法调用完毕之后,就把APK文件中的主dex文件之外的dex文件追加到PathClassLoader(也就是BaseClassLoader)中DexPathListde Element[]数组中。这样在加载一个类的时候就会遍历所有的dex文件,保证了打包的类都能够正常加载。

    至于V14和V4中的install()方法,主要的思想都是一致的,在细节上有一些不同,有兴趣的可以自行查看相关源码。

    小结一下:
    MultiDex的install()方法实际上是先抽取出APK文件中的.dex文件,然后利用反射把这个.dex文件生成对应的数组,最后把这些dex路径追加到PathClassLoader加载dex的路径中,从而保证了APK中所有.dex文件中类都能够被正确的加载。

    分析完了,MultiDex加载secondartDex的逻辑,我们再来看一下从APK文件中抽取出.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(循环冗余校验)
        long currentCrc = getZipCrc(sourceApk);
    
        List<File> files;
        // 如果不需要重新加载并且文件没有被修改过
        // isModified()方法是根据SharedPreference中存放的APK文件上一次修改的时间戳和currentCrc来判断是否修改过文件
        if (!forceReload && !isModified(context, sourceApk, currentCrc)) {
            try {
                // 从缓存目录中加载已经抽取过的文件
                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文件中去加载,这个过程时间会长一点
                files = performExtractions(sourceApk, dexDir);
                // 把抽取信息保存到SharedPreferences中,方便下次使用
                putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
    
            }
        } else {
            // 如果强制加载或者APK文件已经修改过就重新抽取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;
    }
    
    

    根据前后顺序的话,App第一次运行的时候需要从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.
        // 由于这个dexDir缓存目录可能不止一个APK在使用,在抽取一个APK之前如果有缓存过的与APK相关的dex文件就需要先删除掉,如果dexDir目录不存在就需要创建
        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;
                // 创建文件
                File extractedFile = new File(dexDir, fileName);
                // 添加到集合中
                files.add(extractedFile);
    
                Log.i(TAG, "Extraction is needed for file " + extractedFile);
                // 抽取过程中存在失败的可能,可以多次尝试,使用isExtractionSuccessful作为是否成功的标志
                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中对应序号的dex文件,存放到extractedFile这个zip文件中,只包含它一个
                    // extract方法就是一个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 + ")");
                }
                // 继续下一个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;
    }
    

    当MultiDexExtractor的performExtractions()方法调用完毕的时候就把APK中所有的dex文件抽取出来,并以一定文件名格式的zip文件保存在缓存目录中。然后再把一些关键的信息通过调用putStoredApkInfo(Context context, long timeStamp, long crc, int totalDexNumber)方法保存到SP中。

    当APK之后再启动的时候就会从缓存目录中去加载已经抽取过的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文件的总数量,调用这个方法的前提是已经抽取过dex文件,所以SP中是有值的
        int totalDexNumber = getMultiDexPreferences(context).getInt(KEY_DEX_NUMBER, 1);
        final List<File> files = new ArrayList<File>(totalDexNumber);
    
        // 从第2个dex开始遍历,这是因为主dex由Android系统自动加载的,从第2个开始即可
        for (int secondaryNumber = 2; secondaryNumber <= totalDexNumber; secondaryNumber++) {
            // 文件名,格式是"${apkName}.classes${secondaryNumber}.zip"
            String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
            // 根据缓存目录和文件名得到抽取后的文件
            File extractedFile = new File(dexDir, fileName);
            // 如果是一个文件就保存到抽取出的文件列表中
            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 总结

    分析到这,MultiDex安装多个dex的原理应该介绍清楚了,无非就是通过一定的方式把dex文件抽取出来,然后把这些dex文件追加到DexPathList的Element[]数组的后面,这个过程要尽可能的早,所以一般是在Application的attachBaseContext()方法中。
    一些热修复技术,就是通过一定的方式把修复后的dex插入到DexPathList的Element[]数组前面,实现了修复后的class抢先加载。

    参考

    相关文章

      网友评论

        本文标题:类加载机制系列3——MultiDex原理解析

        本文链接:https://www.haomeiwen.com/subject/aytgwxtx.html