前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >MultiDex原理

MultiDex原理

作者头像
None_Ling
发布2020-09-17 17:28:14
6120
发布2020-09-17 17:28:14
举报
文章被收录于专栏:Android相关Android相关

简述

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
代码语言:javascript
复制
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文件
代码语言:javascript
复制
 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文件 , 用于文件锁
  • 锁住文件
代码语言:javascript
复制
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中
代码语言:javascript
复制
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中返回
代码语言:javascript
复制
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写入
代码语言:javascript
复制
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版本进行安装
代码语言:javascript
复制
 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中
代码语言:javascript
复制
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);
        }
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 简述
  • MultiDex加载过程简述
  • MultiDex加载过程
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档