multidex

这是一篇关于multidex的使用、源码解析、存在哪些坑的文章。

使用

网上一大坨,且使用比较简单,所以忽略。

源码解析

  • MultiDex
  • MultiDexApplication
  • MultiDexExtractor
  • ZipUtil

1、首先检查手机的java vm已经支持了multidex,只要java vm的版本大于等于2.1即是原生支持了miltidex的功能,multidex的support包就是无效的了。

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

2、将 secondary dex从apk中提取出来

  • sourceApk:/data/app/pkgname.apk
  • dexDir:/data/data/pkgname/code_cache/secondary-dexes
  • 进行文件CRC的检查,以及是否需要强制更新

实际的提取是在performExtractions中的extract()方法中:

extract(apk, dexFile, extractedFile, extractedFilePrefix);

apk(ZipFile) 指向/data/app/pkgname.apk
dexFile(ZipEntry) 指向classes2.dex
extractTo(File) 指向/data/data/pkgname/code_cache/secondary-dexes/pkgname.apk.classes2.zip
extractedFilePrefix(String) pkgname.apk.classes

  1. 根据apk文件生成apk文件的对应的ZipFile
  2. 从生成的ZipFile中提取出classes2.dex
  3. 生成最后的zip文件的路径
  4. 将dexFile写入到extractTo指向的文件路径上
  5. 完整dex的提取

3、安装secondary dex
提供三种不同sdk版本的安装V4、V14、V19。
其实就是通过反射的方式去修改classLoad的值

延伸阅读:dalvik.system.BaseDexClassLoader
classload章节

是什么引起Dex超出

Dex的结构定义?

/*
 * Direct-mapped "header_item" struct.
 */
struct DexHeader {
    u1  magic[8];
    u4  checksum;
    u1  signature[kSHA1DigestLen];
    u4  fileSize;
    u4  headerSize;
    u4  endianTag;
    u4  linkSize;
    u4  linkOff;
    u4  mapOff;
    u4  stringIdsSize;
    u4  stringIdsOff;
    u4  typeIdsSize;
    u4  typeIdsOff;
    u4  protoIdsSize;
    u4  protoIdsOff;
    u4  fieldIdsSize;
    u4  fieldIdsOff;
    u4  methodIdsSize; // 这里存放了方法字段索引的大小,methodIdsSize的类型为u4
    u4  methodIdsOff;
    u4  classDefsSize;
    u4  classDefsOff;
    u4  dataSize;
    u4  dataOff;
};

/*
 * These match the definitions in the VM specification.
 */
typedef uint8_t             u1;
typedef uint16_t            u2;
typedef uint32_t            u4;
typedef uint64_t            u8;
typedef int8_t              s1;
typedef int16_t             s2;
typedef int32_t             s4;
typedef int64_t             s8;

methodIdsSize的类型是uint32_t,它是4字节大小的,为2^32 = 65536 * 65536,比65536大的多。

所以不是因为dex结构的定义导致方法书超出的。

DexOpt优化造成?

当Android系统安装一个应用的时候,有一步是对Dex进行优化,这个过程有一个专门的工具来处理,叫DexOpt。DexOpt的执行过程是在第一次加载Dex文件的时候执行的。这个过程会生成一个ODEX文件,即Optimised Dex。执行ODex的效率会比直接执行Dex文件的效率要高很多。

DexOpt会产生两个问题:

  1. DexOpt 会把每一个类的方法 id 检索起来,存在一个链表结构里面,但是这个链表的长度是用一个 short 类型来保存的,导致了方法 id 的数目不能够超过65536个。当一个项目足够大的时候,显然这个方法数的上限是不够的。
  2. Dexopt 使用 LinearAlloc 来存储应用的方法信息。Dalvik LinearAlloc 是一个固定大小的缓冲区。在Android 版本的历史上,LinearAlloc 分别经历了4M/5M/8M/16M限制。Android 2.2和2.3的缓冲区只有5MB,Android 4.x提高到了8MB 或16MB。当方法数量过多导致超出缓冲区大小时,也会造成dexopt崩溃。

新版本的Android系统已经优化DexOpt方法数65K的限制问题,并且扩大了LinearAlloc限制,但是我们仍然需要对低版本的Android系统做兼容。

可问题是DexOpt是在安转时,并不是在构建的时候,所以根本的原因还不在这里。所以它的限制是在安装时。

DexMerger的检测?

将第三方的jar的代码和自己的代码合并成dex。

DexMerger.java

...
private void mergeMethodIds() {
        new IdMerger<MethodId>(idsDefsOut) {
            @Override TableOfContents.Section getSection(TableOfContents tableOfContents) {
                return tableOfContents.methodIds;
            }
            @Override MethodId read(Dex.Section in, IndexMap indexMap, int index) {
                return indexMap.adjust(in.readMethodId());
            }
            @Override void updateIndex(int offset, IndexMap indexMap, int oldIndex, int newIndex) {
                if (newIndex < 0 || newIndex > 0xffff) {
                    throw new DexIndexOverflowException(
                            "method ID not in [0, 0xffff]: " + newIndex);
                }
                indexMap.methodIds[oldIndex] = (short) newIndex;
            }
            @Override void write(MethodId methodId) {
                methodId.writeTo(idsDefsOut);
            }
        }.mergeSorted();
    }
...

可以看出在这里强制做了大小的判断。

为什么要判断?

https://source.android.com/devices/tech/dalvik/dalvik-bytecode.html

这是dalvik bytecode的限制。65536是bytecode的16位限制算出来的:2^16。

如何进行分dex

参数配置:

--multi-dex:多 dex 打包的开关。
--main-dex-list=<file>:参数是一个类列表的文件,在该文件中的类会被打包在第一个 dex 中。
--minimal-main-dex:只有在--main-dex-list 文件中指定的类被打包在第一个 dex,其余的都在第二个 dex 文件中。

如何我们想对multiDex有更多的控制的话,可以在gradle中加入:

afterEvaluate {
    tasks.matching {
        it.name.startsWith('dex')
    }.each { dx ->
        if (dx.additionalParameters == null) {
            dx.additionalParameters = []
        }
        dx.additionalParameters += '--multi-dex'
        // 设置multidex.keep文件中class为第一个dex文件中包含的class,如果没有下一项设置此项无作用
        dx.additionalParameters += "--main-dex-list=keeppath".toString()
        //此项添加后第一个classes.dex文件只能包含-main-dex-list列表中class  
        dx.additionalParameters += '--minimal-main-dex'
        // 设置主dex大小
        dx.additionalParameters += '--set-max-idx-number=20000' 
    }
}

MultiDex的实现原理

Dex的加载实现

http://souly.cn/%E6%8A%80%E6%9C%AF%E5%8D%9A%E6%96%87/2016/02/25/android%E5%88%86%E5%8C%85%E5%8E%9F%E7%90%86/

https://segmentfault.com/a/1190000004053072#articleHeader2

https://github.com/TangXiaoLv/Android-Easy-MultiDex

results matching ""

    No results matching ""