为什么需要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
。不需要调用的原因并非不需要,只是因为运行时已经帮我们做了这一步了。
文档中说,当minSdkVersion
为21
时,默认开启了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/
;而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,校验失败则会抛异常,导致外部走强制提取文件流程。
上面代码的逻辑也很简单,根据forceReload
和isModified
决定要不要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;