Android打包相关知识整理(二)

Multidex

Dalvik执行的是由多个.class文件合并成的一个.dex文件,而在这其中,会对.dex包中所有的方法通过方法ID做一个索引,存储在一个链表中,而这个链表的长度用一个short类型来保存。short类型占两个字节,所以一个short最大值应该是65536,因此如果我们一个包中的方法数如果太多,以至于链表索引的长度超过了65546,就会有问题。虽然在新版本中Google已经修复了这一问题,但是为了兼容低版本系统,我们需要在方法数超过这一限制时做一些处理。
解决方法有很多,其中有一条就是分割dex,即如果一个dex放不下,就多分几个dex包,这样就不会有问题了。Google官方推出了multidex方案,可以有效的进行dex分包。multidex的原理很简单,在打包时主动分包,分为一个主dex包和多个次dex包,然后一起打包成apk。在应用启动时,会首先加载主dex包作为入口,然后依次加载次dex包。

配置使用

配置使用很简单,分为两步:

  1. 修改Gradle配置,使项目支持multidex。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
android {
compileSdkVersion 27
defaultConfig {
...
minSdkVersion 14
targetSdkVersion 27
...
// Enabling multidex support.
multiDexEnabled true
}
...
}
dependencies {
compile 'com.android.support:multidex:1.0.3'
}
  1. 在代码中启动MultiDex。
    具体来讲,有三种方式。第一种,通过在AndroidManifest文件中指定使用MultidexApplication
1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.multidex.myapplication">
<application
...
android:name="android.support.multidex.MultiDexApplication">
...
</application>
</manifest>

第二种,自定义Application并继承MultiDexApplication

1
2
3
4
5
6
7
8
9
public class DemoApp extends MultiDexApplication {

@Override
public void onCreate() {
super.onCreate();
// do nothing
}

}

第三种,自定义Application,然后在attachBaseContext方法中启用MultiDex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class BaseApplication extends Application {

@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(base);
}

@Override
public void onCreate() {
super.onCreate();
}

}

其实上述三种方法最终目的都是调用MultiDex.install()这一方法。

dex包拆分

dex拆包的步骤:

  1. 扫描整个工程代码,得到一个记录了标记主、从dex的main-dex-list。

  2. 根据main-dex-list,对把项目编译后的.class文件按主、从分开。

  3. 分别将主、从.class文件打包为主、从.dex文件。

    生成mai-dex-list的工具在Android Sdk的Build Tools中,是一个名字叫mainDexClasses的脚本文件。其核心部分为先生成一个jar包,然后连同所有的文件一起作为参数,调用com.android.multidex.MainDexListBuilder:

1
java -cp "$jarpath" com.android.multidex.MainDexListBuilder ${disableKeepAnnotated} "${tmpOut}" ${@} ||  exit 11

MainDexListBuilder源码可以在这里查看:MainDexListBuilder.java
主要做的工作就是将符合keep规则的部分加入到主dex中。因为分包之后的合包必须先执行主包再一一合包次包,因此有些必需的文件就得装在主dex包中,这里的keep规则就是为了能够将这部分文件做保护,这样在生成main-dex-list时就会默认放入主dex包中了。

dex包合并

初始化

根据前面MultiDex配置的方法,那么MultiDe在合并的时候就是在首次安装运行时,将从dex包逐一安装,入口就是Application中的这行代码:

1
MultiDex.install(this);

在install中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static void install(Context context) {
Log.i("MultiDex", "Installing application");
//这里判断一下当前系统是否支持MultiDex,如果已经支持,就不用MultiDex自己合并了
if (IS_VM_MULTIDEX_CAPABLE) {
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 {
//获取Application对象的引用
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;
}
//具体的安装过程
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");
}
}

所以具体的安装是在doInstallation()中执行的,在调用这个方法时传入的主要参数有一个context引用,一个存放base apk的目录,一个app的data目录,一个字符串secondary-dexes作为从dex目录。

总体过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
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 {
//这里的installedApk是一个HashSet,应该是保存当前以及安装的dex包
Set var6 = installedApk;
synchronized(installedApk) {
//如果souceApk不存在,就将他加入到这个HashSet中
if (!installedApk.contains(sourceApk)) {
installedApk.add(sourceApk);
//如果编译版本大于当前支持的最大版本,就会提示
if (VERSION.SDK_INT > 20) {
Log.w("MultiDex", "MultiDex is not guaranteed to work in SDK version " + VERSION.SDK_INT + ": SDK version higher than " + 20 + " 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") + "\"");
}
//用当前的Context获取一个ClassLoader对象的引用
ClassLoader loader;
try {
loader = mainContext.getClassLoader();
} catch (RuntimeException var25) {
Log.w("MultiDex", "Failure while trying to obtain Context class loader. Must be running in test mode. Skip patching.", var25);
return;
}

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);
}
//创造一个dex目录
File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName);
//创造一个MultiDexExtractor对象
MultiDexExtractor extractor = new MultiDexExtractor(sourceApk, dexDir);
IOException closeException = null;

try {
//调用MultiDexExtractor对象的load()方法,获取一个文件列表,这个文件列表应该是其他的从dex
List files = extractor.load(mainContext, prefsKeyPrefix, false);

try {
//安装这些从dex包
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);
}
} finally {
try {
extractor.close();
} catch (IOException var23) {
closeException = var23;
}

}

if (closeException != null) {
throw closeException;
}
}
}
}
}

梳理一下,就是首先看一下sourceApk是否已经安装,如果没有则安装,然后用Context获取一个ClasLoader,清除掉旧的缓存,获取从dex包,然后用前面获取的那个ClassLoader去加载这些从dex包。然后依次来看一下那几个重要的方法。
首先是clearOldDexDir():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private static void clearOldDexDir(Context context) throws Exception {
//获取文件目录
File dexDir = new File(context.getFilesDir(), "secondary-dexes");
if (dexDir.isDirectory()) {
Log.i("MultiDex", "Clearing old secondary dex dir (" + dexDir.getPath() + ").");
//获取目录下的所有子文件
File[] files = dexDir.listFiles();
if (files == null) {
Log.w("MultiDex", "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
return;
}

File[] var3 = files;
int var4 = files.length;
//遍历所有的子文件,依次调用delete()方法删除
for(int var5 = 0; var5 < var4; ++var5) {
File oldFile = var3[var5];
Log.i("MultiDex", "Trying to delete old file " + oldFile.getPath() + " of size " + oldFile.length());
if (!oldFile.delete()) {
Log.w("MultiDex", "Failed to delete old file " + oldFile.getPath());
} else {
Log.i("MultiDex", "Deleted old file " + oldFile.getPath());
}
}

if (!dexDir.delete()) {
Log.w("MultiDex", "Failed to delete secondary dex dir " + dexDir.getPath());
} else {
Log.i("MultiDex", "Deleted old secondary dex dir " + dexDir.getPath());
}
}

}

所以这里是清除了/data/data//files/secondary-dexes这一目录下的所有文件。
然后是getDexDir():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private static File getDexDir(Context context, File dataDir, String secondaryFolderName) throws IOException {

File cache = new File(dataDir, "code_cache");

try {//创建这个code_cache目录
mkdirChecked(cache);
} catch (IOException var5) {
//如果创建失败,就创建这个files/code_cache目录
cache = new File(context.getFilesDir(), "code_cache");
mkdirChecked(cache);
}
//在刚刚创建的files/code_cache目录下再创建一个secondary-dexes目录
File dexDir = new File(cache, secondaryFolderName);
mkdirChecked(dexDir);
return dexDir;
}

总结一下,这里先创建一个/data/data//code_cache目录,如果创建失败,改为创建一个/data/data//files/code_cache目录,然后再在这一目录下创建一个secondary目录,因此总的来说创建了一个/data/data//files/code_cache/secondary-dexes或者/data/data//code_cache/secondary-dexes目录,并返回。这样也跟刚才清除目录项对应了,clear的时候清除的就是创建失败时再次创建的这一目录。
再看创建一个extractor.load()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
List<? extends File> load(Context context, String prefsKeyPrefix, boolean forceReload) throws IOException {
Log.i("MultiDex", "MultiDexExtractor.load(" + this.sourceApk.getPath() + ", " + forceReload + ", " + prefsKeyPrefix + ")");
//验证当前获取的文件锁是否有效
if (!this.cacheLock.isValid()) {
throw new IllegalStateException("MultiDexExtractor was closed");
} else {
List files;
//如果不是强制的重新获取 或者 未被修改
if (!forceReload && !isModified(context, this.sourceApk, this.sourceCrc, prefsKeyPrefix)) {
try {
//提取之前已经存在的files
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);
//如果失败,通过performExtractions()方法获取
files = this.performExtractions();
//存储apk信息???
putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files);
}
} else {
//如果是强制的 或者 已经发生了修改
if (forceReload) {
Log.i("MultiDex", "Forced extraction must be performed.");
} else {
Log.i("MultiDex", "Detected that extraction must be performed.");
}
//也是需要调用performExtraction()方法提取
files = this.performExtractions();
putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files);
}

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

所以load()方法首先要确保当前操作获取到的文件锁是有效的,应该是为了确保多进程操作的安全。然后根据传入的参数forceReload以及是否有修改,来决定如何提取文件。如果可以,就会获取Existing文件,在此推测是缓存文件。否则,就要通过performExtractions()方法来获取并通过putStoreApkInfo()方法保存了。
看看this.loadExistingExtractions()方法是否符合推测:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
private List<MultiDexExtractor.ExtractedDex> loadExistingExtractions(Context context, String prefsKeyPrefix) throws IOException {
Log.i("MultiDex", "loading existing secondary dex files");
//构造一个apkName.classes为前缀
String extractedFilePrefix = this.sourceApk.getName() + ".classes";
//获取一个SP对象
SharedPreferences multiDexPreferences = getMultiDexPreferences(context);
//获取总的dex数量,即totalDexNumber
int totalDexNumber = multiDexPreferences.getInt(prefsKeyPrefix + "dex.number", 1);
//一个存放从dex文件的list 数量为总数量-1 也就是去掉了主dex文件
List<MultiDexExtractor.ExtractedDex> files = new ArrayList(totalDexNumber - 1);
//从第二个开始遍历,也就是跳过了第一个主dex
for(int secondaryNumber = 2; secondaryNumber <= totalDexNumber; ++secondaryNumber) {
//以前缀+当前的数字+.zip为文件名,比如 xxx.classes2.zip
String fileName = extractedFilePrefix + secondaryNumber + ".zip";
//获取这一文件
MultiDexExtractor.ExtractedDex extractedFile = new MultiDexExtractor.ExtractedDex(this.dexDir, fileName);
if (!extractedFile.isFile()) {//不存在 就会报错
throw new IOException("Missing extracted secondary dex file '" + extractedFile.getPath() + "'");
}
//存在 就会先做一些验证工作
extractedFile.crc = getZipCrc(extractedFile);
long expectedCrc = multiDexPreferences.getLong(prefsKeyPrefix + "dex.crc." + secondaryNumber, -1L);
long expectedModTime = multiDexPreferences.getLong(prefsKeyPrefix + "dex.time." + secondaryNumber, -1L);
long lastModified = extractedFile.lastModified();
if (expectedModTime != lastModified || expectedCrc != extractedFile.crc) {
//验证不通过 会报错
throw new IOException("Invalid extracted dex: " + extractedFile + " (key \"" + prefsKeyPrefix + "\"), expected modification time: " + expectedModTime + ", modification time: " + lastModified + ", expected crc: " + expectedCrc + ", file crc: " + extractedFile.crc);
}
//最终 验证通过 把这一文件加入到list中
files.add(extractedFile);
}
//最后返回那个构建的从dex文件list
return files;
}

首先获取一个SP对象,里面保存了dex文件数量相关的数据。然后通过遍历,获取所有的dex文件,加入到一个list钟返回。如果期间发现获取不到,则抛出一个异常。结合前面的代码,抛出异常后就需要通过performExtractions()方法重新提取一次:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
private List<MultiDexExtractor.ExtractedDex> performExtractions() throws IOException {
//同样的,先构造一个apkName.classes的前缀
String extractedFilePrefix = this.sourceApk.getName() + ".classes";
this.clearDexDir();
List<MultiDexExtractor.ExtractedDex> files = new ArrayList();
//获取一个zipfile 这就是原始apk
ZipFile apk = new ZipFile(this.sourceApk);

try {
int secondaryNumber = 2;
//让number从2开始累加 做一个遍历 期间的dexFile文件是apk通过classes+number+.dex为文件名获取的
for(ZipEntry dexFile = apk.getEntry("classes" + secondaryNumber + ".dex"); dexFile != null; dexFile = apk.getEntry("classes" + secondaryNumber + ".dex")) {
//构造一个文件名 apkName+.classes+number+.zip
String fileName = extractedFilePrefix + secondaryNumber + ".zip";
//这里的dexDir就是前面创建的缓存目录 xxx/secondary-dexs,这里是用这两个参数获取到dex文件
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;
//开始提取文件,不过只给了三次机会
while(numAttempts < 3 && !isExtractionSuccessful) {
++numAttempts;
//提取dex文件的核心方法
extract(apk, dexFile, extractedFile, extractedFilePrefix);

try {
//获取crc
extractedFile.crc = getZipCrc(extractedFile);
isExtractionSuccessful = true;
} catch (IOException var18) {
isExtractionSuccessful = false;
Log.w("MultiDex", "Failed to read crc from " + extractedFile.getAbsolutePath(), var18);
}

Log.i("MultiDex", "Extraction " + (isExtractionSuccessful ? "succeeded" : "failed") + " '" + extractedFile.getAbsolutePath() + "': length " + extractedFile.length() + " - crc: " + extractedFile.crc);
if (!isExtractionSuccessful) {
extractedFile.delete();
if (extractedFile.exists()) {
Log.w("MultiDex", "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;
}
} finally {
try {
apk.close();
} catch (IOException var17) {
Log.w("MultiDex", "Failed to close resource", var17);
}

}

return files;
}

可以看到,会从2开始,不断的构造一个dex文件,然后提取这一文件到dexDir中,提取的核心方法为extract():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo, String extractedFilePrefix) throws IOException, FileNotFoundException {
//这里的dexFile就是源dex文件 所以这里先获取了他的文件输入流
InputStream in = apk.getInputStream(dexFile);
ZipOutputStream out = null;
//以一个tmp文件作为交换文件
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 {
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);
}

out.closeEntry();
} finally {
out.close();
}

if (!tmp.setReadOnly()) {
throw new IOException("Failed to mark readonly \"" + tmp.getAbsolutePath() + "\" (tmp of \"" + extractTo.getAbsolutePath() + "\")");
}

Log.i("MultiDex", "Renaming to " + extractTo.getPath());
//将交换文件转换为目标文件,即secondary-dexs目录下的相对应的文件
if (!tmp.renameTo(extractTo)) {
throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() + "\" to \"" + extractTo.getAbsolutePath() + "\"");
}
} finally {
closeQuietly(in);
tmp.delete();
}

}

具体的提取过程到这里就结束了,总结一下,就是通过load()方法,将源dex文件一一写入到新的secondary-dexs目录下对应的文件中,这一过程会将源apk文件进行一个解压,且只会从第二个dex文件开始,因为第一个就是主dex文件,主dex是已经安装好了的。

install具体过程

好了,现在所有的次dex包都已经提取出来,接下来就是安装了,安装的入口就是前面的installSecondaryDexes()方法:

1
2
3
4
5
6
7
8
9
10
11
12
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);
}
}

}

安装dex包时,会根据不同的sdk版本有不同的方法。就以sdk>=19举例,他的install()方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static void install(ClassLoader loader, List<? extends File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
//通过反射 获取ClassLoader 的 pathList 字段
Field pathListField = MultiDex.findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
ArrayList<IOException> suppressedExceptions = new ArrayList();
//先调用makeDexElements方法 把从Dex文件们变成Element对象
//然后调用expandFieldArray方法 把刚才得到的Element对象们添加到dexPathList的dexElements字段的后面(这个字段是个数组)
MultiDex.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory, suppressedExceptions));
//添加一些IO异常信息
if (suppressedExceptions.size() > 0) {
Iterator var6 = suppressedExceptions.iterator();

while(var6.hasNext()) {
IOException e = (IOException)var6.next();
Log.w("MultiDex", "Exception in makeDexElement", e);
}

Field suppressedExceptionsField = MultiDex.findField(dexPathList, "dexElementsSuppressedExceptions");
IOException[] dexElementsSuppressedExceptions = (IOException[])((IOException[])suppressedExceptionsField.get(dexPathList));
if (dexElementsSuppressedExceptions == null) {
dexElementsSuppressedExceptions = (IOException[])suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
} else {
IOException[] combined = new IOException[suppressedExceptions.size() + dexElementsSuppressedExceptions.length];
suppressedExceptions.toArray(combined);
System.arraycopy(dexElementsSuppressedExceptions, 0, combined, suppressedExceptions.size(), dexElementsSuppressedExceptions.length);
dexElementsSuppressedExceptions = combined;
}

suppressedExceptionsField.set(dexPathList, dexElementsSuppressedExceptions);
IOException exception = new IOException("I/O exception during makeDexElement");
exception.initCause((Throwable)suppressedExceptions.get(0));
throw exception;
}
}

在install中,主要就是通过反射机制,从ClassLoader中获取一个叫做pathlist的Field,再将dex包们通过makeElements()方法产生一个Element数组,然后调用expandFieldArray()方法,按字面意思应该是对数组进行了扩展。最后,查看是否有异常需要抛出。

在Android中,类加载器有两种,DexClassLoader和PathClassLoader,这两个加载器都是继承自BaseClassLoader的。pathlist就是定义在BaseClassLoader中的:

1
2
3
4
5
6
7
8
/**
* Base class for common functionality between various dex-based
* {@link ClassLoader} implementations.
*/
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;

}

再看看这个DexPathList具体是什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* A pair of lists of entries, associated with a {@code ClassLoader}.
* One of the lists is a dex/resource path &mdash; typically referred
* to as a "class path" &mdash; list, and the other names directories
* containing native code libraries. Class path entries may be any of:
* a {@code .jar} or {@code .zip} file containing an optional
* top-level {@code classes.dex} file as well as arbitrary resources,
* or a plain {@code .dex} file (with no possibility of associated
* resources).
*
* <p>This class also contains methods to use these lists to look up
* classes and resources.</p>
*/
/*package*/ final class DexPathList {
}

所以他相当于是封装了一个存放dex路径的list。回到install方法中,按步骤看一下。首先是makeElements():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 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 (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory,
suppressedExceptions);
}

看注释就知道,这里也是通过反射,调用了前面DexPathList的makeDexElements()方法,那就来看看吧:

1
2
3
4
5
6
7
8
9
/**
* Makes an array of dex/resource path elements, one per element of
* the given array.
*/
private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
List<IOException> suppressedExceptions,
ClassLoader loader) {
return makeElements(files, optimizedDirectory, suppressedExceptions, false, loader);
}

创建了一个dex/resource的数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
private static Element[] makeElements(List<File> files, File optimizedDirectory,
List<IOException> suppressedExceptions,
boolean ignoreDexFiles,
ClassLoader loader) {
//创建一个Element数组
Element[] elements = new Element[files.size()];
int elementsPos = 0;
/*
* Open all files and load the (direct or contained) dex files
* up front.
*/
//遍历 打开所有的文件
for (File file : files) {
File zip = null;
File dir = new File("");
DexFile dex = null;
String path = file.getPath();
String name = file.getName();
//这个zipSeparator的值为 !/
if (path.contains(zipSeparator)) {
String split[] = path.split(zipSeparator, 2);
zip = new File(split[0]);
dir = new File(split[1]);
} else if (file.isDirectory()) {
// We support directories for looking up resources and native libraries.
// Looking up resources in directories is useful for running libcore tests.
elements[elementsPos++] = new Element(file, true, null, null);
} else if (file.isFile()) {
//根据前面的源码 这里的ignoreDexFiles为false,而DEX_SUFFIX的值为 .dex
if (!ignoreDexFiles && name.endsWith(DEX_SUFFIX)) {
// Raw dex file (not inside a zip/jar).
try {
//找到了dex文件,尝试加载
dex = loadDexFile(file, optimizedDirectory, loader, elements);
} catch (IOException suppressed) {
System.logE("Unable to load dex file: " + file, suppressed);
suppressedExceptions.add(suppressed);
}
} else {
zip = file;

if (!ignoreDexFiles) {
try {
dex = loadDexFile(file, optimizedDirectory, loader, elements);
} catch (IOException suppressed) {
/*
* IOException might get thrown "legitimately" by the DexFile constructor if
* the zip file turns out to be resource-only (that is, no classes.dex file
* in it).
* Let dex == null and hang on to the exception to add to the tea-leaves for
* when findClass returns null.
*/
suppressedExceptions.add(suppressed);
}
}
}
} else {
System.logW("ClassLoader referenced unknown path: " + file);
}
//加载完毕 将获取到的zip或者dex先封装为一个Element对象,再将这个Element对象放入前面的Element数组中
if ((zip != null) || (dex != null)) {
elements[elementsPos++] = new Element(dir, false, zip, dex);
}
}
if (elementsPos != elements.length) {
elements = Arrays.copyOf(elements, elementsPos);
}
return elements;
}

然后是expandFieldArray()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 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 {
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);
}

结合注释就知道,这里是用一个包含了原来的elements的新的数组,加入了新的elemets。这里也是反射,实际发生了变化的是DexPathList的数组:

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

总结一下,具体的安装过程就是将Dex包们封装为Element对象,再将这些Element对象插入到原来的Element数组中,充当为新的数组,而这整个过程都是通过反射来做的,具体的执行者就是DexPathList

总结

上面都是一些工具方法,主要用途就是通过反射,获取对象,调用指定方法。
dex合并就到这里了,总结一下合并的步骤:

  1. 检查当前系统能够自动合并包,以及如果不支持的话系统版本是否支持multidex。
  2. 清除旧的备用缓存目录,之所以叫备用是因为在创建目录时创建失败才会创建备用目录,这个备用目录是需要删除的。
  3. 创建新的缓存目录,然后提取从dex文件至缓存目录中。
  4. 安装缓存目录中断dex文件,这时根据当前系统的版本有不同的安装方法,但大体上逻辑是一样的。