MultiDex.install()源码分析

Android 应用 (APK) 文件包含 Dalvik Executable (DEX) 文件形式的可执行字节码文件,这些文件包含用来运行应用已编译代码。Dalvik Executable 规范将可在单个 DEX 文件内引用的方法总数限制为 65,536,其中包括 Android 框架方法、库方法以及自己的代码中的方法,即“64K 引用限制”。

为什么引用MultiDex

由于Android 5.0(API 级别 21)之前的平台版本使用 Dalvik 运行时来执行应用代码。默认情况下,Dalvik 将应用限制为每个 APK 只能使用一个 classes.dex 字节码文件。不可避免地,当方法数超过64K限制时,编译就会报错,因此google通过多 dex 文件支持库来解决这个问题,即MultiDex。而Android 5.0(API 级别 21)及更高版本使用名为 ART 的运行时,它本身支持从 APK 文件加载多个 DEX 文件。ART 在应用安装时执行预编译,扫描 classesN.dex 文件,并将它们编译成单个 .oat 文件,以供 Android 设备执行,因此MultiDex仅针对5.0以下的Android系统。

MultiDex分析

根据MultiDex的用法MultiDex.install()作为切入点开始分析,下面直接看install()方法

public static void install(Context context) {
        Log.i("MultiDex", "Installing application");
        if (IS_VM_MULTIDEX_CAPABLE) { //5.0以上,忽略
            Log.i("MultiDex", "VM has multidex support, MultiDex support library is disabled.");
        } else if (VERSION.SDK_INT < 4) {
            throw new RuntimeException("MultiDex installation failed. SDK " + VERSION.SDK_INT + " is unsupported. Min SDK version is " + 4 + ".");
        } else {
            try {
                ApplicationInfo applicationInfo = getApplicationInfo(context);
                if (applicationInfo == null) {
                    Log.i("MultiDex", "No ApplicationInfo available, i.e. running on a test Context: MultiDex support library is disabled.");
                    return;
                }
				//执行dex安装
                doInstallation(context, new File(applicationInfo.sourceDir), new File(applicationInfo.dataDir), "secondary-dexes", "", true);
            } catch (Exception var2) {
                Log.e("MultiDex", "MultiDex installation failure", var2);
                throw new RuntimeException("MultiDex installation failed (" + var2.getMessage() + ").");
            }

            Log.i("MultiDex", "install done");
        }
    }

install里主要做了一些判断,判断VM是否支持多dex,如果已经支持,则忽略,否则执行doInstallation()

private static void doInstallation(Context mainContext, File sourceApk, File dataDir, String secondaryFolderName, String prefsKeyPrefix, boolean reinstallOnPatchRecoverableException) throws IOException, IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, SecurityException, ClassNotFoundException, InstantiationException {
        synchronized(installedApk) {
            if (!installedApk.contains(sourceApk)) {
               ...
               
                if (loader == null) {
                    Log.e("MultiDex", "Context class loader is null. Must be running in test mode. Skip patching.");
                } else {
                    try {
                        clearOldDexDir(mainContext);
                    } catch (Throwable var24) {
                        Log.w("MultiDex", "Something went wrong when trying to clear old MultiDex extraction, continuing without cleaning.", var24);
                    }
					//创建文件夹/data/data/xxx/cache-code/secondary-dexes
                    File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName);
                    MultiDexExtractor extractor = new MultiDexExtractor(sourceApk, dexDir);
                    IOException closeException = null;

                    try {
                        //用来提取apk文件里的dex文件
                        List files = extractor.load(mainContext, prefsKeyPrefix, false);

                        try {
                            installSecondaryDexes(loader, dexDir, files);
                        } catch (IOException var26) {
                            if (!reinstallOnPatchRecoverableException) {
                                throw var26;
                            }

                            Log.w("MultiDex", "Failed to install extracted secondary dex files, retrying with forced extraction", var26);
                            files = extractor.load(mainContext, prefsKeyPrefix, true);
                            installSecondaryDexes(loader, dexDir, files);
                        }
     
                    ...

通过MultiDexExtractor来提取APK文件里额外的Dex文件,方法如下

List<? extends File> load(Context context, String prefsKeyPrefix, boolean forceReload) throws IOException {
       ...
            List files;
            //判断是否需要重新提取dex
            if (!forceReload && !isModified(context, this.sourceApk, this.sourceCrc, prefsKeyPrefix)) {
                try {
                    //加载已经提取了的dex
                    files = this.loadExistingExtractions(context, prefsKeyPrefix);
                } catch (IOException var6) {
                    Log.w("MultiDex", "Failed to reload existing extracted secondary dex files, falling back to fresh extraction", var6);
                    //执行dex文件提取
                    files = this.performExtractions();
                    putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files);
                }
            } else {
                ...
				//执行dex文件提取
                files = this.performExtractions();
                //把提取信息保存到sp,用来判断是否需要重新提取
                putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files);
            }

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

Dex文件的提取先判断是否已经提取过,如果没有则执行提取,否则直接加载,先看提取方法

private List<MultiDexExtractor.ExtractedDex> performExtractions() throws IOException {
        String extractedFilePrefix = this.sourceApk.getName() + ".classes";
        this.clearDexDir();
        List<MultiDexExtractor.ExtractedDex> files = new ArrayList();
        //解压/data/app/xxxx/xxx.apk文件
        ZipFile apk = new ZipFile(this.sourceApk);

        try {
        	//额外的dex文件从编号2开始,即classes2.dex
            int secondaryNumber = 2;
			//获取classesX.dex
            for(ZipEntry dexFile = apk.getEntry("classes" + secondaryNumber + ".dex"); dexFile != null; dexFile = apk.getEntry("classes" + secondaryNumber + ".dex")) {
                //创建xxx.apk.classesX.zip并把classesX.dex压缩进来
                String fileName = extractedFilePrefix + secondaryNumber + ".zip";
                MultiDexExtractor.ExtractedDex extractedFile = new MultiDexExtractor.ExtractedDex(this.dexDir, fileName);
                files.add(extractedFile);
                Log.i("MultiDex", "Extraction is needed for file " + extractedFile);
                int numAttempts = 0;
                boolean isExtractionSuccessful = false;
				//重试3次
                while(numAttempts < 3 && !isExtractionSuccessful) {
                    ++numAttempts;
                    //执行压缩,把classesX.dex提取到base.apk.classesX.zip
                    extract(apk, dexFile, extractedFile, extractedFilePrefix);
                   ...
                }
                ++secondaryNumber;
            }
        } 
		...
        return files;
    }
    //具体提取过程,文件读写
   private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo, String extractedFilePrefix) throws IOException, FileNotFoundException {
        InputStream in = apk.getInputStream(dexFile);
        ZipOutputStream out = null;
        //创建一个临时压缩文件tmp-xxx.apk.classesxxxx.zip
        File tmp = File.createTempFile("tmp-" + extractedFilePrefix, ".zip", extractTo.getParentFile());
        Log.i("MultiDex", "Extracting " + tmp.getPath());

        try {
            out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp)));

            try {
               //把dex文件重命名为classes.dex
                ZipEntry classesDex = new ZipEntry("classes.dex");
                classesDex.setTime(dexFile.getTime());
                out.putNextEntry(classesDex);
                byte[] buffer = new byte[16384];

                for(int length = in.read(buffer); length != -1; length = in.read(buffer)) {
                    out.write(buffer, 0, length);
                }
			...
			//temp压缩文件写入指定的压缩文件如xxx.apk.classes2.zip
            if (!tmp.renameTo(extractTo)) {
                throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() + "\" to \"" + extractTo.getAbsolutePath() + "\"");
            }
        ...

    }

提取过程是对apk文件解压获取里面的dex文件如classes2.dex并把它压缩到/data/data/xxx/cache-code/secondary-dexes目录下,如xxx.apk.classes2.zip,每个zip下都有一个classes.dex,即要合并进来的额外的dex。提取完后,把提取信息保存下来,供下次使用,方法如下:

private static void putStoredApkInfo(Context context, String keyPrefix, long timeStamp, long crc, List<MultiDexExtractor.ExtractedDex> extractedDexes) {
        SharedPreferences prefs = getMultiDexPreferences(context);
        Editor edit = prefs.edit();
        edit.putLong(keyPrefix + "timestamp", timeStamp);  //用来校验
        edit.putLong(keyPrefix + "crc", crc);      //用来校验
        edit.putInt(keyPrefix + "dex.number", extractedDexes.size() + 1);  //这里就是dex的个数
        int extractedDexId = 2;

        for(Iterator var10 = extractedDexes.iterator(); var10.hasNext(); ++extractedDexId) {
            MultiDexExtractor.ExtractedDex dex = (MultiDexExtractor.ExtractedDex)var10.next();
            edit.putLong(keyPrefix + "dex.crc." + extractedDexId, dex.crc);
            edit.putLong(keyPrefix + "dex.time." + extractedDexId, dex.lastModified());
        }

        edit.commit();
    }

通过保持的信息,判断下次启动是否存在已经提取的dex(通过方法isModified()),如果存在,则直接加载即可,即loadExistingExtractions()

private List<MultiDexExtractor.ExtractedDex> loadExistingExtractions(Context context, String prefsKeyPrefix) throws IOException {
        Log.i("MultiDex", "loading existing secondary dex files");
        String extractedFilePrefix = this.sourceApk.getName() + ".classes";
        SharedPreferences multiDexPreferences = getMultiDexPreferences(context);
        int totalDexNumber = multiDexPreferences.getInt(prefsKeyPrefix + "dex.number", 1);  //执行提取后保存的信息,即dex个数
        List<MultiDexExtractor.ExtractedDex> files = new ArrayList(totalDexNumber - 1);

        for(int secondaryNumber = 2; secondaryNumber <= totalDexNumber; ++secondaryNumber) {
            String fileName = extractedFilePrefix + secondaryNumber + ".zip";
            //ExtractedDex是一个文件
            MultiDexExtractor.ExtractedDex extractedFile = new MultiDexExtractor.ExtractedDex(this.dexDir, fileName);
            ...
            //直接添加进来
            files.add(extractedFile);
        }

        return files;
    }

至此,提取dex文件的过程就分析完了,下面开始分析如何合并这些dex文件

private static void doInstallation(Context mainContext, File sourceApk, File dataDir, String secondaryFolderName, String prefsKeyPrefix, boolean reinstallOnPatchRecoverableException) throws IOException, IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, SecurityException, ClassNotFoundException, InstantiationException {
        ...
        				//提取dex的过程,返回的是ExtractedDex,一个zip文件
                        List files = extractor.load(mainContext, prefsKeyPrefix, false);
                        ...
                            //开始合并dex
                            installSecondaryDexes(loader, dexDir, files);
                        ...
}

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 (VERSION.SDK_INT >= 19) { //不同版本下的实现不同
                MultiDex.V19.install(loader, files, dexDir);
            } else if (VERSION.SDK_INT >= 14) {
                MultiDex.V14.install(loader, files);
            } else {
                MultiDex.V4.install(loader, files);
            }
        }
    }
//合并的具体实现 SDK_INT >= 19
static void install(ClassLoader loader, List<? extends File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
            Field pathListField = MultiDex.findField(loader, "pathList");  //反射获取ClassLoader下的pathList对象
            Object dexPathList = pathListField.get(loader);    
            ...
        }

看到这里,先找一下ClassLoader的pathList对象(因为AS里不能直接查看,这里通过Android社区 查看源码)。ClassLoader有三个子类PathClassLoader、DexClassLoader和BaseDexClassLoader,这里直接找PathClassLoader跟BaseDexClassLoader即可,因为传进来的是PathClassLoader。发现在BaseDexClassLoader里找到了

public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;
   ...

pathList是一个DexPathList对象

final class DexPathList {
    private static final String DEX_SUFFIX = ".dex";
    private static final String JAR_SUFFIX = ".jar";
    private static final String ZIP_SUFFIX = ".zip";
    private static final String APK_SUFFIX = ".apk";

    /** class definition context */
    private final ClassLoader definingContext;

    /**
     * List of dex/resource (class path) elements.
     * Should be called pathElements, but the Facebook app uses reflection
     * to modify 'dexElements' (http://b/7726934).
     */
    private final Element[] dexElements;

继续往下分析

 static void install(ClassLoader loader, List<? extends File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
            Field pathListField = MultiDex.findField(loader, "pathList");
            Object dexPathList = pathListField.get(loader);  //拿到classloader里的pathList对象
            //执行合并
            MultiDex.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory, suppressedExceptions));
            ...
}
//
private static Object[] makeDexElements(Object dexPathList, ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
			//反射调用dexPathList里makeDexElements()方法获取Elements
            Method makeDexElements = MultiDex.findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class, ArrayList.class);
            return (Object[])((Object[])makeDexElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions));
        }

private static void expandFieldArray(Object instance, String fieldName, Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
        Field jlrField = findField(instance, fieldName);  //反射获取dexPathList对象里的dexElements数组
        Object[] original = (Object[])((Object[])jlrField.get(instance));
        Object[] combined = (Object[])((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);
    }

通过上面代码可知,expandFieldArray()的作用是把dexPathList对象里的dexElements数组进行了扩展,原则是把原来的Element放前面,extraElements放后面,extraElements是通过makeDexElements()方法获取,实际是通过反射调用dexPathList里makeDexElements(),下面分析具体实现

 /**
     * Makes an array of dex/resource path elements, one per element of
     * the given array.
     */
    private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
                                             ArrayList<IOException> suppressedExceptions) {
        ArrayList<Element> elements = new ArrayList<Element>();
        /*
         * Open all files and load the (direct or contained) dex files
         * up front.
         */
        for (File file : files) {
            File zip = null;
            DexFile dex = null;
            String name = file.getName();

            if (name.endsWith(DEX_SUFFIX)) {    //.dex文件
                // Raw dex file (not inside a zip/jar).
                ....
                    //生成DexFile
                    dex = loadDexFile(file, optimizedDirectory);
                    ....
            } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)  //.apk .jar .zip文件
                    || name.endsWith(ZIP_SUFFIX)) {
                zip = file;
                ...
                //生成DexFile
                dex = loadDexFile(file, optimizedDirectory);
                ...
                }
            }
            if ((zip != null) || (dex != null)) {
                elements.add(new Element(file, false, zip, dex));
            }
        }
        return elements.toArray(new Element[elements.size()]);
    }
    
    private static DexFile loadDexFile(File file, File optimizedDirectory)
            throws IOException {
        if (optimizedDirectory == null) {  //优化后的dex目录,api 19以上不为空
            return new DexFile(file);
        } else {
            String optimizedPath = optimizedPathFor(file, optimizedDirectory);
            return DexFile.loadDex(file.getPath(), optimizedPath, 0);
        }
    }
//DexFile
/*
 * @param sourcePathName
 *  Jar or APK file with "classes.dex".  (May expand this to include
 *  "raw DEX" in the future.)
 * /
static public DexFile loadDex(String sourcePathName, String outputPathName,
        int flags) throws IOException {

        /*
         * TODO: we may want to cache previously-opened DexFile objects.
         * The cache would be synchronized with close().  This would help
         * us avoid mapping the same DEX more than once when an app
         * decided to open it multiple times.  In practice this may not
         * be a real issue.
         */
        return new DexFile(sourcePathName, outputPathName, flags);
    }

makeDexElements()用来创建Element,而Element创建需要DexFile,DexFile要么通过new DexFile(sourcePathName),要么通过DexFile.loadDex(sourcePathName, outputPathName, flags),两种方式都是new出来的,只不过传入的参数不同。sourcePathName可以是包含了classes.dex的jar/apk/zip文件,即上面通过MultiDexExtractor.load提取出来的zip文件。

优化方向

Multidex耗时在于要从apk文件里提取dex,然后压缩,再通过反射的方式把DexFile加入到dexPathList列表后面,但是提取出dex文件也是可以直接使用的,也就是说不用再次压缩。所以优化方向可以提取dex然后直接反射,跳过压缩的步骤,应该可行,但是还没实践过。

官方文档

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值