MultiDex原理

简述

MultiDex适用于API版本在4-20的Android系统 , 即Android 2.1 - 4.4 . 而在这些版本之间 , MultiDex会通过Application.getClassLoader进行加载. 而如果Dex比较多比较大的话 , 主线程加载Dex时间会很长 , 导致主线程ANR.

由于Android 5.0之后使用ART虚拟机进行dex2oat , 将多dex在安装的时候将APK中多个Dex进行优化 , 优化过后生成一个ELF文件 , 名为.oat文件. 在加载后 , 会将oat文件直接映射到ART虚拟机中使用 , 这样就减少Dex加载的耗时.

MultiDex加载过程简述

在加载过程中 :

  • 读取APK的CRC32以及modifyTime进行校验
  • 通过反射 , 从BaseDexClassLoader中找到pathList对象
  • 通过反射调用PathList.makeDexElements创建Elements[]
  • 通过反射将Elements[]添加到dexElements数组中
  • 后续在该ClassLoader查找类到时候 , 会优先在dexElements中开始遍历查找

MultiDex加载过程

  1. 调用install函数进行加载
  • 获取ApplicationInfo , 以及data、source目录路径
  • 调用doInstallation加载Dex
public static void install(Context context) {
        ...
        // 如果当前版本小于4 , 不支持
        if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) {
            throw new RuntimeException("MultiDex installation failed. SDK " + Build.VERSION.SDK_INT
                    + " is unsupported. Min SDK version is " + MIN_SDK_VERSION + ".");
        }

        try {
            // 获取Application的ApplicationInfo
            ApplicationInfo applicationInfo = getApplicationInfo(context);
            if (applicationInfo == null) {
              Log.i(TAG, "No ApplicationInfo available, i.e. running on a test Context:"
                  + " MultiDex support library is disabled.");
              return;
            }
            // 传入/data/data路径以及代码路径
            doInstallation(context,
                    new File(applicationInfo.sourceDir),
                    new File(applicationInfo.dataDir),
                    CODE_CACHE_SECONDARY_FOLDER_NAME,
                    NO_KEY_PREFIX,
                    true);

        } catch (Exception e) {
            Log.e(TAG, "MultiDex installation failure", e);
            throw new RuntimeException("MultiDex installation failed (" + e.getMessage() + ").");
        }
        Log.i(TAG, "install done");
    }
  1. doInstallation
  • 检查版本
  • 从Application中获取DexClassLoader
  • 清理secondary-dexes文件夹
  • 创建MultiDexExtractor用于读取APK中的文件
  • 调用installSecondaryDexes开始安装classes2.dex后的Dex文件
 private static void doInstallation(Context mainContext, File sourceApk, File dataDir,
            String secondaryFolderName, String prefsKeyPrefix,
            boolean reinstallOnPatchRecoverableException)  {
        synchronized (installedApk) {
            // 如果已经加载过的APK就直接返回
            if (installedApk.contains(sourceApk)) {
                return;
            }
            installedApk.add(sourceApk);
            // 如果版本高于20 , 也就5.0以上的版本就会提示
            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") + "\"");
            }

            //  从Application中拿到ClassLoader , 并且进行类型校验
            // 判断是否为BaseDexClassLoader还是PathClassLoader
            ClassLoader loader = getDexClassloader(mainContext);
            if (loader == null) {
                return;
            }
            // 清理secondary-dexes目录
            try {
              clearOldDexDir(mainContext);
            } catch (Throwable t) {
              Log.w(TAG, "Something went wrong when trying to clear old MultiDex extraction, "
                  + "continuing without cleaning.", t);
            }
            // 得到/data/data/pkg/secondary-dexes目录
            File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName);
            // 创建MultiDexExtractor , 即MultiDex提取器
            MultiDexExtractor extractor = new MultiDexExtractor(sourceApk, dexDir);
            IOException closeException = null;
            try {
                // 通过extractor加载Dex
                List<? extends File> files =
                        extractor.load(mainContext, prefsKeyPrefix, false);
                try {
                    // 安装Dex文件
                    installSecondaryDexes(loader, dexDir, files);
                // Some IOException causes may be fixed by a clean extraction.
                } catch (IOException e) {
                    if (!reinstallOnPatchRecoverableException) {
                        throw e;
                    }
                    Log.w(TAG, "Failed to install extracted secondary dex files, retrying with "
                            + "forced extraction", e);
                    files = extractor.load(mainContext, prefsKeyPrefix, true);
                    installSecondaryDexes(loader, dexDir, files);
                }
            } finally {
                try {
                    extractor.close();
                } catch (IOException e) {
                    // Delay throw of close exception to ensure we don't override some exception
                    // thrown during the try block.
                    closeException = e;
                }
            }
            if (closeException != null) {
                throw closeException;
            }
        }
    }
  1. MultiDexExtractor
  • 获取multidex.lock文件 , 用于文件锁
  • 锁住文件
MultiDexExtractor(File sourceApk, File dexDir) throws IOException {
        Log.i(TAG, "MultiDexExtractor(" + sourceApk.getPath() + ", " + dexDir.getPath() + ")");
        this.sourceApk = sourceApk;
        this.dexDir = dexDir;
        // 获取APK的CRC
        sourceCrc = getZipCrc(sourceApk);
        File lockFile = new File(dexDir, LOCK_FILENAME);
        // 拿到Lock文件的文件锁
        lockRaf = new RandomAccessFile(lockFile, "rw");
        try {
            lockChannel = lockRaf.getChannel();
            try {
                Log.i(TAG, "Blocking on lock " + lockFile.getPath());
                // 锁住File Channel
                cacheLock = lockChannel.lock();
            } catch (IOException | RuntimeException | Error e) {
                closeQuietly(lockChannel);
                throw e;
            }
            Log.i(TAG, lockFile.getPath() + " locked");
        } catch (IOException | RuntimeException | Error e) {
            closeQuietly(lockRaf);
            throw e;
        }
    }
  1. load
  • 检查文件锁是否失效
  • 检查APK的CRC、ModifyTime是否与之前的APK CRC、ModifyTime一样
  • 如果不一样 , 则调用performExtractions来清理之前的无效文件以及读取Dex
  • 将最新的Dex的CRC与ModifyTime保存到SP中
List<? extends File> load(Context context, String prefsKeyPrefix, boolean forceReload)
            throws IOException {
        Log.i(TAG, "MultiDexExtractor.load(" + sourceApk.getPath() + ", " + forceReload + ", " +
                prefsKeyPrefix + ")");
        // 检查文件锁是否失效 
        if (!cacheLock.isValid()) {
            throw new IllegalStateException("MultiDexExtractor was closed");
        }

        List<ExtractedDex> files;
        // forceReload为false , 判断文件的modify时间以及CRC是否相同
        if (!forceReload && !isModified(context, sourceApk, sourceCrc, prefsKeyPrefix)) {
            // 本次文件的CRC与modify time与之前的Dex文件相同的话 , 就会来加载
            try {
                files = loadExistingExtractions(context, prefsKeyPrefix);
            } catch (IOException ioe) {
                Log.w(TAG, "Failed to reload existing extracted secondary dex files,"
                        + " falling back to fresh extraction", ioe);
                files = performExtractions();
                putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), sourceCrc,
                        files);
            }
        } else {
            if (forceReload) {
                Log.i(TAG, "Forced extraction must be performed.");
            } else {
                Log.i(TAG, "Detected that extraction must be performed.");
            }
            // 如果CRC与之前SP中保存不一致的话 , 就会通过这个方法查找dex文件个数
            files = performExtractions();
            putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), sourceCrc,
                    files);
        }

        Log.i(TAG, "load found " + files.size() + " secondary dex files");
        return files;
    }
  1. performExtractions
  • 清理secondary-classes文件夹
  • classes2.dex开始从APK中读取Dex文件
  • 将DexFile写入到本地临时文件中
  • 计算文件CRC
  • 将所有Dex的CRC保存到List中返回
private List<ExtractedDex> performExtractions() throws IOException {
        // 得到xxx.apk.classes
        final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
        // 清理secondary-classes文件夹
        clearDexDir();
        List<ExtractedDex> files = new ArrayList<ExtractedDex>();
        // 创建ZipFile
        final ZipFile apk = new ZipFile(sourceApk);
        try {
            int secondaryNumber = 2;
            // 得到classes2.dex文件, 因为classes.dex已经加载
            ZipEntry dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
            while (dexFile != null) {
                // 找到dex的文件名
                String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
                // 创建DexFile路径文件
                ExtractedDex extractedFile = new ExtractedDex(dexDir, fileName);
                files.add(extractedFile);
                int numAttempts = 0;
                boolean isExtractionSuccessful = false;
                // 每个Dex尝试最多尝试读取3次
                while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) {
                    numAttempts++;
                    // 创建Dex临时文件 , 也就是把Zip里的文件读取后 , 写入一个临时文件
                    extract(apk, dexFile, extractedFile, extractedFilePrefix);
                    // 计算临时文件的CRC32
                    try {
                        extractedFile.crc = getZipCrc(extractedFile);
                        // 计算成功
                        isExtractionSuccessful = true;
                    } catch (IOException e) {
                        isExtractionSuccessful = false;
                        Log.w(TAG, "Failed to read crc from " + extractedFile.getAbsolutePath(), e);
                    }
                    if (!isExtractionSuccessful) {
                        // 如果提取失败 , 删除临时文件
                        extractedFile.delete();
                        if (extractedFile.exists()) {
                            Log.w(TAG, "Failed to delete corrupted secondary dex '" +
                                    extractedFile.getPath() + "'");
                        }
                    }
                }
                // 如果提取3次失败 , 抛异常
                if (!isExtractionSuccessful) {
                    throw new IOException("Could not create zip file " +
                            extractedFile.getAbsolutePath() + " for secondary dex (" +
                            secondaryNumber + ")");
                }
                // 开始读取下一个classes3.dex文件
                secondaryNumber++;
                // 获取下一个dexFile
                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;
    }
  1. putStoredApkInfo
  • 将CRC更新到SharedPreferences中
  • 使用commit写入
private static void putStoredApkInfo(Context context, String keyPrefix, long timeStamp,
            long crc, List<ExtractedDex> extractedDexes) {
        
        SharedPreferences prefs = getMultiDexPreferences(context);
        SharedPreferences.Editor edit = prefs.edit();
        edit.putLong(keyPrefix + KEY_TIME_STAMP, timeStamp);
        // 将APK文件的CRC保存到SP中
        edit.putLong(keyPrefix + KEY_CRC, crc);
        edit.putInt(keyPrefix + KEY_DEX_NUMBER, extractedDexes.size() + 1);

        int extractedDexId = 2;
        // 保存从classes2.dex后的CRC以及时间
        for (ExtractedDex dex : extractedDexes) {
            edit.putLong(keyPrefix + KEY_DEX_CRC + extractedDexId, dex.crc);
            edit.putLong(keyPrefix + KEY_DEX_TIME + extractedDexId, dex.lastModified());
            extractedDexId++;
        }
        // 需要一个同步写入的方法
        edit.commit();
    }

7.installSecondaryDexes开始加载Dex

  • 根据当前Android版本进行安装
 private static void installSecondaryDexes(ClassLoader loader, File dexDir,
        List<? extends File> files)
            throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
            InvocationTargetException, NoSuchMethodException, IOException, SecurityException,
            ClassNotFoundException, InstantiationException {
        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);
            } else {
                V4.install(loader, files);
            }
        }
    }
  1. install
  • V19版本中, 通过pathList对象到makeDexList创建Elements
  • 合并到主Dex中的Elements中
static void install(ClassLoader loader,
                List<? extends File> additionalClassPathEntries,
                File optimizedDirectory)
                        throws IllegalArgumentException, IllegalAccessException,
                        NoSuchFieldException, InvocationTargetException, NoSuchMethodException,
                        IOException {
            // 从PathClassLaoder中读取pathList属性
            Field pathListField = findField(loader, "pathList");
            // 获取pathList对象
            Object dexPathList = pathListField.get(loader);
            ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
            // 调用makeDexElements , 并且添加到dexPathList对象中
            expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
                    new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
                    suppressedExceptions));
            ...
        }
        private static Object[] makeDexElements(
                Object dexPathList, ArrayList<File> files, File optimizedDirectory,
                ArrayList<IOException> suppressedExceptions)
                        throws IllegalAccessException, InvocationTargetException,
                        NoSuchMethodException {
            // 调用pathList.makeDexElements生成Elements对象
            Method makeDexElements =
                    findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class,
                            ArrayList.class);

            return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory,
                    suppressedExceptions);
        }

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Thread.join解析

    join方法的注释上写着:阻塞当前线程,直到收到结束执行或者死亡。当接收者的interrupt方法被调用,并且它被调用了join方法的时候,就会抛出Interu...

    None_Ling
  • AAC---DataBinding使用

    DataBinding类似于ButterKnife,可以将数据结构与XML绑定,节省了很多Activity中UI的代码,比如说findViewById等。也可以...

    None_Ling
  • ViewPager2与Fragment

    在AndroidX之前的Fragment , 由于配合ViewPager使用 , 在Fragment添加到ViewPager上后 , 生命周期会跟Activi...

    None_Ling
  • 1074 食物链 2001年NOI全国竞赛

    1074 食物链 2001年NOI全国竞赛 时间限制: 3 s 空间限制: 64000 KB 题目等级 : 钻石 Diamond 题目描述 Des...

    attack
  • R语言中文分词工具

    使用默认方法安装相关R中文文本挖掘包(tmcn、Rwordseg、Rweibo)时,会出现安装失败。合适的方法是:通过源代码安装相关包的程序:手工下载源代码及其...

    学到老
  • 关于R安装中文分词包安装不上的问题install.packages(\"tm\")

    学到老
  • R语言中文分词工具

    Rwordseg、Rweibo、tm的安装 使用默认方法安装相关R中文文本挖掘包(tmcn、Rwordseg、Rweibo)时,会出现安装失败。合适的方法是:通...

    学到老
  • 关于R安装中文分词包安装不上的问题install.packages(\"tm\")

    使用默认方法安装相关R中文文本挖掘包(tmcn、Rwordseg、Rweibo)时,会出现安装失败。合适的方法是:通过源代码安装相关包的程序:手工下载源代码及...

    学到老
  • 「大数据系列」Ignite:基于内存分布式数据库和缓存和处理平台

    首席架构师智库
  • Log:被BigData遗忘的奠基者

    Log是关系数据库对计算机行业的伟大贡献。在大数据时代,Log更是基础技术之一。然而在大家热烈讨论GFS, NoSQL,乃至Paxos, LSM tree等词语...

    用户1564362

扫码关注云+社区

领取腾讯云代金券