Android MultiDex

澳门新葡亰3522平台游戏 3

4. 回归DexFile

基本上前面基本是一个摸着石头过河、反复验证网络说法的一个过程,虽然回想起来傻傻的,但是这种记录还是有必要的。

前面看到DexFile的存放方法数大小的类型是uint32,但是根据后面的判断,我们确定是打包的过程中产生了65k问题,所以我们得回过头老老实实研究一下dx的打包流程。

… 此处省略分析流程5000字 …

OK,我把dx打包涉及到流程记录下来:

// 源码目录:dalvik/dx
// Main.java
-> main() -> run() -> runMonoDex()(或者runMultiDex()) -> writeDex()
// DexFile
-> toDex() -> toDex0()
// MethodIdsSection extends MemberIdsSection extends UniformItemSection extends  Section
-> prepare() -> prepare0() -> orderItems() -> getTooManyMembersMessage()
// Main.java
-> getTooManyIdsErrorMessage()

最终狐狸的尾巴是在MemberIdsSection漏出来了:

package com.android.dx.dex.file;

import com.android.dex.DexException;
import com.android.dex.DexFormat;
import com.android.dex.DexIndexOverflowException;
import com.android.dx.command.dexer.Main;

import java.util.Formatter;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Member (field or method) refs list section of a {@code .dex} file.
 */
public abstract class MemberIdsSection extends UniformItemSection {

    /**
     * Constructs an instance. The file offset is initially unknown.
     *
     * @param name {@code null-ok;} the name of this instance, for annotation
     * purposes
     * @param file {@code non-null;} file that this instance is part of
     */
    public MemberIdsSection(String name, DexFile file) {
        super(name, file, 4);
    }

    /** {@inheritDoc} */
    @Override
        protected void orderItems() {
            int idx = 0;

            if (items().size() > DexFormat.MAX_MEMBER_IDX + 1) {
                throw new DexIndexOverflowException(getTooManyMembersMessage());
            }

            for (Object i : items()) {
                ((MemberIdItem) i).setIndex(idx);
                idx++;
            }
        }

    private String getTooManyMembersMessage() {
        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();
        try {
            String memberType = this instanceof MethodIdsSection ? "method" : "field";
            formatter.format("Too many %s references: %d; max is %d.%n" +
                    Main.getTooManyIdsErrorMessage() + "%n" +
                    "References by package:",
                    memberType, items().size(), DexFormat.MAX_MEMBER_IDX + 1);
            for (Map.Entry<String, AtomicInteger> entry : membersByPackage.entrySet()) {
                formatter.format("%n%6d %s", entry.getValue().get(), entry.getKey());
            }
            return formatter.toString();
        } finally {
            formatter.close();
        }
    }

}

里面有一段:

// 如果方法数大于0xffff就提示65k错误
if (items().size() > DexFormat.MAX_MEMBER_IDX + 1) {
    throw new DexIndexOverflowException(getTooManyMembersMessage());
}

// 这个DexFormat.MAX_MEMBER_IDX就是0xFFFF
/**
 * Maximum addressable field or method index.
 * The largest addressable member is 0xffff, in the "instruction formats" spec as field@CCCC or
 * meth@CCCC.
 */
public static final int MAX_MEMBER_IDX = 0xFFFF;

至此,真相大白!

multiDex存在的问题

虽然谷歌的分包方案很简单,但是效果并不是那么好,谷歌本身也枚举了分包方案的缺点

  1. 如果在主线程中执行MultiDex.install,加载second
    dex,因为加载从dex是同步的,会阻塞线程,second
    dex太大的话,有可能导致ANR
  2. API Level 14之前,由于Dalvik LinearAlloc
    bug(问题22586,就是上文提到的LinearAlloc问题),很可能会出问题的
  3. 应用程序使用了multiedex配置的,会造成使用比较大的内存
  4. 对于应用程序比较复杂的,存在较多的library的项目。multidex可能会造成不同依赖项目间的dex文件函数相互调用,找不到方法

1. 首先,查找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;
};

u4的类型定义如下:

/*
 * 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,但它的限制为2^32 = 65536 *
65536,比65536大的多。

所以,65k不是dex文件结构本身限制造成的。

PS:Dex文件中存储方法ID用的并不是short类型,无论最新的DexFile.h新定义的u4是uint32_t,还是老版本DexFile引用的vm/Common.h里定义的u4是uint32或者unsigned
int,都不是short类型,特此说明。

如何解决谷歌分包方案的问题

  1. 针对上面的问题,参考网上的一些解决方案,如美团、facebook、微信等,初步使用的解决方法如下:
    第一次启动的时候,检测到未曾加载过second
    dex,那么启动欢迎页面(启动新的进程,原来进程进入阻塞等待,注意,此时不会发生ANR,因为已经不是前台进程了),在欢迎页面里面进行second
    dex的加载,加载完成后通知主线程继续
  2. 设定单个dex文件最大方法数为48000(经验值)而不是65536,避免内存问题
  3. 同上
  4. 控制程序逻辑,未曾加载完second
    dex之前,进入阻塞等待,直到加载完程序才往下走

gradle:

afterEvaluate { 
  tasks.matching { 
    it.name.startsWith('dex') 
  }.each { dx -> 
    if (dx.additionalParameters == null) { 
      dx.additionalParameters = []
    }  
    dx.additionalParameters += '--set-max-idx-number=48000' 
    dx.additionalParameters += "--main-dex-list=$projectDir/multidex.keep".toString()
  } 
}

  } 
}

–set-max-idx-number= 用于控制每一个 dex 的最大方法个数。
–main-dex-list= 参数是一个类列表的文件,在该文件中的类会被打包在第一个
dex 中。
multidex.keep 里面列上需要打包到第一个 dex 的 class
文件,注意,如果需要混淆的话需要写混淆之后的 class 。

http://yydcdut.com/2016/03/20/split-dex/

下面是流程图:

澳门新葡亰3522平台游戏 1

这里写图片描述

65536是怎么算出来的?

65536网上众说纷纭,有对的,有不全对的,也有错的。下面将跟踪最新的AOSP源码来顺藤摸瓜,但是探索问题必然迂回冗余,仅作记录,读者可直接跳过看结果。

谷歌分包方案

谷歌提供了一个multiDex的分包方案,当方法数超过65536的时候,生成多个dex文件,把应用启动时必须用到的类和该类的直接引用类放到main
dex中,把其他类放到second dex中。当应用启动之后,动态加载second
dex,从而避免64k问题。使用Android Studio很容易实现分包方案:

澳门新葡亰3522平台游戏 2

这里写图片描述

在build.gradle中添加:multiDexEnabled true
加入依赖‘compile ‘com.android.support:multidex:1.0.1’’
让应用的Application类直接使用或者继承MultiDexApplication
如果你想使用自定义的Application,又不想继承MultiDexApplication,那么可以在attachBaseContext方法里执行MultiDex.install(base)

以上就是谷歌multiDex方案所需做的设置,通过配置multiDex,便可解决64k方法数限制

新的Jack能否解决65k问题?

据说Jack的方式把class打包成.jack文件。所以我认为,Jack具备解决65k问题的条件:

  1. 打包:新的jack文件肯定是抛弃了dalvik的兼容性,这也注定咱们这两年可能还用不了。
  2. 虚拟机:完全采用新的ART虚拟机,把class转化成本地机器码,就能避开dalvik
    bytecode的16位限制。
  3. 上面两条属于废话,说白了,完全不用dalvik虚拟机了,同时也就完全不用dx了,如此,当然就不存在65k问题了。

以上纯属我个人推测,一切以科学分析为准。

概述

Android开发者应该都遇到了64K最大方法数限制的问题,针对这个问题,google也推出了multidex分包机制,在生成apk的时候,把整个应用拆成n个dex包(classes.dex、classes2.dex、classes3.dex),每个dex不超过64k个方法。使用multidex,在5.0以前的系统,应用安装时只安装main
dex(包含了应用启动需要的必要class),在应用启动之后,需在Application的attachBaseContext
中调用 MultiDex.install(base)
方法,在这时候才加载第二、第三…个dex文件,从而规避了64k问题。
当然,在attachBaseContext
方法中直接install启动second
dex会有一些问题,比如install方法是一个同步方法,当在主线程中加载的dex太大的时候,耗时会比较长,可能会触发ANR。不过这是另外一个问题了,解决方法可以参考:Android最大方法数和解决方案

本文主要分析的是MultiDex.install()
到底做了什么,如何把secondary dexes中的类动态加载进来。

到底是65k还是64k?

都没错,同一个问题,不同的说法而已。

澳门新葡亰3522平台游戏 ,65536按1000算的话,是65k ~ 65 1000;

65536按1024算的话,是64k = 64 1024。

重点是65536=2^16,请大家记住这个数字。

什么是64K限制和LinearAlloc限制

64K限制

随着Android
应用功能的增加,代码量不断地增大,当应用方法数量超过了65536的时候,编译的时候便会提示:

澳门新葡亰3522平台游戏 3

这里写图片描述

这个Android著名的Dex 64k method数量上限。

是什么原因导致方法数不能超过64K呢?(其实不仅仅是方法数,类数量也不能超过64k)

  1. DexOpt优化的限制:当Android系统启动一个应用的时候,有一步是对Dex进行优化,这个过程有一个专门的工具来处理,叫DexOpt。DexOpt的执行过程是在第一次加载Dex文件的时候执行的。这个过程会生成一个ODEX文件,即Optimised
    Dex。执行ODex的效率会比直接执行Dex文件的效率要高很多。但是在早期的Android系统中,DexOpt有一个问题,也就是这篇文章想要说明并解决的问题。DexOpt会把每一个类的方法id检索起来,存在一个链表结构里面。但是这个链表的长度是用一个short类型来保存的,导致了方法id的数目不能够超过65536个。当一个项目足够大的时候,显然这个方法数的上限是不够的。尽管在新版本的Android系统中,DexOpt修复了这个问题,但是我们仍然需要对老系统做兼容
  2. dalvik bytecode的限制:因为 Dalvik 的 invoke-kind
    指令集中,method reference index 只留了 16 bits,最多能引用 65535
    个方法,参考链接:http://stackoverflow.com/questions/21490382/does-the-android-art-runtime-have-the-same-method-limit-limitations-as-dalvik/21492160#21492160,http://source.android.com/devices/tech/dalvik/dalvik-bytecode.html

鉴于以上原因,在打包Android应用的时候,会对方法数做一个检测,当方法数超过了DexFormat.MAX_MEMBER_IDX(定义为0Xffff,
注意,这个不是Dex文件格式的限制,Dex文件中存储方法ID用的并不是short类型,无论最新的DexFile.h新定义的u4是uint32_t,还是老版本DexFile引用的vm/Common.h里定义的u4是uint32或者unsigned
int,都不是short类型,特此说明)便报错
LinearAlloc限制
即使方法数没有超过65536,能正常编译打包成apk,在安装的时候,也有可能会提示INSTALL_FAILED_DEXOPT而导致安装失败,这个一般就是因为LinearAlloc的限制导致的。这个主要是因为Dexopt
使用 LinearAlloc 来存储应用的方法信息。Dalvik LinearAlloc
是一个固定大小的缓冲区。在Android 版本的历史上,LinearAlloc
分别经历了4M/5M/8M/16M限制。Android 2.2和2.3的缓冲区只有5MB,Android
4.x提高到了8MB
或16MB。当方法数量过多导致超出缓冲区大小时,也会造成dexopt崩溃

时间点

从大家的经历和这篇文章:

来看,这个错误是发生在构建时期。