关于multidex的误解与源码分析

为什么需要multidex

我个人知道multidex,源于我好几年前编译一个app,想看看效果,结果编译期却报如下错误

Conversion to Dalvik format failed:
Unable to execute dex: method ID not in [0, 0xffff]: 65536

幸好当时已经有了现成的解决方案——multidex,不然我根本跑不起来那个app。

这个错误的根本原因是Dalvik指令中的invoke指令方法引用索引只有16 bit。因此Android的编译产物dex中限制方法的引用数最大为65536,方法的引用包括引用的Android Framework方法,导入的Library中的方法,以及自己app写的方法,由于debug包不会被proguard shrink,因此debug包最先在编译期就报错了。

一个dex文件最多能引用64K个方法,这个问题通常被称为64K引用限制(64K reference limit)。

常见对multidex的误解

常见的对multidex的误解有两个。

误解一:Android 5.0不需要multidex

我时常看到一些文章,说Android 5.0修复了这个问题,所以5.0不需要multidex。

首先说结论,即便是运行在5.0及以上的系统,也需要multidex。

要消除这个误解,就要明确一点,multidex本质上是两步,第一步是编译时分拆dex,第二步是运行时调用Multidex.install

编译时拆分dex

前面说了,问题的根源在于因为Dalvik指令限制导致dex限制方法引用数,本质上是单个dex中引用不了那么多方法,因此做一个简单的逻辑推导:只要Android的编译产物还是dex,且Dalvik指令不改,那么这个64K限制就一直存在。

因此multidex的第一步编译期分拆dex,无论如何都要做,除非哪一天编译产物变成了别的东西,或者相关的规范改了,才能称得上“修复”这个问题。目前来看,考虑到向前兼容的问题,64K引用限制在编译期暂时无法修复。

运行时Multidex.install

Android有两套运行时,Dalvik和ART,不严谨的说,可以认为Android 5.0及以后使用ART,5.0以前使用Dalvik运行时。

Dalivk只加载app的主dex(classes.dex),因此app需要自行加载子dex(classesN.dex)。Mutlidex.install就是干这个事的。

ART则会加载apk中的主dex和classesN.dex形式的子dex,无论是安装时直接编译出oat文件,还是使用JIT,都不需要app操心,因此当app运行在ART环境时,不需要调用Mutlidex.install。不需要调用的原因并非不需要,只是因为运行时已经帮我们做了这一步了。

文档中说,当minSdkVersion21时,默认开启了multidex,且不需要引入multidex依赖,意思就是只在编译时拆dex,不需要运行时调用Mutlidex.install

5.0不需要multidex,仅仅是说不需要导入multidex那个库而已,打包产物还是要拆分dex的。

误解二:multidex能解决LinearAlloc限制

第二个误解是认为multidex能解决LinearAlloc的限制。

app的dex达到一定的规模后,在2.X的手机上就可能遇到安装阶段失败的问题,报INSTALL_FAILED_DEXOPT错误,实质是因为安装时有个dexopt程序对dex进行处理,dexopt中使用LinearAlloc来保存dex中的class信息,dexopt时,会一次性将主dex的类信息加载到LinearAlloc中,由于2.X版本LinearAlloc大小仅有5M,当class信息足够大时,比如method足够多,就会导致触及LinearAlloc的上限导致报LinearAlloc exceeded capacity,此时有可能主dex方法数都还没超过64K,就率先在LinearAlloc这里超上限了。

一般把这个问题称为linearalloc限制(linearalloc limit),相关的issue可以参考37008143。

在2.X上,如果app有着复杂的interface继承结构,也是能触发这个bug的,36936369中提到其本质原因是没有判重,导致iftable快速膨胀最终导致LinearAlloc达到上限。

对于2.X极其容易出现的dexopt导致安装失败,有人提出通过set-max-idx-number参数,限制每个dex的方法数,这样就可以避免dexopt时挂掉。

这个方法之所以有效,是因为限制方法数,间接使得主dex中的class信息减少,而4.X及以前安装时dexopt只优化主dex,减少主dex class信息确实能防止在dexopt时直接挂掉。

可是LinearAlloc本身是用来存储class信息的东西,dexopt中会用,Dalvik运行时也会用,只不过运行时是按需加载。使用multidex能解决安装失败,但运行时是绕不过去的,如果app不断的加载类,那么app将在运行后的某一时间点因为触及linearalloc limit崩溃,由于2.X LinearAlloc大小只有5M,更容易崩掉。

4.X上,LinearAlloc的大小提升到了8M或16M,显然这并没有从根本上解决问题,但因为LinearAlloc更大,且运行时是按需加载class,更不容易崩掉而已。按Android文档中的说法,只有5.0才真正解决这个问题。

因此multidex是无法解决linearalloc限制的,他只能绕过4.X及以前dexopt时直接挂掉的问题,但运行时依旧无能为力。

Facebook提出的通过JNI修改LinearAlloc上限的方法,大概是目前app唯一能在运行时绕过这个限制的方案。

不过好在目前(2019年12月)绝大部分app,minSdkVersion都已经在14了,升到21也是迟早的事,届时就不用操心了。

Multidex.install干了什么

概要的说,Multidex.install会在运行时不支持multidex时,主动加载子dex,加载时会校验apk信息,确保dex与apk匹配。加载的手段是将子dex信息插入ClassLoader中。下面是详细的源码流程分析。

根据Android的文档指引,无论是否直接用MultiDexApplication,最终都是要我们在attachBaseContext中调用Multidex.install

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

之所以要在Application.attachBaseContext中调用,主要是因为这个方法执行时机早于ContentProvider install时机(即调用早于ContentProvider.onCreate)。动态加载dex的时候要格外小心class加载时,其直接引用到的class所在dex必须已经被加载,否则在4.X及以前版本运行时,不会立即崩溃,将在触发调用的时间点当场崩溃,报NoClassDefFoundError,且由于崩溃时间点延后,对于初次接触该崩溃的人,很容易被误导。

版本判断

接下来是版本相关的判断。

    private static final int MIN_SDK_VERSION = 4;

    public static void install(Context context) {
        Log.i(TAG, "Installing application");
        if (IS_VM_MULTIDEX_CAPABLE) {
            Log.i(TAG, "VM has multidex support, MultiDex support library is disabled.");
            return;
        }

        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 + ".");
        }

        ......
    }

对于运行时本身就有multidex支持的情况,这个调用直接返回,什么都不做;如果当前运行版本低于api level 4,直接抛异常不支持。

判断运行时是否支持multidex,出乎我的意料,并不是通过SDK版本判断的,而是判断java.vm.version属性的值,是否为2.1及以上。在我的小米MIX3(Andorid 9.0)上,该属性值为2.1.0。当然,Android 5.0开始这个属性值就是2.1.0

    private static final boolean IS_VM_MULTIDEX_CAPABLE =
            isVMMultidexCapable(System.getProperty("java.vm.version"));

    private static final int VM_WITH_MULTIDEX_VERSION_MAJOR = 2;

    private static final int VM_WITH_MULTIDEX_VERSION_MINOR = 1;

    static boolean isVMMultidexCapable(String versionString) {
        boolean isMultidexCapable = false;
        if (versionString != null) {
            StringTokenizer tokenizer = new StringTokenizer(versionString, ".");
            String majorToken = tokenizer.hasMoreTokens() ? tokenizer.nextToken() : null;
            String minorToken = tokenizer.hasMoreTokens() ? tokenizer.nextToken() : null;
            if (majorToken != null && minorToken != null) {
                try {
                    int major = Integer.parseInt(majorToken);
                    int minor = Integer.parseInt(minorToken);
                    isMultidexCapable = (major > VM_WITH_MULTIDEX_VERSION_MAJOR)
                            || ((major == VM_WITH_MULTIDEX_VERSION_MAJOR)
                                    && (minor >= VM_WITH_MULTIDEX_VERSION_MINOR));
                } catch (NumberFormatException e) {
                    // let isMultidexCapable be false
                }
            }
        }
        Log.i(TAG, "VM with version " + versionString +
                (isMultidexCapable ?
                        " has multidex support" :
                        " does not have multidex support"));
        return isMultidexCapable;
    }

看完版本判断的代码,接下来继续看install的逻辑:

    public static void install(Context context) {
        ......

        try {
            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;
            }

            ......

        } catch (Exception e) {
            Log.e(TAG, "MultiDex installation failure", e);
            throw new RuntimeException("MultiDex installation failed (" + e.getMessage() + ").");
        }
        Log.i(TAG, "install done");
    }

对于getApplicationInfo,方法很简单,但注释特别多,简单来讲,为了防止app运行旧主dex拿到新ApplicationInfo,保持一致性,直接通过context来获取ApplicationInfo,而不是向系统查询。context.getApplicationInfo()其实就是从LoadedApk中取成员变量而已。

    private static ApplicationInfo getApplicationInfo(Context context) {
        try {
            /* Due to package install races it is possible for a process to be started from an old
             * apk even though that apk has been replaced. Querying for ApplicationInfo by package
             * name may return information for the new apk, leading to a runtime with the old main
             * dex file and new secondary dex files. This leads to various problems like
             * ClassNotFoundExceptions. Using context.getApplicationInfo() should result in the
             * process having a consistent view of the world (even if it is of the old world). The
             * package install races are eventually resolved and old processes are killed.
             */
            return context.getApplicationInfo();
        } 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 ApplicationInfo from Context. " +
                    "Must be running in test mode. Skip patching.", e);
            return null;
        }
    }

接着看代码,其中sourceDir指向安装后的apk文件,一般是/data/app//base.apk;而dataDir指向apk的私有数据目录:

    private static final String CODE_CACHE_SECONDARY_FOLDER_NAME = "secondary-dexes";

    private static final String NO_KEY_PREFIX = "";

    public static void install(Context context) {
            ......
            doInstallation(context,
                    new File(applicationInfo.sourceDir),
                    new File(applicationInfo.dataDir),
                    CODE_CACHE_SECONDARY_FOLDER_NAME,
                    NO_KEY_PREFIX,
                    true);
        ......
    }

接下来看doInstallation

    private static final int MAX_SUPPORTED_SDK_VERSION = 20;

    private static final Set installedApk = new HashSet();

    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)) {
                return;
            }
            installedApk.add(sourceApk);

            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") + "\"");
            }

            ......
    }

首先是同步保护,防止单个进程内执行多次(后面几个小节的代码中可以看到multidex还使用FileLock做进程同步)。然后判断SDK_INT并输出警告,这里的警告是当前app运行在5.0及以上的系统中,运行时却没有multidex支持才输出,不过通常不会有这种情况。

获取ClassLoader

继续往下看。

    private static void doInstallation(...) throws ... {
        synchronized (installedApk) {
            ......

            /* The patched class loader is expected to be a ClassLoader capable of loading DEX
             * bytecode. We modify its pathList field to append additional DEX file entries.
             */
            ClassLoader loader = getDexClassloader(mainContext);
            if (loader == null) {
                return;
            }

            ......
        }
    }

这里要获取一个ClassLoader,详细代码如下:

    private static ClassLoader getDexClassloader(Context context) {
        ClassLoader loader;
        try {
            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 null;
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
            if (loader instanceof dalvik.system.BaseDexClassLoader) {
                return loader;
            }
        } else if (loader instanceof dalvik.system.DexClassLoader
                    || loader instanceof dalvik.system.PathClassLoader) {
            return loader;
        }
        Log.e(TAG, "Context class loader is null or not dex-capable. "
                + "Must be running in test mode. Skip patching.");
        return null;
    }

这里的逻辑很简单,获取context类的ClassLoader,如果是Android 4.0及以上,那么只要它是BaseDexClassLoader就认为可以加载dex;否则要求它必须是DexClassLoader或者PathClassLoader之一。代码这么写,原因也很简单,4.0以前,DexClassLoader和PathClassLoader都是直接继承ClassLoader,其逻辑与成员变量都在自己的类里面。4.0及以后,无论DexClassLoader还是PathClassLoader,其逻辑与成员变量都移到了他们的新父类BaseDexClassLoader中。

回到doInstallation中:

    private static void doInstallation(...) throws ... {
        synchronized (installedApk) {
            ......

            try {
              clearOldDexDir(mainContext);
            } catch (Throwable t) {
              Log.w(TAG, "Something went wrong when trying to clear old MultiDex extraction, "
                  + "continuing without cleaning.", t);
            }

            ......
        }
    }

这个纯粹就是清理早期multidex使用的dex文件输出目录,里面是删除文件夹的逻辑,就不贴出来了。接着往下看:

    private static void doInstallation(...) throws ... {
        synchronized (installedApk) {
            ......

            File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName);
            MultiDexExtractor extractor = new MultiDexExtractor(sourceApk, dexDir);

            ......
        }
    }

getDexDir很简单,其实就是创建一个目录用来作为存储dex相关的文件,主要看MultiDexExtractor的构造方法。

    private static final String LOCK_FILENAME = "MultiDex.lock";

    MultiDexExtractor(File sourceApk, File dexDir) throws IOException {
        this.sourceApk = sourceApk;
        this.dexDir = dexDir;
        sourceCrc = getZipCrc(sourceApk);
        File lockFile = new File(dexDir, LOCK_FILENAME);
        lockRaf = new RandomAccessFile(lockFile, "rw");
        try {
            lockChannel = lockRaf.getChannel();
            try {
                cacheLock = lockChannel.lock();
            } catch (IOException | RuntimeException | Error e) {
                closeQuietly(lockChannel);
                throw e;
            }
        } catch (IOException | RuntimeException | Error e) {
            closeQuietly(lockRaf);
            throw e;
        }
    }

这里有一个FileLock的锁操作,这样可以保证提取dex和dexopt这一系列操作不会并行。解锁操作在MultiDexExtractor.close中进行:

    @Override
    public void close() throws IOException {
        cacheLock.release();
        lockChannel.close();
        lockRaf.close();
    }

提取子dex文件

继续回到doInstallation中:

    private static void doInstallation(...) throws ... {
        synchronized (installedApk) {
            ......
            IOException closeException = null;
            try {
                List files =
                        extractor.load(mainContext, prefsKeyPrefix, false);
                try {
                    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;
            }
        }
    }

首先调用MultiDexExtractor.load获取一系列文件,然后进行installSecondaryDexes。上面代码中的closeException是用来防止extractor.close()抛出的异常覆盖了本来可能要抛出去的异常。

先来看看MultiDexExtractor.load返回的文件到底是什么,我猜测是所有的classesN.dex。

    List load(Context context, String prefsKeyPrefix, boolean forceReload)
            throws IOException {
        if (!cacheLock.isValid()) {
            throw new IllegalStateException("MultiDexExtractor was closed");
        }

        List files;
        if (!forceReload && !isModified(context, sourceApk, sourceCrc, prefsKeyPrefix)) {
            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 {
            ......
            files = performExtractions();
            putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), sourceCrc,
                    files);
        }

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

首先要说明,这里虽然写的是提取文件,但并非直接将dex解压缩出来,dex提取出来后依然以zip文件存在的,每个zip中都有一个子dex。

performExtractions将apk中的classesN.dex全部提取到之前创建的用于存储dex输出文件的目录,前面说过,还是以zip文件的形式存在。调用本方法之后一定会跟着一个putStoredApkInfo调用。

putStoredApkInfo将apk文件的lastModified时间戳,apk的CRC校验码,提取的dex文件数,以及每个子dex对应zip文件的时间戳和CRC持久化到SharedPreferences

loadExistingExtractions中根据持久化信息中的dex文件数,可以直接知道所有提取出来的子dex文件对应zip文件的文件名,然后挨个校验时间戳和CRC,校验失败则会抛异常,导致外部走强制提取文件流程。

上面代码的逻辑也很简单,根据forceReloadisModified决定要不要performExtractions重新提取文件。forceReload为外部强制要求提取文件,isModified用于判断apk是否更新了。如果提取了文件,则会通过putStoredApkInfo持久化一些信息,持久化信息包括apk文件的lastModified时间戳,apk的CRC校验码,提取的dex文件数,以及每个子dex对应zip文件的时间戳和CRC。每次执行install都会将当前apk的时间戳和CRC与之前存储的对比,只要有一个对不上就会重新提取文件,重新持久化信息。如果不需要重新提取文件,直接调用loadExistingExtractions即可。

根据以上的分析,这个方法最终返回的文件列表,并非直接的子dex文件列表,而是一系列包含单个子dex的zip文件列表。

加载子dex

接下来看看installSecondaryDexes到底是如何加载子dex的。

    private static void installSecondaryDexes(ClassLoader loader, File dexDir,
        List 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);
            }
        }
    }

可以看到api level从4-13,使用V4实现,从14-18,使用V14实现,从19开始,使用V19实现,调用完对应版本的install方法,就代表已经加载的子dex。

实际上相当于,原本ClassLoader只加载了主dex,这里multidex是把子dex插到了app的ClassLoader中。接下来就从高版本到低版本,挨个看看实现。

V19

根据前面的调用,可知这里的loader一定是一个继承了BaseDexClassLoader的实例,具体是什么我们不关心;additionalClassPathEntries是一系列zip文件,每个zip文件里都有一个子dex;optimizedDirectory是一个目录。

    private static final class V19 {

        static void install(ClassLoader loader,
                List additionalClassPathEntries,
                File optimizedDirectory)
                        throws ... {
            Field pathListField = findField(loader, "pathList");
            Object dexPathList = pathListField.get(loader);
            ArrayList suppressedExceptions = new ArrayList();
            expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
                    new ArrayList(additionalClassPathEntries), optimizedDirectory,
                    suppressedExceptions));
            if (suppressedExceptions.size() > 0) {
                ......
                throw exception;
            }
        }
        ......
    }

逻辑很简单,反射loader中的pathList,查阅Android 4.4(19)BaseDexClassLoader源码可知其对应的类型为DexPathList,之后再反射其makeDexElements,通过这个方法可以将additionalClassPathEntries转换成一个Element[],再将反射pathList的成员dexElements,其类型为Element[],将两个数组合成一个数组,覆盖原dexElements的值,即完成子dex加载。

贴出来的代码中省略了异常处理逻辑,意图很简单,这里就不贴了。

V19的逻辑,本质上就是合并两个Element[],获取子dex的Element[]时,借助了DexPathList.makeDexElements直接获取,然后进行合并。

V14

V14的代码比V19的代码要长的多,还是从install看起。
根据前面的调用,可知这里的参数与V19完全相同。

        static void install(ClassLoader loader,
                List additionalClassPathEntries)
                        throws  ... {
            Field pathListField = findField(loader, "pathList");
            Object dexPathList = pathListField.get(loader);
            Object[] elements = new V14().makeDexElements(additionalClassPathEntries);
            try {
                expandFieldArray(dexPathList, "dexElements", elements);
            } catch (NoSuchFieldException e) {
                // dexElements was renamed pathElements for a short period during JB development,
                // eventually it was renamed back shortly after.
                Log.w(TAG, "Failed find field 'dexElements' attempting 'pathElements'", e);
                expandFieldArray(dexPathList, "pathElements", elements);
            }
        }

V14比V19复杂的地方在于获取子dex的Element[]

同时注意一个细节,合并数组时如果出现NoSuchFieldException,则会尝试反射pathElements。为什么会做这种尝试,其实挺好笑的,原因我写在本文末尾。

在V14构造方法里,会尝试反射Element的构造方法:

        private  V14() throws ClassNotFoundException, SecurityException, NoSuchMethodException {
            ElementConstructor constructor;
            Class elementClass = Class.forName("dalvik.system.DexPathList$Element");
            try {
                constructor = new ICSElementConstructor(elementClass);
            } catch (NoSuchMethodException e1) {
                try {
                    constructor = new JBMR11ElementConstructor(elementClass);
                } catch (NoSuchMethodException e2) {
                    constructor = new JBMR2ElementConstructor(elementClass);
                }
            }
            this.elementConstructor = constructor;
        }

每一个XXConstractor都对应一个Element构造方法,Android 4.X的Element构造方法变了好几次,这里会尝试反射到其中一个,ICSElementConstructor对应4.0~4.1,JBMR11ElementConstructor对应4.3,JBMR2ElementConstructor对应4.2。
接下来看V14.makeDexElements:

        private Object[] makeDexElements(List files)
                throws IOException, SecurityException, IllegalArgumentException,
                InstantiationException, IllegalAccessException, InvocationTargetException {
            Object[] elements = new Object[files.size()];
            for (int i = 0; i < elements.length; i++) {
                File file = files.get(i);
                elements[i] = elementConstructor.newInstance(
                        file,
                        DexFile.loadDex(file.getPath(), optimizedPathFor(file), 0));
            }
            return elements;
        }

可见V14生成子dex Element[]时,是反射Element构造方法,主动调用其构造方法进行构造。

V4

最后一个是V4.install。根据前文可知,这里的loader要么是一个PathClassLoader,要么是一个DexClassLoader
由于早期没有DexPathList这个类,所以V4的实现要修改loader实例中的path, mPaths, mFiles, mZips, mDexs共五个字段。

    private static final class V4 {
        static void install(ClassLoader loader,
                List additionalClassPathEntries)
                        throws IllegalArgumentException, IllegalAccessException,
                        NoSuchFieldException, IOException {
            int extraSize = additionalClassPathEntries.size();

            Field pathField = findField(loader, "path");

            StringBuilder path = new StringBuilder((String) pathField.get(loader));
            String[] extraPaths = new String[extraSize];
            File[] extraFiles = new File[extraSize];
            ZipFile[] extraZips = new ZipFile[extraSize];
            DexFile[] extraDexs = new DexFile[extraSize];
            for (ListIterator iterator = additionalClassPathEntries.listIterator();
                    iterator.hasNext();) {
                File additionalEntry = iterator.next();
                String entryPath = additionalEntry.getAbsolutePath();
                path.append(':').append(entryPath);
                int index = iterator.previousIndex();
                extraPaths[index] = entryPath;
                extraFiles[index] = additionalEntry;
                extraZips[index] = new ZipFile(additionalEntry);
                extraDexs[index] = DexFile.loadDex(entryPath, entryPath + ".dex", 0);
            }

            pathField.set(loader, path.toString());
            expandFieldArray(loader, "mPaths", extraPaths);
            expandFieldArray(loader, "mFiles", extraFiles);
            expandFieldArray(loader, "mZips", extraZips);
            expandFieldArray(loader, "mDexs", extraDexs);
        }
    }

path的类型为String,其他几个分别是各自类型的数组。将原path的值和各个zip文件的路径以:连接起来,再覆盖path

其他的字段,都是构造对应的数组,然后调用expandFieldArray合并原值并覆盖,与前面V14、V19修改dexElements的逻辑一致。

为何尝试反射pathElements

V14的实现中,可以看到它在异常处理中会尝试反射pathElements,正式的Android版本中并没有这个字段,这个原因也比较有趣。

在Android 4.3的开发中间,有很短暂的一段时间,DexPathList的私有成员dexElements被改名为pathElements,本身DexPathList就不是公开API,更何况这还是个私有成员,理论上Google可以随便改。

然而Facebook很早就遇到linearalloc limit和64K reference limit,受一篇名为Custom Class Loading in Dalvik文章的启发,FB老早就已经在搞动态加载dex了。Facebook app会反射DexPathList中一个叫dexElements的成员并修改其值,如果Google改了这个字段名,Facebook app就得崩。

令我惊奇的是Google竟然又把这个字段名改回来了,还加了注释,说这个字段应该叫pathElements,但Facebook app会用反射来改dexElements,所以又给改回来了。难道这就是传说中的“客大欺店”?(狗头):

    /**
     * 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;

你可能感兴趣的:(关于multidex的误解与源码分析)