抖音BoostMultiDex为APP启动插上翅膀

1,088 阅读37分钟

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框架就分析的差不多了,由于水平有限,理解可能出现偏差,对于有不理解或者有歧义的地方都以官方文档代码为准。在此非常感谢抖音团队为开源社区做出的贡献,期待开源更多的好项目,致敬!