【分析】多dex加载机制

相关文章链接:

构造多dex的apk

---------------------------------------------------------------------------------

Android源码版本:5.0.2_r1


下面是多dex加载的时序图: 



Android项目有两种方式支持多dex:

1. 项目中的Application类继承MultiDexApplication
2. 在自己的Application类的attachBaseContext方法中调用MultiDex.install(this);


我从MultiDexApplication这个类开始分析。


MultiDexApplication类继承了Application,并重载了attachBaseContext方法,在这个方法中调用了MultiDex.install(this);

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

MultiDex.install方法:

/**
 * Patches the application context class loader by appending extra dex files
 * loaded from the application apk. This method should be called in the
 * attachBaseContext of your {@link Application}, see
 * {@link MultiDexApplication} for more explanation and an example.
 *
 * @param context application context.
 * @throws RuntimeException if an error occurred preventing the classloader
 *         extension.
 */
public static void install(Context context) {
    Log.i(TAG, "install");

    ......

    try {
        ApplicationInfo applicationInfo = getApplicationInfo(context);
        if (applicationInfo == null) {
            // Looks like running on a test Context, so just return without patching.
            return;
        }

        synchronized (installedApk) {
            String apkPath = applicationInfo.sourceDir;
            // installedApk的类型是:Set<String>。
            // 如果这个apk已经安装,则不重复安装。
            if (installedApk.contains(apkPath)) {
                return;
            }
            installedApk.add(apkPath);

            ......

            // 类加载器应该直接或间接继承于BaseDexClassLoader。
            // 修改BaseDexClassLoader类中的DexPathList pathList字段,追加额外的DEX文件项。
            /* 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;

            ......

            // dex将会输出到SECONDARY_FOLDER_NAME目录。
            File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME);
            List<File> files = MultiDexExtractor.load(context, applicationInfo, dexDir, false);
            // 校验这些zip文件是否合法。
            if (checkValidZipFiles(files)) {
                // 安装提取出来的zip文件。
                installSecondaryDexes(loader, dexDir, files);
            } else {
                Log.w(TAG, "Files were not valid zip files.  Forcing a reload.");
                // 最后一个参数是true,代表强制加载。
                // Try again, but this time force a reload of the zip file.
                files = MultiDexExtractor.load(context, applicationInfo, dexDir, true);

                // 校验这些zip文件是否合法。
                if (checkValidZipFiles(files)) {
                    // 安装提取出来的zip文件。
                    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");
}

MultiDexExtractor.load获得/导出apk中多出的dex,这些dex导出后会被打包成zip文件:

/**
 * 提取/获得apk中多dex的提取zip文件。
 * 如果不是加载已经存在的文件的情况,则还要保存apk的信息:时间戳、crc值、apk中dex的总个数。
 * 
 * Extracts application secondary dexes into files in the application data
 * directory.
 *
 * @return a list of files that were created. The list may be empty if there
 *         are no secondary dex files.
 * @throws IOException if encounters a problem while reading or writing
 *         secondary dex files
 */
static List<File> load(Context context, ApplicationInfo applicationInfo, File dexDir,
        boolean forceReload) throws IOException {
    Log.i(TAG, "MultiDexExtractor.load(" + applicationInfo.sourceDir + ", " + forceReload + ")");
    final File sourceApk = new File(applicationInfo.sourceDir);

    long currentCrc = getZipCrc(sourceApk);

    List<File> files;
    // isModified方法判断apk是否被修改过。
    if (!forceReload && !isModified(context, sourceApk, currentCrc)) {
        try {
            // 加载已经存在的文件,如果有的文件不存在,或者不是zip文件,则会抛出异常。
            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中提取出多dex,然后将这些dex逐个打包为zip文件,最终返回提取出来的zip文件列表。
            files = performExtractions(sourceApk, dexDir);
            // getTimeStamp方法中调用的是sourceApk.lastModified()方法。
            // putStoredApkInfo方法存储apk的信息:时间戳、crc值、apk中dex的总个数。
            putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);

        }
    } else {
        Log.i(TAG, "Detected that extraction must be performed.");
        // 这里的performExtractions和putStoredApkInfo同上。
        files = performExtractions(sourceApk, dexDir);
        putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
    }

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

MultiDexExtractor.performExtractions方法:

/**
 * 从apk中提取出多dex,然后将这些dex逐个打包为zip文件。
 * @param sourceApk apk文件。
 * @param dexDir 输出目录。
 * @return 提取出来的zip文件列表。
 */
private static List<File> performExtractions(File sourceApk, File dexDir)
        throws IOException {

    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.
    prepareDexDir(dexDir, extractedFilePrefix);

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

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

        int secondaryNumber = 2;

        ZipEntry dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
        while (dexFile != null) {
            // 输出的文件名。
            String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
            // 输出的文件。
            File extractedFile = new File(dexDir, fileName);
            files.add(extractedFile);

            Log.i(TAG, "Extraction is needed for file " + extractedFile);
            int numAttempts = 0;
            boolean isExtractionSuccessful = false;
            while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) {
                numAttempts++;

                // 提取apk中的多dex文件,然后打包成一个zip文件。
                // Create a zip file (extractedFile) containing only the secondary dex file
                // (dexFile) from the apk.
                extract(apk, dexFile, extractedFile, extractedFilePrefix);

                // 验证提取的文件是否是一个zip文件。
                // 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 + ")");
            }
            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.putStoredApkInfo方法:

private static void putStoredApkInfo(Context context, long timeStamp, long crc,
        int totalDexNumber) {
    SharedPreferences prefs = getMultiDexPreferences(context);
    SharedPreferences.Editor edit = prefs.edit();
    edit.putLong(KEY_TIME_STAMP, timeStamp);    // 时间戳
    edit.putLong(KEY_CRC, crc); // crc值。
    /* SharedPreferences.Editor doc says that apply() and commit() "atomically performs the
     * requested modifications" it should be OK to rely on saving the dex files number (getting
     * old number value would go along with old crc and time stamp).
     */
    edit.putInt(KEY_DEX_NUMBER, totalDexNumber);    // dex总个数。
    apply(edit);
}

MultiDex.installSecondaryDexes方法,对dex进行安装:

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.install分析:

/**
 * 安装多dex。
 * @param loader 
 * @param additionalClassPathEntries zip文件列表,这些zip文件中都只有一个文件classes.dex。
 * @param optimizedDirectory 优化的dex存放的目录。
 */
private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
        File optimizedDirectory)
                throws IllegalArgumentException, IllegalAccessException,
                NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
    // 被打补丁的类加载器应该直接或间接继承BaseDexClassLoader。
    // 我们修改DexPathList pathList字段,追加额外的DEX文件项。
    /* 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.
     */
    // dexPathList = load.pathList;
    Field pathListField = findField(loader, "pathList");
    Object dexPathList = pathListField.get(loader);

    ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
    // makeDexElements方法调用了DexPathList中的makeDexElements方法,这个方法可以加载并优化dex、zip、jar。
    // expandFieldArray方法将makeDexElements返回的数组patch到dexPathList.dexElements中。
    expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
            new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
            suppressedExceptions));

    ......
}

V19.makeDexElements方法:

/**
 * A wrapper around
 * {@code private static final dalvik.system.DexPathList#makeDexElements}.
 */
private static Object[] makeDexElements(
        Object dexPathList, ArrayList<File> files, File optimizedDirectory,
        ArrayList<IOException> suppressedExceptions)
                throws IllegalAccessException, InvocationTargetException,
                NoSuchMethodException {
    Method makeDexElements =
            findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class,
                    ArrayList.class);

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

MultiDex.expandFieldArray方法:

/**
 * 原字段内容加上扩展的数组元素替换相应字段的内容,这个字段是一个数组。
 * 
 * Replace the value of a field containing a non null array, by a new array containing the
 * elements of the original array plus the elements of extraElements.
 * @param instance the instance whose field is to be modified.
 * @param fieldName the field to modify.
 * @param extraElements elements to append at the end of the array.
 */
private static void expandFieldArray(Object instance, String fieldName,
        Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException,
        IllegalAccessException {
    // combined = new <Type>[original.length + extraElements.length];
    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);
}


  • 4
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

i不歪

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值