1.MultiDex原理
在andriod4.4及以下采用的是Dalvik虚拟机,默认只支持单个classes.dex文件,在单个Dex文件的方法数超过65536个就会报错。
trouble writing output:
Too many field references: 131000; max is 65536.
You may try using --multi-dex option.
我们从MemberIdSection.java文件的orderItems方法中可以看到此限制。
/** The largest addressable member is 0xffff, in the dex spec as field@CCCC or meth@CCCC. */
private static final int MAX_MEMBERS = 0x10000;
/** {@inheritDoc} */
@Override
protected void orderItems() {
int idx = 0;
if (items().size() > MAX_MEMBERS) {
throw new DexException(tooManyMembersMessage());
}
for (Object i : items()) {
((MemberIdItem) i).setIndex(idx);
idx++;
}
}
private String tooManyMembersMessage() {
Map<String, AtomicInteger> membersByPackage = new TreeMap<String, AtomicInteger>();
for (Object member : items()) {
String packageName = ((MemberIdItem) member).getDefiningClass().getPackageName();
AtomicInteger count = membersByPackage.get(packageName);
if (count == null) {
count = new AtomicInteger();
membersByPackage.put(packageName, count);
}
count.incrementAndGet();
}
Formatter formatter = new Formatter();
String memberType = this instanceof MethodIdsSection ? "methods" : "fields";
formatter.format("Too many %s: %d; max is %d. By package:",
memberType, items().size(), MAX_MEMBERS);
for (Map.Entry<String, AtomicInteger> entry : membersByPackage.entrySet()) {
formatter.format("%n%6d %s", entry.getValue().get(), entry.getKey());
}
return formatter.toString();
}
以上代码来自于andriod4.2版本,遇到单个dex文件方法数超过65536个的情况,我们通常会使用MultiDex方案来对Dex文件进行拆分,使得单个Dex文件方法数不超过限制。
MultiDex会把Dex文件拆分成多个,并取名为classes.dex,classes2.dex,classes3.dex...classesN.dex,再一同打包进APK中。
当APK被安装时,Dalvik虚拟机会在dexopt阶段对Classes.dex进行优化,并且将dex文件构造成Element对象,添加到DexPathList类中的dexElements数组中。而剩下的classes2.dex,classes3.dex...classesN.dex将由MultiDex库负责从APK中解压出来,然后对每个dex文件进行Zip压缩,生成classesN.zip文件。接着,对每个Zip文件进行优化,生成classesN.zip.odex文件。再构造成Element对象,添加进DexPathList类中的dexElements数组,从而突破单个Dex文件方法数不能超过65536个的限制。
这里采用的策略跟QQ空间超级补丁使用的热修复方案类似,只不过QQ空间超级补丁是把补丁dex文件插入到dexElements数组前面,让Classloader在findClass时先找到补丁类。而MultiDex方案中只需将classes.dex构造的Element与classesN.dex构造的Element合并,这样Classloader就能加载classesN.dex中的类。
2.BoostMultiDex是干嘛的?
MutilDex方案看似很完美,但是也存在一些问题,比如:
1.可能报Could not find class异常
2.首次启动可能会报ANR
造成这些问题的原因无非就是secondary dex(classesN.dex)加载过慢,我们来分析一下使用MultiDex方案加载secondary dex的过程中存在哪些耗时操作。
从上图可以看见,MultiDex首先会从APK文件中解压secondary dex,然后进行zip压缩,最后再对每个zip文件进行优化操作。这一系列过程本身就非常的耗时,而且大多都是I/O操作,并且随着dex数量的增多,耗时会不断的增加。这样首次启动APP就很有可能造成上述问题,而BoostMultiDex的出现就是为了解决MultiDex方案存在的问题。
3.实现原理
那么BoostMultiDex是如何解决MultiDex方案存在的问题呢?BoostMultiDex在首次启动APP时会直接从APK中读取原始dex文件的数据进行加载,这样就不会影响APP的启动,然后再单独开启一个后台进程,进行opt工作。
除了开启一个后台进程进行opt,还有其他地方可以优化的吗?
如果将每个secondary dex先进行zip压缩再优化,那么就会多出把原始Dex压缩为Zip格式的时间,还会多出从Zip中解压原Dex再进行优化的时间。那么是否能够不进行Zip压缩操作,直接将原始dex文件进行opt操作?肯定是可行的,只是根据抖音官方给出的数据,不进行Zip压缩,Dex文件磁盘占用空间比进行Zip压缩时增加了一倍多,但是整体耗时会减少40%左右。收益还是很可观的,如果在磁盘空间充裕的时候,这种方案还是非常可取的。
还有一种情况,就是APP启动以后,后台进程开始进行优化操作,这时候用户重启了APP,由于优化尚未完成,还未生成odex文件,那么就必须再一次从APK中解压secondary dex文件,并且读取数据进行加载。从APK中解压文件肯定存在耗时,所以在第一次从APK中读取dex数据以后可以在本地生成dex文件,如果opt操作尚未完成,就不需要再次从APK中去解压读取数据,而是直接读取已经存在的dex文件,从而避免APK解压的时间。(注意这里指的从APK解压并不是像在Windows那样把文件解压到磁盘,而是从APK中得到某个文件的InputStream然后保存为字节数组)
那么BoostMultiDex加载Dex的策略就可以细分为以下4点:
1.当APP第一次安装的时候,这时候会直接从APK中读取dex源文件进行加载,并且唤起优化进程开始OPT操作,优化进程会将Apk中的dex文件提取出来保存在磁盘并且生成odex文件。
2.APP第二次启动时,如果opt尚未完成,但是已经生成了dex文件,那么就直接从dex文件中读取数据进行加载,避免了从APK中进行解压的操作。在真实的环境中,有可能出现两个文件进度不一样的情况,比如classes2.dex已经完成了opt操作生成了classes2.odex文件,而classes3.dex还未完成opt,没有生成classes3.odex,那么在进行加载的时候,会加载classes2.odex与classes3.dex,而优化进程会接着对classes3.dex进行优化。
3.APP第二次启动时,如果opt已经全部完成,那么就直接加载odex文件。
4.还有一种情况,就是当前磁盘空间不够或者遇见某些特殊机型(android4.4却使用了ART虚拟机,阿里云OS机型),那么就会从APK中解压dex文件,然后压缩成zip文件,再进行opt操作生成odex文件。
细心的朋友肯定发现了一个问题,就是两个进程同时对dex文件进行操作,是否存在读写一致性问题呢?肯定是存在的,一般的解决方案是采用文件锁互斥的方式,MultiDex中也是采用这种方案来避免一个进程在加载Dex,另外一个进程在操作Dex的情况。但是这就有可能导致主进程被阻塞的情况,例如在官方文档里面提到了这样一个场景:
用户打开APP以后,唤起了OPT进程,但是由于某些原因杀死了主进程(andriod5.0以上才会杀死进程组),这时候OPT进程已经开始执行OPT操作,当主进程再次启动时,由于获取不到文件锁而导致阻塞,从而出现黑屏。
显然这种使用单文件锁的方式不可取,所以BoostMultiDex采用了双文件锁的方式。
1.互斥锁:互斥锁保证加载Dex和处理Dex的过程是互斥的。
2.准备锁:准备锁用来通知OPT进程,主进程需要加载Dex。
从官方给出的图中我们可以看到,主进程在唤起OPT进程以后,由于某种原因被杀死了,接着OPT进程持有了互斥锁保证同一时间其他进程没办法对Dex进行操作,接着开始OPT,在执行完第一个Dex文件的OPT操作以后,尝试获取准备锁,如果获取准备锁成功,说明没有其他进程占用准备锁,换句话说就是没有进程要加载dex文件,那么就会马上释放准备锁,以便其他进程能够获取到,接着继续对第二个Dex文件进行OPT操作,就在这时候,主进程启动了,主进程获取了准备锁,并且尝试获取互斥锁,这个时候OPT进程占用的互斥锁,肯定会获取失败,主进程就会被阻塞,当OPT进程对第二个Dex文件操作完成以后,会尝试获取准备锁,因为这个时候准备锁已经被主进程获取到,所以OPT进程获取准备锁失败,OPT进程也就知道了主进程需要加载Dex文件,于是OPT进程马上释放互斥锁并且退出。OPT进程释放了互斥锁,主进程就能够获取到互斥锁,有了互斥锁主进程就会释放准备锁并且开始加载Dex,加载完成以后,主进程释放互斥锁并再次唤醒OPT进程进行优化。
从上面的情况中我们可以发现,就算是采用双文件锁的方式,主进程也是有可能被阻塞的,但是使用双文件锁的好处在于OPT进程能够主动感知到主进程有加载Dex文件的需要,从而中断对Dex文件的处理,释放互斥锁。主进程不用等待OPT进程处理完所有Dex,只需要等待OPT进程完成最近一个Dex文件的处理。
总结一下主进程和OPT进程的操作步骤:
OPT进程:
1.获取互斥锁。
2.执行OPT。
3.非阻塞地尝试获取准备锁。
4.如果没有获取到准备锁,则释放互斥锁,并退出OPT进程。
5.如果获取到准备锁,则进行执行下一个文件的OPT操作。
6.完成所有文件的OPT操作,释放互斥锁,退出进程。
主进程:
1.阻塞获取准备锁。
2.阻塞获取互斥锁。
3.释放准备锁。
4.完成加载操作。
5.释放互斥锁。
到此为止BoostMultiDex的原理就介绍完了,更详细的原理以及遇到的问题大家可以去看抖音官方给出的文档。
4.BoostMultiDex的使用
1.在build.gradle的dependencies中添加依赖:
dependencies {
... ...
implementation 'com.bytedance.boost_multidex:boost_multidex:${ARTIFACT_VERSION}'
}
2.与官方MultiDex类似,在Application.attachBaseContext的最前面进行初始化即可:
public class YourApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
BoostMultiDex.install(base);
... ...
}
5.BoostMultiDex实现分析
5.1文件结构分析
BoostMultiDex框架主要文件:
1.BoostMultiDex:框架的门面类,所有的操作从这里开始。
2.BoostNative:定义了要使用的native方法。
3.DexHolder:Dex文件抽象类,定义了对Dex文件的操作。
4.ApkBuffer:DexHolder的子类,实现了从Apk加载的操作。
5.DexBuffer:DexHolder的子类,实现了从Dex文件加载的操作。
6.DexOpt:DexHolder的子类,实现了从Dex的ODex文件加载的操作。
7.ZipOpt:DexHolder的子类,实现了从Zip的ODex文件加载的操作。
8.DexInstallProcessor:里面定义了主进程和OPT进程分别执行的方法。
9.DexLoader:里面定义了不同android版本的Element类构造器,还有将Element添加进dexElements数组的实现。
10.Locker:文件锁的实现。
11.OptimizeService:继承与IntentService,在OPT进程中执行OPT操作。
12.Utility:工具类。
5.2代码分析
BoostMultiDex的整个流程无非就做两件事。
1.加载dex文件并构造成Element对象。
2.将Element对象添加的dexElements数组中。
有了大方向,我们再具体来分析每一步的实现。
我们在前文中说了4中加载策略。其中第一种直接从APK中读取dex文件进行加载。这里有两个问题需要解决:
1、如何从APK中读取Dex文件?
2、如何让Dalvik虚拟机直接执行没有优化过的Dex?
5.2.1如何从APK中读取Dex文件?
BoostMultiDex采用的方式是通过APK文件构造一个ZipFile对象,使用ZipFile的getEntry()去得到一个dex文件的对应的ZipEntry对象,然后获取dex文件的输入流,将dex文件的数据存放在字节数组中。
private List<DexHolder> obtainDexObjectList(File apkFile,File rootDir,File dexDir,File odexDir,File zipDir,Result result)throws IOException{
...
int secondaryNumber = 2;
//1.通过APK文件构造ZipFile对象
final ZipFile apkZipFile = new ZipFile(apkFile);
ZipEntry dexEntry;
//2.循环从APK中读取classes2.dex,classes3.dex...构建ZipEntry对象
while((dexEntry = apkZipFile.getEntry(Constants.DEX_PREFIX + secondaryNumber + Constants.DEX_SUFFIX)) != null){
File dexFile = new File(dexDir,secondaryNumber+Constants.DEX_SUFFIX);
File optDexFile = new File(odexDir,secondaryNumber+Constants.ODEX_SUFFIX);
//是否支持快速加载
if(BoostNative.isSupportFastLoad()){
//支持快速加载的情况
if(Utility.isBetterUseApkBuf()){
//3.从APK中读取dex文件保存为字节数组
byte[] bytes = obtainEntryBytesInApk(apkZipFile,dexEntry);
//使用字节数组构建ApkBuffer对象,并保存到dexHoldersList列表中
dexHoldersList.add(new DexHolder.ApkBuffer(secondaryNumber,bytes,dexFile,optDexFile));
}else{
//从APK中读取dex文件并保存为File
File validDexFile = obtainEntryFileInApk(apkZipFile,dexEntry,dexFile);
//使用File构建DexBuffer对象,并保存到dexHoldersList列表中
dexHoldersList.add(DexHolder.obtainValidDexBuffer(mPreferences,secondaryNumber,validDexFile,optDexFile));
}
}else{
//不支持快速加载的情况
if(Environment.getDataDirectory().getFreeSpace() > Constants.SPACE_THRESHOLD){
//构建DexOpt对象,并保存到dexHoldersList列表中
dexHoldersList.add(DexHolder.obtainValidForceDexOpt(mPreferences,secondaryNumber,dexFile,optDexFile,apkZipFile,dexEntry));
}else{
File zipFile = new File(zipDir,secondaryNumber + Constants.ZIP_SUFFIX);
File zipOptFile = new File(zipDir,secondaryNumber + Constants.ODEX_SUFFIX);
//构建ZipOpt,并保存到dexHoldersList列表中
dexHoldersList.add(DexHolder.obtainValidZipDex(mPreferences,secondaryNumber,zipFile,zipOptFile,apkZipFile,dexEntry));
}
}
secondaryNumber++;
}
...
}
private byte[] obtainEntryBytesInApk(ZipFile apkZipFile,ZipEntry dexFileEntry)throws IOException{
return Utility.obtainEntryBytesInZip(apkZipFile,dexFileEntry);
}
static byte[] obtainEntryBytesInZip(ZipFile apkZipFile, ZipEntry dexFileEntry) throws IOException {
IOException suppressedException = null;
//设置尝试次数3
int retriedCount = Constants.MAX_EXTRACT_ATTEMPTS;
while (retriedCount > 0) {
InputStream in = null;
try {
//4.得到dex文件的输入流
in = apkZipFile.getInputStream(dexFileEntry);
return obtainBytesFromInputStream(in);
} catch (IOException e) {
suppressedException = e;
} finally {
closeQuietly(in);
}
retriedCount--;
}
throw suppressedException;
}
static byte[] obtainBytesFromInputStream(InputStream inputStream) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try {
byte[] buffer = new byte[Constants.BUFFER_SIZE];
int length;
while ((length = inputStream.read(buffer)) != -1) {
//5.写入ByteArrayOutputStream
byteArrayOutputStream.write(buffer, 0, length);
}
//6.生成字节数组
return byteArrayOutputStream.toByteArray();
} finally {
closeQuietly(byteArrayOutputStream);
}
}
注释中1->2->3->4->5->6就是从APK中读取dex文件的步骤。
在前面的加载策略中我们说了,如果磁盘空间不够或者遇见特殊机型那么就只能走zip压缩的流程。这里的判断逻辑就是在BoostNative.isSupportFastLoad()方法中实现的。
static synchronized boolean isSupportFastLoad(){
if(!alreadyInit){
checkSupportFastLoad(Result.get());
alreadyInit = true;
}
return supportFastLoadDex;
}
private static void checkSupportFastLoad(Result result){
try{
Method getPropertyMethod = Class.forName("android.os.SystemProperties").getDeclaredMethod("get",String.class,String.class);
//android 4.4及以下采用 dalvik虚拟机
if(Build.VERSION.SDK_INT >= 19){
String vmLibName = (String)getPropertyMethod.invoke(null,"persist.sys.dalvik.vm.lib",null);
result.vmLibName = vmLibName;
Monitor.get().logInfo("VM lib is " + vmLibName);
1.//如果是android4.4版本 但是却使用了ART虚拟机 那么就跳过
if ("libart.so".equals(vmLibName)) {
Monitor.get().logWarning("VM lib is art, skip!");
return;
}
}
//2.判断是否是阿里云的系统,如果是的话跳过
String yunosVersion = (String) getPropertyMethod.invoke(null, "ro.yunos.version", null);
if(yunosVersion != null && !yunosVersion.isEmpty() || new File(Constants.LIB_YUNOS_PATH).exists()){
result.isYunOS = true;
Monitor.get().logWarning("Yun os is " + yunosVersion + ", skip boost!");
return;
}
//调用native方法initialize进行初始化
supportFastLoadDex = initialize(Build.VERSION.SDK_INT,RuntimeException.class);
result.supportFastLoadDex = supportFastLoadDex;
}catch (Throwable tr){
result.addUnFatalThrowable(tr);
Monitor.get().logWarning("Fail to init", tr);
}
}
5.2.2 如何让Dalvik虚拟机直接执行没有优化过的Dex?
通过调用DexFile类中openDexFile方法可以实现直接对Dex文件的加载。
/*
* Open a DEX file based on a {@code byte[]}. The value returned
* is a magic VM cookie. On failure, a RuntimeException is thrown.
*/
native private static int openDexFile(byte[] fileContents);
我们可以看见该方法是一个native方法,并且接收一个byte数组,我们只要从APK中读取Dex文件并保存为byte数组,再调用此方法将byte数组传入,就能够实现直接加载Dex文件。
我们来看一下native层的实现:
/*
* private static int openDexFile(byte[] fileContents) throws IOException
*
* Open a DEX file represented in a byte[], returning a pointer to our
* internal data structure.
*
* The system will only perform "essential" optimizations on the given file.
*
*/
static void Dalvik_dalvik_system_DexFile_openDexFile_bytearray(const u4* args,
JValue* pResult)
{
ArrayObject* fileContentsObj = (ArrayObject*) args[0];
u4 length;
u1* pBytes;
RawDexFile* pRawDexFile;
DexOrJar* pDexOrJar = NULL;
if (fileContentsObj == NULL) {
dvmThrowNullPointerException("fileContents == null");
RETURN_VOID();
}
/* TODO: Avoid making a copy of the array. (note array *is* modified) */
length = fileContentsObj->length;
pBytes = (u1*) malloc(length);
if (pBytes == NULL) {
dvmThrowRuntimeException("unable to allocate DEX memory");
RETURN_VOID();
}
memcpy(pBytes, fileContentsObj->contents, length);
if (dvmRawDexFileOpenArray(pBytes, length, &pRawDexFile) != 0) {
ALOGV("Unable to open in-memory DEX file");
free(pBytes);
dvmThrowRuntimeException("unable to open in-memory DEX file");
RETURN_VOID();
}
ALOGV("Opening in-memory DEX");
pDexOrJar = (DexOrJar*) malloc(sizeof(DexOrJar));
pDexOrJar->isDex = true;
pDexOrJar->pRawDexFile = pRawDexFile;
pDexOrJar->pDexMemory = pBytes;
pDexOrJar->fileName = strdup("<memory>"); // Needs to be free()able.
addToDexFileTable(pDexOrJar);
RETURN_PTR(pDexOrJar);
}
方法最后会返回DexOrJar的地址,通过该地址作为cookie来构造DexFile对象,有了DexFile我们就能调用Element的构造方法创建Element对象。
但是该方法在andriod4.0到4.3都能在DexFile文件中找到定义,在android4.4版本中,DexFile文件去掉了对该方法的定义了。那android4.4版本中该怎么处理呢?好在andriod4.4版本在java层删掉了openDexFile(byte[])方法的定义,但是在native层却是保留了该方法,那么是否可以在native层直接调用Dalvik_dalvik_system_DexFile_openDexFile_bytearray方法呢?很可惜不行,因为该方法是static修饰,并没有被导出,所以会找不到该方法。直接调用不行,还有其他的办法吗?由于该方法是JNI方法,并且通过动态注册到虚拟机,我们就可以找到对应的函数注册表,通过遍历我们就能得到该方法的指针,从而调用该方法。
const DalvikNativeMethod dvm_dalvik_system_DexFile[] = {
{ "openDexFileNative", "(Ljava/lang/String;Ljava/lang/String;I)I",
Dalvik_dalvik_system_DexFile_openDexFileNative },
{ "openDexFile", "([B)I",
Dalvik_dalvik_system_DexFile_openDexFile_bytearray },
{ "closeDexFile", "(I)V",
Dalvik_dalvik_system_DexFile_closeDexFile },
{ "defineClassNative", "(Ljava/lang/String;Ljava/lang/ClassLoader;I)Ljava/lang/Class;",
Dalvik_dalvik_system_DexFile_defineClassNative },
{ "getClassNameList", "(I)[Ljava/lang/String;",
Dalvik_dalvik_system_DexFile_getClassNameList },
{ "isDexOptNeeded", "(Ljava/lang/String;)Z",
Dalvik_dalvik_system_DexFile_isDexOptNeeded },
{ NULL, NULL, NULL },
};
由于dvm_dalvik_system_DexFilep[]需要动态的注册进虚拟机,所以这个符号一定会被导出。
在上一小节我们讲了从APK中读取Dex文件并且保存为byte数组。那么接下来就要调用openDexFile方法将byte数组传入。
/*
* 初始化方法,将需要用到的类、方法、字段保存为全局的
*/
Java_org_zzy_lib_optimize_1boot_BoostNative_initialize(JNIEnv *env, jclass, jint sdkVersion, jclass runtimeExceptionClass){
...
//1
jclass clazz = env->FindClass("dalvik/system/DexFile"); CHECK_EXCEPTION;
sDexFileClazz = static_cast<jclass>(env->NewGlobalRef(clazz));CHECK_EXCEPTION;
sCookieFieldId = env->GetFieldID(sDexFileClazz,"mCookie","I");CHECK_EXCEPTION;
sFileNameFieldId = env->GetFieldID(sDexFileClazz,"mFileName","Ljava/lang/String;");CHECK_EXCEPTION;
sGuardFiledId = env->GetFieldID(sDexFileClazz,"guard","Ldalvik/sytem/CloseGuard;");CHECK_EXCEPTION;
const char* dvm = "libdvm.so";
//2
void* handler = dlopen(dvm,RTLD_NOW);
if(handler == nullptr){
env->ThrowNew(runtimeExceptionClass,"Fail to find dvm");
return JNI_FALSE;
}
//小于android4.4
if(sdkVersion < 19){
//3
sOpenDexFileMethodId = env->GetStaticMethodID(sDexFileClazz,"openDexFile","([B)I");
env->ExceptionClear();
}else{
//android 4.4 com/android/dex/Dex类的构造方法public Dex(byte[] data)
clazz = env->FindClass("com/android/dex/Dex");CHECK_EXCEPTION;
sDexClazz = static_cast<jclass>(env->NewGlobalRef(clazz)); CHECK_EXCEPTION;
sDexConstructor = env->GetMethodID(sDexClazz, "<init>", "([B)V"); CHECK_EXCEPTION;
//检查是否是HTC
sIsSpecHtc = CheckIsSpecHtc();
}
if(sOpenDexFileMethodId == nullptr){
//4
auto* natives_DexFile = (JNINativeMethod *)dlsym(handler,"dvm_dalvik_system_DexFile");
if(natives_DexFile == nullptr){
env->ThrowNew(runtimeExceptionClass, "Fail to find DexFile symbols");
return JNI_FALSE;
}
//5
openDexFileBytes = findOpenDexFileFunc(natives_DexFile,"openDexFile","([B)I");
if(openDexFileBytes == nullptr){
return JNI_FALSE;
}
}
...
return JNI_TRUE;
}
在使用openDexFile方法之前,我们需要找到使用该方法需要的一些数据,所以会先调用initialize方法。
在注释1处我们得到了DexFile类和该类的一些字段(mCookie,mFileName,guard),我们需要在调用完openDexFile方法后返回一个DexFile对象给JAVA层,所以在初始化方法中我们需要先得到这些数据。
注释2处通过dlopen函数加载动态链接库libdvm.so,其中RTLD_NOW的意思是需要在dlopen返回前,解析出所有未定义的符号,如果解析不出来,就会报undefined symbol: xxxx.......。
注释3处进行了sdk版本的判断,如果是小于andriod4.4版本,那么直接获取openDexFile的MethedId,如果不小于android4.4版本,这里会得到Dex类与其构造方法,在这里得到Dex类是为了防止在调用Class.getDex()方法时报错,后面会说到这个错误。
注释4处会判断sOpenDexFileMethodId是否为null,如果为null说明在注释3处并没有得到openDexFile的MethodId,那么我们需要通过dlsym函数得到dvm_dalvik_system_DexFile数组的指针,前面说过需要通过遍历dvm_dalvik_system_DexFile数组得到openDexFile对应的native方法的指针来调用 Dalvik_dalvik_system_DexFile_openDexFile_bytearray函数。
注释5处就是对dvm_dalvik_system_DexFile数组进行遍历,并且得到Dalvik_dalvik_system_DexFile_openDexFile_bytearray函数的指针,通过该指针就可以调用该函数。
Java_org_zzy_lib_optimize_1boot_BoostNative_loadDirectDex(JNIEnv *env, jclass clazz, jstring jFilePath,
jbyteArray jFileContent) {
...
int32_t cookie;
if(sOpenDexFileMethodId != nullptr){
//如果字节数组为空,从文件路径中打开文件得到字节数组
if(jFileContent == nullptr){
uint32_t file_size = 0;
const char *file_path = env->GetStringUTFChars(jFilePath, nullptr);
//通过mmap映射文件
void *ptr = MapFile(file_path,&file_size);
env->ReleaseStringUTFChars(jFilePath,file_path);
if(ptr == nullptr){
ALOGE("fail to map file");
return nullptr;
}
jFileContent = env->NewByteArray(file_size);
//munmap取消ptr指针所指的映射内存起始地址,file_size则是欲取消的内存大小
CHECK_EXCEPTION_AND_EXE_ABORT("fail to new bytes", munmap(ptr, file_size));
//赋值
env->SetByteArrayRegion(jFileContent,0,file_size, static_cast<const jbyte *>(ptr));
munmap(ptr,file_size);
CHECK_EXCEPTION_AND_ABORT("fail to set bytes");
}
//调用静态方法 native private static int openDexFile(byte[] fileContents);
//该方法可以通过byte[]打开一个dex文件,返回vm magic cookie
cookie = env->CallStaticIntMethod(sDexFileClazz,sOpenDexFileMethodId,jFileContent);
CHECK_EXCEPTION_AND_ABORT("fail to call open dex file bytes method");
}else{
//openDexFile方法没找到的情况
uint32_t args[1];
ArrayObject *array_object_ptr;
uint32_t length;
if(jFileContent == nullptr){
//字节数组为空的情况,跟上面的一样,打开文件进行映射,然后读出文件内容拷贝到ArrayObject的contents中
uint32_t file_size = 0;
const char *file_path = env->GetStringUTFChars(jFilePath, nullptr);
//通过mmap映射文件
void *ptr = MapFile(file_path,&file_size);
env->ReleaseStringUTFChars(jFilePath,file_path);
if(ptr == nullptr){
ALOGE("fail to map file");
return nullptr;
}
length = sizeof(ArrayObject) - sizeof(ArrayObject::contents)+file_size;
array_object_ptr = static_cast<ArrayObject *>(malloc(sizeof(ArrayObject) - sizeof(ArrayObject::contents) + length));
if(array_object_ptr == nullptr){
ALOGE("fail to alloc array object");
munmap(ptr, file_size);
return nullptr;
}
array_object_ptr->length = file_size;
//内存拷贝函数,从ptr中拷贝length个字节到array_object_ptr->contents中
memcpy(array_object_ptr->contents,ptr,length);
munmap(ptr, file_size);
}else{
//字节数组不为空,将字节数组拷贝到ArrayObject中的contents中
uint32_t jarray_length = static_cast<uint32_t>(env->GetArrayLength(jFileContent));
uint8_t * jarray_ptr = static_cast<uint8_t *>(env->GetPrimitiveArrayCritical(jFileContent, nullptr));
length = sizeof(ArrayObject) - sizeof(ArrayObject::contents) + jarray_length;
array_object_ptr = static_cast<ArrayObject *>(malloc(sizeof(ArrayObject) - sizeof(ArrayObject::contents) + length));
if (array_object_ptr == nullptr) {
ALOGE("fail to alloc array object for jFileContents");
return nullptr;
}
array_object_ptr->length = jarray_length;
//内存拷贝函数,从jarray_ptr中拷贝length个字节到array_object_ptr->contents中
memcpy(array_object_ptr->contents,jarray_ptr,length);
//释放
env->ReleasePrimitiveArrayCritical(jFileContent,jarray_ptr,0);
}
args[0] = reinterpret_cast<uint32_t>(array_object_ptr);
//调用Dalvik_dalvik_system_DexFile_openDexFile_bytearray方法,返回值保存在cookie中
openDexFileBytes(args,&cookie);
CHECK_EXCEPTION_AND_ABORT("fail to open dex file bytes");
if(sDexClazz != nullptr && sDexConstructor!= nullptr){
//cookie实际上就是DexOrJar
DexOrJar * dexOrJar = reinterpret_cast<DexOrJar *>(cookie);
if(jFileContent == nullptr){
jFileContent = env->NewByteArray(length);
CHECK_EXCEPTION_AND_ABORT("fail to new array of file bytes");
//赋值
env->SetByteArrayRegion(jFileContent,0,length, reinterpret_cast<const jbyte *>(array_object_ptr->contents));
CHECK_EXCEPTION_AND_ABORT("fail to set array of file bytes");
}
//创建一个全局的Dex对象public Dex(byte[] data)
jobject dex_object = env->NewGlobalRef(env->NewObject(sDexClazz,sDexConstructor,jFileContent));
//为pDvmDex->dex_object赋值,防止在getDex方法中抛出异常,dex_boject不为null,就直接返回了,不会执行
//com.android.dex.Dex.create方法,从而防止memMap没有赋值的而造成的异常
if(!sIsSpecHtc){
dexOrJar->pRawDexFile->pDvmDex->dex_object = dex_object;
}else{
dexOrJar->pRawDexFile->pDvmDex->dex_object = dex_object;
dexOrJar->pRawDexFile->pDvmDex->dex_object_htc = dex_object;
}
}
free(array_object_ptr);
}
//创建DexFile对象,该对象是未初始化的
jobject dex_file = env->AllocObject(sDexFileClazz);
//设置mCookie
env->SetIntField(dex_file,sCookieFieldId,cookie);
//设置 mFileName
env->SetObjectField(dex_file,sFileNameFieldId,jFilePath);
//设置guard
env->SetObjectField(dex_file,sGuardFiledId,env->CallStaticObjectMethod(sCloseGuardClazz,sGuardGetMethodId));
return dex_file;
}
Java_org_zzy_lib_optimize_1boot_BoostNative_loadDirectDex函数接收两个参数,一个是jFilePath它是一个String类型表示文件地址,一个是jFileContent它是一个byte[]表示dex的内容。该方法会在两种情况下调用。
第一个种情况是从APK中读取dex文件的内容,以字节数组的方式传入该方法,这时候第一个参数为null。
第二种情况就是从磁盘上的Dex文件加载,这时候第一个参数为Dex文件的地址,第二个参数为null。
所以在该方法中会判断jFileContent是否null,如果不为null则表示第一种情况,如果为null表示第二种情况。
进入该方法后会判断sOpenDexFileMethodId是否为null,如果为null说明从DexFile文件中找不到openDexFile(byte[])方法的定义,那么只能通过native层进行调用。
我们先看不为空的情况,如果不为空,我们就判断是否传入了byte[],如果传入的是byte[]就可以直接调用openDexFile方法,得到DexOrJar的地址。
如果传入的是文件地址,那么就通过open函数打开该文件,得到该文件的文件描述符fd,然后通过mmap函数创建一个新的虚拟内存区域,并将fd指定对象的一个连续chunk映射到这个新的区域。最后再通过SetByteArrayRegion函数赋值给jFileContent,相当于拿到了byte[],拿到byte[]后就可以调用openDexFile方法,得到DexOrJar的地址。这里也就知道了为什么不在JAVA层将File转换为byte数组再传入native层,而是直接传文件地址。以下是MapFile的代码:
static void* MapFile(const char* file_path,uint32_t *out_file_size){
//通过路径打开文件
int fd = TEMP_FAILURE_RETRY(open(file_path,O_RDONLY,S_IRUSR));
if(fd == -1){
ALOGE("fail to open %s", file_path);
return nullptr;
}
uint32_t file_size = static_cast<uint32_t>(lseek(fd,0,SEEK_END));
ALOGV("mapping file size is %zu", file_size);
//mmap函数要求内核创建一个新的虚拟内存区域,并将文件描述符fd指定的对象的一个连续chunk映射到这个新的区域
//PROT_READ表示这个区域内的页面可读,MAP_SHARED表示是一个共享对象
//成功时返回指向映射区域的指针,出错返回MAP_FAILED
void *ptr = mmap(nullptr,file_size,PROT_READ,MAP_SHARED,fd,0);
TEMP_FAILURE_RETRY(close(fd));
if(ptr == MAP_FAILED){
ALOGE("fail to map file %s", file_path);
return nullptr;
}
*out_file_size = file_size;
return ptr;
}
如果sOpenDexFileMethodId为空,我们就需要调用Dalvik_dalvik_system_DexFile_openDexFile_bytearray函数,我们前面在initialize函数中得到了Dalvik_dalvik_system_DexFile_openDexFile_bytearray函数指针,接下来就是要构造传入该函数所需的参数,该方法有两个入参,第一是ArrayObject指针,另外一个是指向DexOrJar的指针。
struct ArrayObject : Object {
uint32_t length;
uint64_t contents[1];
};
ArrayObject的结构如上,我们会把byte数组存放在contents里面,Dalvik_dalvik_system_DexFile_openDexFile_bytearray函数内部会从contents里面读取数据。
如果jFileContent不为空,直接使用memcpy函数将数据拷贝过去。如果jFileContent为空,那么跟上面一样,先使用mmap函数创建新的虚拟内存区域,再使用memcpy函数将数据拷贝到array_object_ptr->contents中。
构造完入参以后就可以使用Dalvik_dalvik_system_DexFile_openDexFile_bytearray函数指针调用该函数。
uint32_t args[1];
int32_t cookie;
args[0] = reinterpret_cast<uint32_t>(array_object_ptr);
//调用Dalvik_dalvik_system_DexFile_openDexFile_bytearray方法,返回值保存在cookie中
openDexFileBytes(args,&cookie);
执行完Dalvik_dalvik_system_DexFile_openDexFile_bytearray函数,结果会保存在cookie里面。
接着就会创建一个DexFile对象,在initialize函数中已经得到了该类的属性,只需要设置进去就可以了,其中mCookie属性就是openDexFile函数返回值。创建完成以后就把DexFile对象返回给Java层。
在Java_org_zzy_lib_optimize_1boot_BoostNative_loadDirectDex函数中,我们看见有一段这样的代码:
if(sDexClazz != nullptr && sDexConstructor!= nullptr){
//cookie实际上就是DexOrJar
DexOrJar * dexOrJar = reinterpret_cast<DexOrJar *>(cookie);
if(jFileContent == nullptr){
jFileContent = env->NewByteArray(length);
CHECK_EXCEPTION_AND_ABORT("fail to new array of file bytes");
//赋值
env->SetByteArrayRegion(jFileContent,0,length, reinterpret_cast<const jbyte *>(array_object_ptr->contents));
CHECK_EXCEPTION_AND_ABORT("fail to set array of file bytes");
}
//创建一个全局的Dex对象public Dex(byte[] data)
jobject dex_object = env->NewGlobalRef(env->NewObject(sDexClazz,sDexConstructor,jFileContent));
//为pDvmDex->dex_object赋值,防止在getDex方法中抛出异常,dex_boject不为null,就直接返回了,不会执行
//com.android.dex.Dex.create方法,从而防止memMap没有赋值的而造成的异常
if(!sIsSpecHtc){
dexOrJar->pRawDexFile->pDvmDex->dex_object = dex_object;
}else{
dexOrJar->pRawDexFile->pDvmDex->dex_object = dex_object;
dexOrJar->pRawDexFile->pDvmDex->dex_object_htc = dex_object;
}
}
代码判断了sDexClazz与sDexConstructor是否为null,这两个值是在initialize函数中获取的,当时的判断是sdk版本不小于19,那么就会进行获取,这段代码的用途在于给dex_object赋值,为什么要给dex_object赋值呢?因为在使用Gson的时候,会调用到Class.getDex()方法,该方法内部会调用com/android/dex/Dex.create()方法,create方法需要一个memMap参数,这个参数只会在ODex情况下赋值,而我们跳过了ODEX,直接加载的Dex,所以该参数就没用进行赋值,使用的时候就会报错。在执行create()方法之前会判断dex_object是否为空,如果不为空就直接返回了,不会执行create()方法,也就不会报错,所以这就是给dex_object赋值的原因。更详细的说明请看官方文档。
5.2.3 构造Element
在得到DexFile对象以后,我们就可以开始构建Element对象,然后添加进dexElements数组。
void install(ClassLoader loader, List<DexHolder> dexHolderList, SharedPreferences preferences) throws Exception {
//得到BaseDexClassLoader中的pathList属性
Field pathListField = Utility.findFieldRecursively(loader.getClass(), "pathList");
Object dexPathList = pathListField.get(loader);
//通过secondary dex构建Elements[]
Object[] elements = makeDexElements(dexHolderList, preferences);
//合并Classes.dex与secondary dex构建的Elements数组
Utility.expandFieldArray(dexPathList, "dexElements", elements);
}
首先从BaseDexClassLoader类中得到pathList字段,并获得该字段的对象DexPathList,然后构建将所有的DexFile构建成Elements数组,最后将我们构建的Elements数组与DexPathList中dexELements数组进行合并。
private Object[] makeDexElements(List<DexHolder> dexHolderList, SharedPreferences preferences) throws Exception {
ArrayList<Object> elements = new ArrayList<>();
for (int i = 0; i < dexHolderList.size(); ++i) {
DexHolder dexHolder = dexHolderList.get(i);
//构造Element对象
Object element = dexHolder.toDexListElement(mElementConstructor);
while (element == null && dexHolder != null) {
Monitor.get().logWarning("Load faster dex in holder " + dexHolder.toString());
dexHolder = dexHolder.toFasterHolder(preferences);
if (dexHolder != null) {
element = dexHolder.toDexListElement(mElementConstructor);
}
}
if (element != null) {
Monitor.get().logInfo("Load dex in holder " + dexHolder.toString());
dexHolderList.set(i, dexHolder);
elements.add(element);
} else {
throw new RuntimeException("Fail to load dex, index is " + i);
}
String dexInfo = dexHolder.toString();
Result.get().addDexInfo(dexInfo);
Monitor.get().logInfo("Add info: " + dexInfo);
}
return elements.toArray();
}
protected Object toDexListElement(DexLoader.ElementConstructor elementConstructor)throws Exception{
Object dexFile = toDexFile();
return dexFile == null ? null : elementConstructor.newInstance(mFile,dexFile);
}
构造Element对象主要是通过toDexListElement方法实现,该方法内部通过elementConstructor.newInstance(mFile,dexFile)完成Element的构建,这里的elementConstructor就是Element的构造方法。由于Element的构造方法在android4.0-4.4版本除了android4.0与android4.1相同外,其余都不相同,所以这里对每个版本的构造方法进行了单独的定义和调用。
/**
* android 4.0
*/
private static class ICSElementConstructor implements ElementConstructor{
private final Constructor<?> mConstructor;
ICSElementConstructor(Class<?> elementClass)throws SecurityException,NoSuchMethodException{
mConstructor = Utility.findConstructor(elementClass,File.class, ZipFile.class,DexFile.class);
mConstructor.setAccessible(true);
}
@Override
public Object newInstance(File file, Object dex) throws IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException, IOException {
return mConstructor.newInstance(file,null,dex);
}
}
/**
* android 4.2.2_r1
* Applies for some intermediate JB (MR1.1).
* <p>
* See Change-Id: I1a5b5d03572601707e1fb1fd4424c1ae2fd2217d
*/
private static class JBMR11ElementConstructor implements ElementConstructor{
private final Constructor<?> mConstructor;
JBMR11ElementConstructor(Class<?> elementClass)throws SecurityException,NoSuchMethodException{
mConstructor = Utility.findConstructor(elementClass,File.class, File.class,DexFile.class);
mConstructor.setAccessible(true);
}
@Override
public Object newInstance(File file, Object dex) throws IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException, IOException {
return mConstructor.newInstance(file,null,dex);
}
}
/**
* android 4.3_r1
* Applies for latest JB (MR2).
* <p>
* See Change-Id: Iec4dca2244db9c9c793ac157e258fd61557a7a5d
*/
private static class JBMR2ElementConstructor implements ElementConstructor{
private final Constructor<?> mConstructor;
JBMR2ElementConstructor(Class<?> elementClass)throws SecurityException,NoSuchMethodException{
mConstructor = Utility.findConstructor(elementClass,File.class, boolean.class, File.class, DexFile.class;
mConstructor.setAccessible(true);
}
@Override
public Object newInstance(File file, Object dex) throws IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException, IOException {
return mConstructor.newInstance(file, false, null, dex);
}
}
/**
* android 4.4
*/
private static class KKElementConstructor implements ElementConstructor{
private final Constructor<?> mConstructor;
KKElementConstructor(Class<?> elementClass)throws SecurityException,NoSuchMethodException{
mConstructor = Utility.findConstructor(elementClass,File.class,boolean.class,File.class, DexFile.class);
mConstructor.setAccessible(true);
}
@Override
public Object newInstance(File file, Object dex) throws IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException, IOException {
return mConstructor.newInstance(file,false,null,dex);
}
}
最后将Elements数组进行合并。
/**
* 合并两个数组到新数组
*
* @param instance DexPathList实例
* @param fieldName 需要反射的字段名称(dexElements)
* @param extraElements elements数组
*/
static void expandFieldArray(Object instance, String fieldName, Object[] extraElements) throws NoSuchFieldException,
IllegalArgumentException, IllegalAccessException {
Field field = findField(instance.getClass(), fieldName);
if (!field.isAccessible()) {
field.setAccessible(true);
}
Object[] original = (Object[]) field.get(instance);
Object[] combined = (Object[]) Array.newInstance(original.getClass().getComponentType(),
original.length + extraElements.length);
//这里不是热修复,所以只要按顺序插入Element就行
System.arraycopy(original, 0, combined, 0, original.length);
System.arraycopy(extraElements, 0, combined, original.length, extraElements.length);
field.set(instance, combined);
}
5.2.4 如何加载ODex文件?
前面介绍了如何从APK中加载和从Dex文件加载,都是使用了自定义的native方法loadDirectDex,在其中调用openDexFile(byte[])函数。那么如何加载ODex文件呢?
static class DexOpt extends DexHolder{
private int mIndex;
private File mOptFile;
private boolean mForceOpt;
@Override
Object toDexFile() {
try {
//加载ODex文件
return DexFile.loadDex(mFile.getPath(),mOptFile.getPath(),0);
}catch (IOException e1){
Monitor.get().logError("Fail to load dex file first time", e1);
try {
if(mForceOpt){
return DexFile.loadDex(mFile.getPath(),mOptFile.getPath(),0);
}else{
return BoostNative.loadDirectDex(mFile.getPath(),null);
}
}catch (IOException e2){
Monitor.get().logError("Fail to load dex file", e2);
throw new RuntimeException(e2);
}
}
}
}
static class ZipOpt extends DexHolder {
private int mIndex;
private File mOptFile;
@Override
Object toDexFile() {
try {
//加载ODex文件
return DexFile.loadDex(mFile.getPath(), mOptFile.getPath(), 0);
} catch (IOException e) {
Monitor.get().logError("Fail to load dex file");
throw new RuntimeException(e);
}
}
}
我们可以看到,都是采用了DexFile.loadDex直接进行加载,loadDex方法会创建并返回一个DexFile对象。
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);
}
/*
* DexFile构造方法
*/
private DexFile(String sourceName, String outputName, int flags) throws IOException {
if (outputName != null) {
try {
String parent = new File(outputName).getParent();
if (Libcore.os.getuid() != Libcore.os.stat(parent).st_uid) {
throw new IllegalArgumentException("Optimized data directory " + parent
+ " is not owned by the current user. Shared storage cannot protect"
+ " your application from code injection attacks.");
}
} catch (ErrnoException ignored) {
// assume we'll fail with a more contextual error later
}
}
mCookie = openDexFile(sourceName, outputName, flags);
mFileName = sourceName;
guard.open("close");
//System.out.println("DEX FILE cookie is " + mCookie);
}
而在DexFile的构造方法中又调用了openDexFile方法,看到最后几行是不是很面熟,我们在前面通过调用openDexFile(byte[])方法得到了DexOrJar的地址,将它赋值给mCookie,而这里是使用openDexFile(String , String , int )方法得到DexOrJar的地址赋值给mCookie,那么为什么我们前面不这样干呢?因为如果调用openDexFile(String , String , int )方法会进行OPT操作,我们使用openDexFile(byte[])方法的目的不就是为了不进行OPT操作,而直接加载Dex的内容吗?
/*
* Open a DEX file. The value returned is a magic VM cookie. On
* failure, an IOException is thrown.
*/
native private static int openDexFile(String sourceName, String outputName,
int flags) throws IOException;
static void Dalvik_dalvik_system_DexFile_openDexFile(const u4*args,
JValue*pResult) {
StringObject * sourceNameObj = (StringObject *) args[0];
StringObject * outputNameObj = (StringObject *) args[1];
DexOrJar * pDexOrJar = NULL;
JarFile * pJarFile;
RawDexFile * pRawDexFile;
char*sourceName;
char*outputName;
if (sourceNameObj == NULL) {
dvmThrowNullPointerException("sourceName == null");
RETURN_VOID();
}
sourceName = dvmCreateCstrFromString(sourceNameObj);
if (outputNameObj != NULL)
outputName = dvmCreateCstrFromString(outputNameObj);
else
outputName = NULL;
/*
* We have to deal with the possibility that somebody might try to
* open one of our bootstrap class DEX files. The set of dependencies
* will be different, and hence the results of optimization might be
* different, which means we'd actually need to have two versions of
* the optimized DEX: one that only knows about part of the boot class
* path, and one that knows about everything in it. The latter might
* optimize field/method accesses based on a class that appeared later
* in the class path.
*
* We can't let the user-defined class loader open it and start using
* the classes, since the optimized form of the code skips some of
* the method and field resolution that we would ordinarily do, and
* we'd have the wrong semantics.
*
* We have to reject attempts to manually open a DEX file from the boot
* class path. The easiest way to do this is by filename, which works
* out because variations in name (e.g. "/system/framework/./ext.jar")
* result in us hitting a different dalvik-cache entry. It's also fine
* if the caller specifies their own output file.
*/
if (dvmClassPathContains(gDvm.bootClassPath, sourceName)) {
ALOGW("Refusing to reopen boot DEX '%s'", sourceName);
dvmThrowIOException(
"Re-opening BOOTCLASSPATH DEX files is not allowed");
free(sourceName);
free(outputName);
RETURN_VOID();
}
/*
* Try to open it directly as a DEX if the name ends with ".dex".
* If that fails (or isn't tried in the first place), try it as a
* Zip with a "classes.dex" inside.
*/
if (hasDexExtension(sourceName)
&& dvmRawDexFileOpen(sourceName, outputName, & pRawDexFile,false) ==0){
ALOGV("Opening DEX file '%s' (DEX)", sourceName);
pDexOrJar = (DexOrJar *) malloc(sizeof(DexOrJar));
pDexOrJar -> isDex = true;
pDexOrJar -> pRawDexFile = pRawDexFile;
pDexOrJar -> pDexMemory = NULL;
} else if (dvmJarFileOpen(sourceName, outputName, & pJarFile,false) ==0){
ALOGV("Opening DEX file '%s' (Jar)", sourceName);
pDexOrJar = (DexOrJar *) malloc(sizeof(DexOrJar));
pDexOrJar -> isDex = false;
pDexOrJar -> pJarFile = pJarFile;
pDexOrJar -> pDexMemory = NULL;
} else{
ALOGV("Unable to open DEX file '%s'", sourceName);
dvmThrowIOException("unable to open DEX file");
}
if (pDexOrJar != NULL) {
pDexOrJar -> fileName = sourceName;
addToDexFileTable(pDexOrJar);
} else {
free(sourceName);
}
free(outputName);
RETURN_PTR(pDexOrJar);
}
从代码中我们可以看见,如果存在dex文件,那么就会执行dvmRawDexFileOpen函数,在该函数中如果有ODex文件那么就打开该文件,没有ODex就进行OPT操作生成ODex文件,然后给pRawDexFile赋值。接着就会构造DexOrJar对象返回给Java层。
int dvmRawDexFileOpen(const char*fileName, const char*odexOutputName,
RawDexFile**ppRawDexFile, bool isBootstrap) {
/*
* TODO: This duplicates a lot of code from dvmJarFileOpen() in
* JarFile.c. This should be refactored.
*/
DvmDex * pDvmDex = NULL;
char*cachedName = NULL;
int result = -1;
int dexFd = -1;
int optFd = -1;
u4 modTime = 0;
u4 adler32 = 0;
size_t fileSize = 0;
bool newFile = false;
bool locked = false;
dexFd = open(fileName, O_RDONLY);
if (dexFd < 0) goto bail;
/* If we fork/exec into dexopt, don't let it inherit the open fd. */
dvmSetCloseOnExec(dexFd);
if (verifyMagicAndGetAdler32(dexFd, & adler32) <0){
ALOGE("Error with header for %s", fileName);
goto bail;
}
if (getModTimeAndSize(dexFd, & modTime, &fileSize) <0){
ALOGE("Error with stat for %s", fileName);
goto bail;
}
/*
* See if the cached file matches. If so, optFd will become a reference
* to the cached file and will have been seeked to just past the "opt"
* header.
*/
if (odexOutputName == NULL) {
cachedName = dexOptGenerateCacheFileName(fileName, NULL);
if (cachedName == NULL)
goto bail;
} else {
cachedName = strdup(odexOutputName);
}
ALOGV("dvmRawDexFileOpen: Checking cache for %s (%s)",
fileName, cachedName);
//1
optFd = dvmOpenCachedDexFile(fileName, cachedName, modTime,
adler32, isBootstrap, & newFile, /*createIfMissing=*/true);
if (optFd < 0) {
ALOGI("Unable to open or create cache for %s (%s)",
fileName, cachedName);
goto bail;
}
locked = true;
/*
* If optFd points to a new file (because there was no cached
* version, or the cached version was stale), generate the
* optimized DEX. The file descriptor returned is still locked,
* and is positioned just past the optimization header.
*/
if (newFile) {
u8 startWhen, copyWhen, endWhen;
bool result;
off_t dexOffset;
dexOffset = lseek(optFd, 0, SEEK_CUR);
result = (dexOffset > 0);
if (result) {
startWhen = dvmGetRelativeTimeUsec();
result = copyFileToFile(optFd, dexFd, fileSize) == 0;
copyWhen = dvmGetRelativeTimeUsec();
}
if (result) {
//2
result = dvmOptimizeDexFile(optFd, dexOffset, fileSize,
fileName, modTime, adler32, isBootstrap);
}
if (!result) {
ALOGE("Unable to extract+optimize DEX from '%s'", fileName);
goto bail;
}
endWhen = dvmGetRelativeTimeUsec();
ALOGD("DEX prep '%s': copy in %dms, rewrite %dms",
fileName,
(int) (copyWhen - startWhen) / 1000,
(int) (endWhen - copyWhen) / 1000);
}
/* 3
* Map the cached version. This immediately rewinds the fd, so it
* doesn't have to be seeked anywhere in particular.
*/
if (dvmDexFileOpenFromFd(optFd, & pDvmDex) !=0){
ALOGI("Unable to map cached %s", fileName);
goto bail;
}
if (locked) {
/* unlock the fd */
if (!dvmUnlockCachedDexFile(optFd)) {
/* uh oh -- this process needs to exit or we'll wedge the system */
ALOGE("Unable to unlock DEX file");
goto bail;
}
locked = false;
}
ALOGV("Successfully opened '%s'", fileName);
*ppRawDexFile = (RawDexFile *) calloc(1, sizeof(RawDexFile));
( * ppRawDexFile)->cacheFileName = cachedName;
( * ppRawDexFile)->pDvmDex = pDvmDex;
cachedName = NULL; // don't free it below
result = 0;
bail:
free(cachedName);
if (dexFd >= 0) {
close(dexFd);
}
if (optFd >= 0) {
if (locked)
(void) dvmUnlockCachedDexFile(optFd);
close(optFd);
}
return result;
}
在dvmRawDexFileOpen()函数中要注意第二个参数odexOutputName,它代表着我们的ODex文件地址。进入该方法以后会判断该值是否为null,为空的话会生成一个,不为空就直接使用strdup函数赋值给cacheName。
在注释1处会调用dvmOpenCachedDexFile()函数打开或创建ODex文件,其中newFile标识ODex文件是新创建的还是旧的。如果是新创建的ODex文件,那么会在注释2处调用dvmOptimizeDexFile()函数进行OPT操作。这里我们讨论的是加载ODex文件,所以肯定是旧的,不会执行OPT操作。接着就在注释3处调用dvmDexFileOpenFromFd()函数,该函数会给pDvmDex赋值,还记得前面我们提到使用Class.getDex()方法出错的问题吗?就是pDvmDex->memMap没有赋值导致的,这个方法内部就会对pDvmDex->memMap赋值,前面由于我们并没有调用dvmRawDexFileOpen()函数,所以会报错。最后再给ppRawDexFile赋值并返回。
当通过openDexFile()方法得到DexOrJar的地址以后赋值给mCookie,然后创建DexFile对象,接着构建Element对象,再插入DexPathList的dexElements数组中,就完成了整个ODex的加载流程。
5.2.5 OPT进程做了些什么?
BoostMultiDex库的AndroidManifest.xml中将OptimizeService定义在:boost_multidex进程中运行。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.bytedance.boost_multidex">
<application>
<service
android:name=".OptimizeService"
android:process=":boost_multidex"
android:exported="false" />
</application>
</manifest>
主进程在启动完成以后,会启动OptimizeService唤起OPT进程。
OPT进程在获取到互斥锁以后,开始执行优化任务,具体来说就是遍历所有Dex,根据当前Dex的type值来决定执行何种优化操作。在BoostMultiDex库中定义了5种type类型。
/**
* 从APK中直接获取字节数组
* */
int LOAD_TYPE_APK_BUF = 0;
/**
* 从Dex文件中得到字节数组
* */
int LOAD_TYPE_DEX_BUF = 1;
/**
* 从Dex文件中ODEX文件
* */
int LOAD_TYPE_DEX_OPT = 2;
/**
* 从ZIP文件优化得到ODEX文件
* */
int LOAD_TYPE_ZIP_OPT = 3;
int LOAD_TYPE_INVALID = 9;
如果当前type值是LOAD_TYPE_APK_BUF,那么需要做的就是在磁盘中创建dex文件,并且通过文件输出流把字节数组写入。成功创建dex文件后,type类型会提升到LOAD_TYPE_DEX_BUF,这时候需要做的就是对dex文件进行OPT操作,生成ODex文件。在成功创建ODex文件以后,会把type类型改为LOAD_TYPE_DEX_OPT,这时候就完成了优化操作。
每次type状态的改变都会被记录在SharedPreferences中,下次启动APP时,发现当前分包的type类型为LOAD_TYPE_DEX_OPT,那么就会直接加载ODex文件,从而避免再次OPT操作。
可以看见dex_cache文件夹用于存放所有的dex文件,odex_cache文件夹用于存放dex对应的odex文件。
还有一种情况就是不支持快速加载的情况,比如android 4.4的系统使用了ART虚拟机,这时候是没有openDexFile(byte[])这个方法的,也就没办法直接加载原始Dex文件。所以就只能先将APK中的dex文件读取出来然后压缩成Zip文件,再使用正常的加载方式DexFile.loadDex()方法,该方法前面讲过了,如果没有生成ODex文件,调用该方法后会创建ODex文件,也就是说遇到特殊机型的情况,主进程第一次加载的时候就要进行OPT操作,没有办法分离开让OPT进程来进行优化。
具体来看一下实现代码:
private void handleOptimize() throws IOException {
if (sAlreadyOpt) {
Monitor.get().logInfo("opt had already done, skip");
return;
}
sAlreadyOpt = true;
Monitor.get().doBeforeHandleOpt();
String keyApkDexNum = Constants.KEY_DEX_NUMBER;
Locker locker = new Locker(new File(mRootDir, Constants.LOCK_INSTALL_FILENAME));
//获取互斥锁
locker.lock();
try {
ApplicationInfo applicationInfo = this.getApplicationInfo();
if (applicationInfo == null) {
throw new RuntimeException("No ApplicationInfo available, i.e. running on a test Context:"
+ " BoostMultiDex support library is disabled.");
}
File apkFile = new File(applicationInfo.sourceDir);
SharedPreferences preferences = this.getSharedPreferences(Constants.PREFS_FILE, Context.MODE_PRIVATE);
//得到总的分包数量
int totalDexNum = preferences.getInt(keyApkDexNum, 0);
//遍历分包
for (int secondaryNumber = 2; secondaryNumber <= totalDexNum; secondaryNumber++) {
//得到当前分包的类型
int type = preferences.getInt(Constants.KEY_DEX_OBJ_TYPE + secondaryNumber, Constants.LOAD_TYPE_APK_BUF);
File dexFile = new File(mDexDir, secondaryNumber + Constants.DEX_SUFFIX);
File optDexFile = new File(mOptDexDir, secondaryNumber + Constants.ODEX_SUFFIX);
DexHolder dexHolder;
if (type == Constants.LOAD_TYPE_APK_BUF) {
ZipFile apkZipFile = new ZipFile(apkFile);
ZipEntry dexFileEntry = apkZipFile.getEntry(Constants.DEX_PREFIX + secondaryNumber + Constants.DEX_SUFFIX);
byte[] bytes = Utility.obtainEntryBytesInZip(apkZipFile, dexFileEntry);
dexHolder = new DexHolder.ApkBuffer(secondaryNumber, bytes, dexFile, optDexFile);
} else if (type == Constants.LOAD_TYPE_DEX_BUF) {
dexHolder = new DexHolder.DexBuffer(secondaryNumber, dexFile, optDexFile);
} else if (type == Constants.LOAD_TYPE_DEX_OPT) {
dexHolder = new DexHolder.DexOpt(secondaryNumber, dexFile, optDexFile, false);
} else if (type == Constants.LOAD_TYPE_ZIP_OPT) {
File zipFile = new File(mZipDir, secondaryNumber + Constants.ZIP_SUFFIX);
File zipOptFile = new File(mZipDir, secondaryNumber + Constants.ODEX_SUFFIX);
dexHolder = new DexHolder.ZipOpt(secondaryNumber, zipFile, zipOptFile);
} else {
dexHolder = null;
}
Monitor.get().logInfo("Process beginning holder " + dexHolder.toString() + ", type: " + type);
DexHolder fasterHolder = dexHolder;
while (fasterHolder != null) {
long freeSpace = Environment.getDataDirectory().getFreeSpace();
if (freeSpace < Constants.SPACE_MIN_THRESHOLD) {
Monitor.get().logWarning("Free space is too small: " + freeSpace
+ ", compare to " + Constants.SPACE_THRESHOLD);
return;
} else {
Monitor.get().logInfo("Free space is enough: " + freeSpace + ", continue...");
}
Monitor.get().logDebug("Process holder, " + fasterHolder);
try {
long start = System.nanoTime();
//进行优化
fasterHolder = fasterHolder.toFasterHolder(preferences);
if (fasterHolder != null) {
long cost = System.nanoTime() - start;
DexHolder.StoreInfo info = fasterHolder.getInfo();
Monitor.get().logDebug("Put info, " + info.index + " file is " + info.file.getPath());
long reducedSpace = Environment.getDataDirectory().getFreeSpace() - freeSpace;
Monitor.get().reportAfterInstall(cost, freeSpace, reducedSpace, fasterHolder.toString());
}
} catch (Throwable tr) {
Monitor.get().logErrorAfterInstall("Fail to be faster", tr);
Result.get().unFatalThrowable.add(tr);
}
//获取准备锁,如果获取不到说明主进程需要进行加载,暂停OPT
Locker prepareLocker = new Locker(new File(mRootDir, Constants.LOCK_PREPARE_FILENAME));
if (prepareLocker.test()) {
prepareLocker.close();
} else {
Monitor.get().logInfo("Other process is waiting for installing");
return;
}
}
}
} catch (Throwable e) {
Monitor.get().logWarning("Failed to install extracted secondary dex files", e);
} finally {
locker.close();
Monitor.get().logInfo("Exit quietly");
stopSelf();
System.exit(0);
}
}
从代码中可以看到,会一个一个遍历分包,并且是一个分包一个分包的优化,所以前文说到,每个分包的进度有可能不一样,因为每个分包在完成优化type进行提升以后会获取一次准备锁,如果获取不到说明主进程需要进行加载,这时候会停止遍历,退出进程。
进行优化的代码主要看DexHolder的toFasterHolder类,我们在前文说了DexHolder是一个抽象类,其中toFasterHolder()就是一个抽象的方法,继承DexHolder的子类都需要实现它。该方法会返回一个DexHolder对象,我们可以看到代码中有一处while循环,判断fasterHolder是否为空,不为空就一直循环,而在循环内又会调用toFasterHolder()方法对fasterHolder对象重新赋值,这样只要fasterHolder不为空,就会一直进行调用toFasterHolder()方法进行优化操作,提升type。
static class ApkBuffer extends DexHolder{
@Override
public DexHolder toFasterHolder(SharedPreferences preferences) {
//将字节数组写入文件
if (Utility.storeBytesToFile(mBytes, mFile)) {
try {
//提升type为LOAD_TYPE_DEX_BUF,返回DexBuffer
return DexHolder.obtainValidDexBuffer(preferences, mIndex, mFile, mOptFile);
} catch (IOException e) {
Monitor.get().logError("fail to get dex buffer", e);
return null;
}
} else {
return null;
}
}
}
static class DexBuffer extends DexHolder{
@Override
public DexHolder toFasterHolder(SharedPreferences preferences) {
try {
//如果不支持快速加载或者不能在naive层直接使用dvmRawDexFileOpen对DEX进行OPT操作,那么使用JAVA层原始的loadDex进行OPT
if (!BoostNative.isSupportFastLoad() || !BoostNative.makeOptDexFile(mFile.getPath(), mOptFile.getPath())) {
Monitor.get().logWarning("Opt dex in origin way");
DexFile.loadDex(mFile.getPath(), mOptFile.getPath(), 0).close();
}
//提升type为LOAD_TYPE_DEX_OPT,返回DexOpt
return obtainValidDexOpt(preferences, mIndex, mFile, mOptFile);
} catch (IOException e) {
Monitor.get().logError("Fail to opt dex finally", e);
return null;
}
}
}
static class DexOpt extends DexHolder{
@Override
public DexHolder toFasterHolder(SharedPreferences preferences) {
return null;
}
}
static class ZipOpt extends DexHolder {
@Override
public DexHolder toFasterHolder(SharedPreferences preferences) {
return null;
}
}
我们可以看到对于ApkBuffer来说,它会将字节数组写入到磁盘的dex文件中,并且提升type类型返回DexBuffer,这样在handleOptimize()方法的while循环里,
fasterHolder的值会更新为DexBuffer对象,并且再次调用DexBuffer的toFasterHolder()方法,这时候需要对dex文件进行Opt操作,这里会进行判断,如果不支持快速加载或者我们自定义的native方法makeOptDexFile()不能进行优化,那么才使用系统的DexFile.loadDex()方法进行优化。优化完成优化返回DexOpt对象,并且提升type类型。接着DexOpt的toFasterHolder()方法会返回null,因为已经生成了ODex文件,已经没有可以优化的了。包括ZipOpt的toFasterHolder()方法也会返回null,同样是因为这个type等级已经完成了优化。
上面提到,在DexBuffer的toFasterHolder()方法中,我们先会使用自定义的native方法makeOptDexFile()进行优化,那么我们为什么不直接使用系统的
DexFile.loadDex()方法来进行OPT操作呢?在BoostMultiDex官方文档中提到,他们在使用DexFile.loadDex()进行OPT时遇到了一个Native Crash,主要的原因是因为在执行loadDex()方法的时候发生了GC,GC要求当前的线程挂起,而loadDex()方法内部调用了JNI方法,却没有将线程状态从RUNNING转换到Native状态,并且在执行OPT操作的时候,处于高强度的I/O操作,那么线程就无法进行挂起,当线程挂起超时,系统就会杀掉这个APP。
所以这里采用自定义Native方法去调用dvmRawDexFileOpen来进行OPT操作,这样线程就会被切换到Native状态,在Native状态时,发生GC是不会要求挂起的。并且这里在单独的OPT进程进行优化操作,也进一步的避免了GC的发生和对主进程的影响。
Java_com_bytedance_boost_1multidex_BoostNative_initialize(JNIEnv *env, jclass, jint sdkVersion, jclass runtimeExceptionClass) {
...
const char* dvm = "libdvm.so";
void* handler = dlopen(dvm, RTLD_NOW);
if (handler == nullptr) {
env->ThrowNew(runtimeExceptionClass, "Fail to find dvm");
return JNI_FALSE;
}
dvmRawDexFileOpen = (func_dvmRawDexFileOpen) dlsym(handler, "_Z17dvmRawDexFileOpenPKcS0_PP10RawDexFileb");
if (dvmRawDexFileOpen == nullptr) {
ALOGE("fail to get dvm func");
}
...
}
Java_com_bytedance_boost_1multidex_BoostNative_makeOptDexFile(JNIEnv *env, jclass,
jstring jFilePath,
jstring jOptFilePath) {
if (dvmRawDexFileOpen == nullptr) {
return JNI_FALSE;
}
if (sigsetjmp(sSigJmpBuf, 1) != 0) {
ALOGE("recover and skip crash");
return JNI_FALSE;
}
ScopedSetSigFlag scoped;
const char *file_path = env->GetStringUTFChars(jFilePath, nullptr);
const char *opt_file_path = env->GetStringUTFChars(jOptFilePath, nullptr);
void *arg;
int result = dvmRawDexFileOpen(file_path, opt_file_path, &arg, false);
env->ReleaseStringUTFChars(jFilePath, file_path);
env->ReleaseStringUTFChars(jOptFilePath, opt_file_path);
return (result != -1) ? JNI_TRUE : JNI_FALSE;
}
代码很简单,还是在initialize()函数中先通过dlopen函数加载动态链接库libdvm.so,在通过符号_Z17dvmRawDexFileOpenPKcS0_PP10RawDexFileb拿到
dvmRawDexFileOpen()函数的指针,在makeOptDexFile函数中调用dvmRawDexFileOpen()函数进行OPT操作,生成ODex文件。
6.结语
到此为止,BoostMultiDex框架就分析的差不多了,由于水平有限,理解可能出现偏差,对于有不理解或者有歧义的地方都以官方文档和代码为准。在此非常感谢抖音团队为开源社区做出的贡献,期待开源更多的好项目,致敬!