澳门新葡萄京官网首页 12

澳门新葡萄京官网首页由Android 65K方法数限制引发的思考

前言

没想到,65536真的很小。

Unable to execute dex: method ID not in [0, 0xffff]: 65536

PS:本文只是纯探索一下这个65K的来源,仅此而已。

什么是64K限制和LinearAlloc限制

64K限制

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

澳门新葡萄京官网首页 1

这里写图片描述

这个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崩溃

原文发表于 

到底是65k还是64k?

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

65536按1000算的话,是65k ~ 65 1000;

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

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

谷歌分包方案

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

澳门新葡萄京官网首页 2

这里写图片描述

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

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

作者:
凯冯 发表于: 2016-05-05

时间点

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

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

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文件函数相互调用,找不到方法

澳门新葡萄京官网首页 3

65536是怎么算出来的?

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

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

  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/

下面是流程图:

澳门新葡萄京官网首页 4

这里写图片描述

2016 年 3 月 10 日, Google 向外界发布了 Android N 的预览版,并宣布了
Android N
的 Roadmap,Android
N 的最终版源代码将于今年 8 或 9 月份释出到 AOSP 项目。

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类型,特此说明。

概述

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中的类动态加载进来。

在众多的 Android N 新特性中,有一项新工具链的出现与 Android
生态圈的所有开发者息息相关,即 Jack & Jill 编译器的引入。

2. DexOpt优化造成?

这个说法源自:

当Android系统启动一个应用的时候,有一步是对Dex进行优化,这个过程有一个专门的工具来处理,叫DexOpt。DexOpt的执行过程是在第一次加载Dex文件的时候执行的。这个过程会生成一个ODEX文件,即Optimised
Dex。执行ODex的效率会比直接执行Dex文件的效率要高很多。但是在早期的Android系统中,DexOpt有一个问题,也就是这篇文章想要说明并解决的问题。DexOpt会把每一个类的方法id检索起来,存在一个链表结构里面。但是这个链表的长度是用一个short类型来保存的,导致了方法id的数目不能够超过65536个。当一个项目足够大的时候,显然这个方法数的上限是不够的。尽管在新版本的Android系统中,DexOpt修复了这个问题,但是我们仍然需要对老系统做兼容。

鉴于我能力有限,没有找到这块逻辑对应的代码。

但我有个疑问,这个限制是在Android启动一个应用的时候发生的,但从前面的“时间点”章节,65k问题是在构建的时候就发生了,还没到启动或者运行这一步。

我不敢否定这种说法,但说明65k至少还有其他地方限制。

在依赖了 Sun/Oracle 的 Java 编译器十年之后,Android 终于有了自己的 Java
编译器。

3. DexMerger的检测

只能在dalvik目录下搜索关键字”methid ID not
in”,在DexMergger里找到了抛出异常的地方:

/**
 * Combine two dex files into one.
  */
public final class DexMerger {

    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();
    }
}

这里定义了indexMap的methodIds的单项值要强转short,所以在存放之前check一下范围是不是0
~ 0xffff。我们看看IndexMap的定义:

/**
 * Maps the index offsets from one dex file to those in another. For example, if
 * you have string #5 in the old dex file, its position in the new dex file is
 * {@code strings[5]}.
 */
public final class IndexMap {
    private final Dex target;
    public final int[] stringIds;
    public final short[] typeIds;
    public final short[] protoIds;
    public final short[] fieldIds;
    public final short[] methodIds;

    // ... ...
}

看上去是对了,可是这个DexMerger是合并两个dex的,默认情况下我们只有一个dex的,那么这个65k是哪里限制的呢?再查!

本文试图对市面上非常有限的资料进行总结,向大家介绍 Jack & Jill
的缘起,工作方式和原理。

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;

至此,真相大白!

Jack 是 Java Android Compiler Kit 的缩写,它可以将 Java 代码直接编译为
Dalvik 字节码,并负责 Minification, Obfuscation, Repackaging,
Multidexing, Incremental compilation。它试图取代
javac/dx/proguard/jarjar/multidex 库等工具。

5. 根本原因

为什么定义DexFormat.MAX_MEMBER_IDX为0xFFFF?

虽然我们找到了65k报错的地方,但是为什么程序中方法数超过0xFFFF就要报错呢?

通过搜索”instruction formats”, 我最终查到了Dalvik VM
Bytecode,找到最新的官方说明:

里面说明了上面的@CCCC的范围必须在0~65535之间,这是dalvik
bytecode的限制。

所以,65536是bytecode的16位限制算出来的:2^16。

PS:以上分析得到群里很多朋友的讨论和帮忙。

git
源代码地址是 https://android.googlesource.com/toolchain/jack。

6. 回顾

我好像明白了什么:

  1. 65k问题是dx打包单个Dex时报的错,所以只要用dx打包单个dex就可能有这个问题。
  2. 不仅方法数,字段数也有65k问题。
  3. 目前来说,65k问题和系统无关。
  4. 目前来说,65k问题和art无关。
  5. 即使分包MultiDex,当主Dex的方法数超过65k依然会报错。
  6. MultiDex方案不是从根本上解决了65k问题,但是大大缓解甚至说基本解决了65k问题。

Jill 是 Jack Intermediate Library Linker 的缩写,它负责 “Shielding JACK
from Java byte code”;实际上辅助 Jack 对.class 做预处理,生成 .jack 文件

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

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

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

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

git
源代码地址是 https://android.googlesource.com/toolchain/jill。

缘起

虽然 Google 是在宣布 Android N 预览版时隆重介绍了Jack & Jill。但是,早在
2014 年 Google 就对外宣布了新编译器 Jack 的存在 meet our new
experimental
toolchain,
它的开发启动时间更是远远早于 2014 年。

下面是我总结的 Jack 的缘起

一家名叫 FlexyCore 的小公司基于 GCC toolchain 开发了 Android 平台上的
AOT 编译器,被 Google 看中并于 2013 年被收购

FlexyCore team 基于 LLVM toolchain 开发了 ART,并成为 Android 5.0
之后的缺省 Java Runtime

FlexyCore team 基于 Eclipse ecj 编译器开始开发 Jack,基于 ASM4 开发
Jill。 他们早在 2014 年 2 月就开始提交 Jill 的代码了 Jill initial
commit;
3 月份开始提交 Jack的代码 Jack initial
commit

自 Android build-tools 21.1 开始,里面已经内置 jack.jar 和 jill.jar

Android Gradle plugin 自 0.14 开始支持 Jack & Jill initial
commit

自 Android 6.0 开始,Jack & Jill 成为 AOSP 的官方编译器, 也就是说所有的
Android 6.0 ROM 都是 Jack
编译出来的 link,也代表
Google 认为 Jack 达到了一定的成熟度

预计等 Android 7.0 正式发布时,Jack 可能会成为官方推荐的编译器

为什么要抛弃 Javac/dx,开发 Jack 和
Jill

据个人推测主要有三个目的

提高编译速度

应对 Oracle 的法律诉讼

将编译器掌控权拿在自己手中,不再受制于 Oracle,可以做一些 Android only
的优化

下面比较一下旧的 javac/dx/ProGuard/jarjar toolchain 和新的 Jack
编译器的工作流程

旧编译流程

简单的说,将 Java 代码和依赖库编译为 dex 有两个大的阶段

javac (.java –> .class) –> dx (.class –> .dex)

下面是用流程图表示的旧编译过程

澳门新葡萄京官网首页 5

javac 将 java 代码编译为 java bytecode, 以 .class 的形式存在; 以 jar 和
aar 形式存在的依赖库,代码在里面以一堆.class 的形式存在

Proguard 工具读取 Proguard 配置,对 .class 做 shrinking,
obfuscation,输出 Proguard mapping

dx 将多个 .class 转化为单一的 classes.dex ; 如果 dex 方法数超过 65k,
就生成 classes.dex, classes1.dex…classesN.dex

新编译流程

新的编译过程只有一个阶段了,它完全抛弃了 javac, ProGuard, jarjar
等工具,一个工具搞定一切

Jack (.java –> .jack –> .dex)

下面是用流程图表示的 Jill 预处理过程

澳门新葡萄京官网首页 6

下面是用流程图表示的 Jack 编译过程

澳门新葡萄京官网首页 7

各种依赖库仍然以 jar/aar 的形式存在

辅助工具 Jill 将根据依赖库中的 .class 生成 Jayce 格式的 IL,并调用 Jack
做 pre-dex 并生成 .jack,此过程只在编译 app 时发生一次

Jack 将 java 源代码也编译为 .jack,然后将多个 .jack 转化为单一的 .dex;
如果 dex 方法数超过 65k, 就生成 classes.dex, classes1.dex…classesN.dex

pre-dex
的详细解释可以参阅此链接 new-build-system

Improving Build Server performance.

The Gradle based build system has a strong focus on incremental builds.
One way it is doing this in doing pre-dexing on the dependencies of each
modules, so that each gets turned into its own dex file (ie converting
its Java bytecode into Android bytecode). This allows the dex task to do
less work and to only re-dex what changed and merge all the dex files.

.Jack中间文件

.Jack 的具体格式如下图所示

澳门新葡萄京官网首页 8

可见里面包含了 Jayce 格式的 IL ,pre-dex,原始 aar 中的资源文件,以及
Jack 会用到的一些 meta 信息

下图简单比较了 java 代码转化的 .class, Jayce IL 和 dex 的内容异同

澳门新葡萄京官网首页 9

简单比较下三种 IL 的区别:

Sun/Oracle Hotspot VM
是基于栈式的,所以 .class 文件的内容就是不断地压操作数到栈顶,从栈顶读取操作数,比较或做运算,将结果再压回栈顶

Dalvik VM 是基于寄存器的,所以 .dex 的内容就是不断地 move
操作数到寄存器,比较或做运算,将结果写回寄存器或内存地址

Jayce 则是 Jack&Jill 专有的 IL, 目前没有查阅到更多的官方资料。只能参阅
Jill 源代码中 com.android.jill.backend.jayce 包的代码了,比如其中的
Token 类就定义了 Jayce 的 Token 定义。

个人推测 Jayce 存在的意义是:

为了在整合多个 jack 文件,生成单一的 dex 时,方便 Jack
做一些全局性的后端编译优化。

从 Android 生态圈中完全去除 Oracle 的 Java Bytecode 格式

使用Jack编译器的优势

对依赖库做 pre dex,且成果会被保存到 build/intermediates/jill/debug
目录。

之后的编译过程中,只要依赖库的数目和版本不变,之前的 pre dex
成果会被复用;Jack 只需要编译变化的源代码,然后对多个 dex 进行 merge
即可,能够加速整个编译过程。

编译时会启动一个 Jack compilation server,并开启并行编译

Jack 文档是这么介绍的

This server brings an intrinsic speedup, because it avoids launching a
new host JRE JVM, loading Jack code, initializing Jack and warming up
the JIT at each compilation. It also provides very good compilation
times during small compilations (e.g. in incremental mode).

The server is also a short-term solution to control the number of
parallel Jack compilations, and so to avoid overloading your computer
(memory or disk issue), because it limits the number of parallel
compilations.

支持 Java 8 的一部分特性

Jack 由 Google 完全掌控,未来可能成为 Android sdk 的默认编译器

向后兼容到 Android 2.3

采用 Jack
对打包流程的影响

不再需要独立的 ProGuard。Jack 支持读取旧的 ProGuard 配置,完成
shrinking, obfuscation 的工作

不再需要独立的 jarjar。Jack 支持读取旧的 jarjar 配置,完成 repackaging
的工作

没有 .class 文件了,直接操纵或读取 Java 字节码的各种工具如
JaCoCo/Lint/Mokito/Retrolambda 没有了用武之地。但是仍然可以在 Android
Library 上使用这些工具,编译为 aar/jar 后作为 Jill 的输入

annotation processors 如 Dagger, ButterKife 仍可以使用

Scala/Kotlin 等第三方 JVM 语言编写的内容必须先被 Jill 处理,再作为 Jack
的输入

Jack
当前的局限(截止到2016/03/15)

暂时还不支持 Android Studio 2.0 的 Instant Run 特性

暂时还不支持 data binding

65k
方法数目问题

为什么会有 65k
问题?

当你的 app 足够复杂之后,在打包时常常会遇到这种错误提示

Unable to execute dex: method ID not in [0, 0xffff]: 65536

为什么方法数目不能超过 65k 呢?有人说是 dexopt 的问题,有人说是 dex
格式的限制,下面我们看看这个 log
到底是哪里吐出来的,然后分析下具体原因。

dex 格式的限制?

首先我们看一下 dex 的结构定义

//Direct-mapped “header_item” struct.

structDexHeader {

  u4  methodIdsSize;

};

//These match the definitions in the VM specification.

typedefuint32_tu4;

可见 dex 文件结构是用 32 位来存储 method id 的,最大支持 2 的 32
次方,因此 65k 的原因不在于此。

dexopt 的原因?

dexopt 是 app 已经打包成功,安装到手机之后才会发生的过程。但是 65k
问题是在打包时发生的,所以问题原因也不在此

一般提到的 dexopt 错误,其实是 Android 2.3 及其以下在 dexopt
执行时只分配 5M 内存,导致方法数目过多(数量不一定到 65k)时在 odex
过程中崩溃,官方称之为 Dalvik linearAlloc bug(Issue 22586) 。

另:这个 linearAlloc 的限制不仅存在于 dexopt 里,还在 dalvik rumtime
中存在……

以下链接详细解释了此问题:https://github.com/simpleton/dalvik_patch

错误 log 是哪里吐出来的?

//MemberIdsSection.java

if(items().size() > DexFormat.MAX_MEMBER_IDX +1) {

thrownewDexIndexOverflowException(getTooManyMembersMessage());

}

/*

Maximum addressable field or method index.

The largest addressable member is 0xffff, in the “instruction formats”
spec as field@CCCC or meth@CCCC.

*/

publicstaticfinalintMAX_MEMBER_IDX =0xFFFF;

通过查阅 dalvik-bytecode 可知,@CCCC
的范围必须在 0~65535 之间。

所以归根结底,65k 问题是因为 dalvik bytecode 中的指令格式使用了 16
位来放 @CCCC 导致的;所以,不仅 Method 数目不能超过 65k, Field 和 Class
数目也不能超过 65k。

为什么 jack 没有 65k
问题

前文已经很清楚地解释了 65k 问题的由来,可见只要 dalvik bytecode
指令格式不升级,65k 问题是逃不掉的。

Jack 官网对 65k 问题是这么说的:

Multidex support

Since dex files are limited to 65K methods, apps with over 65K methods
must be split into multiple dex files. (See ‘Building Apps with Over 65K
Methods’ for more information about multidex.)

Jack offers native and legacy multidex support.

所以,Jack 和旧工具链对 multidex 的支持方式是相同的

被 Jack 编译出来的 app 执行时也和以前一样

若是 dalvik 虚拟机,它只支持读取一个 classes.dex。而 multidex
解决方案会读取多个 .dex,帮我们做 dex 数组合并

若是 art 虚拟机,它会扫描 classes.dex, classes1.dex…classesN.dex,调用
dex2oat 转化为单一的 oat

Jack 是怎么支持 Java 8
的?

以 lambda 表达式为例

Interface lambda = i -> i +1;

会被转化为 anonymous classes

Interface lambda =newInterface() {

publicintm(inti){

returni +1;

  }

};

Jack当前支持的 Java 8
特性可参见 j8-jack。

如何在 Gradle 脚本中使用 Jack 编译器编译
app

想使用 Jack 和 Jill 需要指定你的 Build Tools version 是 21.1.0+, Gradle
plugin version 是1.0.0+。

以下的配置是我个人测试通过的配置

使用 Android Gradle 插件 2.1.0-alpha2

dependencies {

classpath’com.android.tools.build:gradle:2.1.0-alpha2′

}

使用以下版本的 sdk 和 build-tool

compileSdkVersion’android-N’

buildToolsVersion’24.0.0 rc1′

在 defaultConfig 中指定用 Jack

defaultConfig {

  jackOptions {

enabledtrue

  }

}

使用 gradle 2.10 以上

distributionUrl=http://mirrors.taobao.net/mirror/gradle/gradle-2.10-bin.zip

使用 Android Studio 2.1 (preview) 或者命令行编译

可能需要提升 javaMaxHeapSize

dexOptions{

javaMaxHeapSize”2g”

}

性能比较

经过测试,当前版本(2016/03/15)的 Jack 编译器比起 Javac+dx
在编译时间,编译出的 apk 体积,编译出的 apk 的性能上暂时并没有优势。

但是,可以期待 Google 将在 Jack 编译器上做大量的智力投资,Jack
的未来是光明的。

下图是 guardsquare 公司对 Javac+dx 和 Jack 做的对比测试

澳门新葡萄京官网首页 10

对于不 proguard 的 clean build,javac/dx 耗时 56s, jack 耗时 1 m 48
s;之所以 jack 这么慢是因为它要做大量的 pre-dex。

澳门新葡萄京官网首页 11

对于不 proguard 的 clean build,javac/dx 和 jack 编译出来的 app
性能相差无几。

澳门新葡萄京官网首页 12

对于共用 proguard 配置文件情况,javac/dx 和jack 编译出来的 app
体积也差不多。

我个人测试的编译速度 / apk 体积等对比也大致如此,在此不再赘述.

结语

虽然 Jack 编译器的现状并不出彩,但是它终究有一天会成为 Android app
的官方推荐编译器。

期待 Google Android team 加倍努力,让这一天早日到来。

参考文献

https://www.guardsquare.com/blog/the_upcoming_jack_and_jill_compilers_in_android

http://source.android.com/devices/tech/dalvik/dex-format.html

http://tools.android.com/tech-docs/jackandjill

https://developer.android.com/intl/zh-cn/tools/building/multidex.html

https://www.guardsquare.com/blog/DroidconLondon2015

发表评论

电子邮件地址不会被公开。 必填项已用*标注