Android-Dex分包最全总结:含Facebook解决方案,移动app开发

Android-Dex分包最全总结:含Facebook解决方案,移动app开发,第1张

Android-Dex分包最全总结:含Facebook解决方案,移动app开发 65536

trouble writing output: Too many method references: 70048; max is 65536.
或者
UNEXPECTED TOP-LEVEL EXCEPTION:

java.lang.IllegalArgumentException: method ID not in [0, 0xffff]: 65536
at com.android.dx.merge.DexMerger 6. u p d a t e I n d e x ( D e x M e r g e r . j a v a : 501 ) a t c o m . a n d r o i d . d x . m e r g e . D e x M e r g e r 6.updateIndex(DexMerger.java:501) at com.android.dx.merge.DexMerger 6.updateIndex(DexMerger.java:501)atcom.android.dx.merge.DexMergerIdMerger.mergeSorted(DexMerger.java:276)
at com.android.dx.merge.DexMerger.mergeMethodIds(DexMerger.java:490)
at com.android.dx.merge.DexMerger.mergeDexes(DexMerger.java:167)
at com.android.dx.merge.DexMerger.merge(DexMerger.java:188)
at com.android.dx.command.dexer.Main.mergeLibraryDexBuffers(Main.java:439)
at com.android.dx.command.dexer.Main.runMonoDex(Main.java:287)
at com.android.dx.command.dexer.Main.run(Main.java:230)
at com.android.dx.command.dexer.Main.main(Main.java:199)
at com.android.dx.command.Main.main(Main.java:103):Derp:dexDerpDebug FAILED

编译环境

buildscript {
repositories {
jcenter()
}
dependencies {
classpath ‘com.android.tools.build:gradle:1.3.0’
}
}

android {
compileSdkVersion 23
buildToolsVersion “25.0.3”
//…
defaultConfig {
minSdkVersion 14
targetSdkVersion 23
//…
}
}

为什么是65536

根据 StackOverFlow – Does the Android ART runtime have the same method limit limitations as Dalvik? 上面的说法,是因为 Dalvik 的 invoke-kind 指令集中,method reference index 只留了 16 bits,最多能引用 65535 个方法。Dalvik bytecode :

即使 dex 里面的引用方法数超过了 65536,那也只有前面的 65536 得的到调用。所以这个不是 dex 的原因。其次,既然和 dex 没有关系,那在打包 dex 的时候为什么会报错。我们先定位 Too many 关键字,定位到了 MemberIdsSection :

public abstract class MemberIdsSection extends UniformItemSection {

@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 membersByPackage = new TreeMap();
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 entry : membersByPackage.entrySet()) {
formatter.format("%n%6d %s", entry.getValue().get(), entry.getKey());
}
return formatter.toString();
} finally {
formatter.close();
}
}
}

items().size() > DexFormat.MAX_MEMBER_IDX + 1 ,那 DexFormat 的值是:

public final class DexFormat {

public static final int MAX_MEMBER_IDX = 0xFFFF;
}

dx 在这里做了判断,当大于 65536 的时候就抛出异常了。所以在生成 dex 文件的过程中,当调用方法数不能超过 65535 。那我们再跟一跟代码,发现 MemberIdsSection 的一个子类叫 MethodidsSection :

public final class MethodIdsSection extends MemberIdsSection {}

回过头来,看一下 orderItems() 方法在哪里被调用了,跟到了 MemberIdsSection 的父类 UniformItemSection :

public abstract class UniformItemSection extends Section {
@Override
protected final void prepare0() {
DexFile file = getFile();

orderItems();

for (Item one : items()) {
one.addContents(file);
}
}

protected abstract void orderItems();
}

再跟一下 prepare0 在哪里被调用,查到了 UniformItemSection 父类 Section :

public abstract class Section {
public final void prepare() {
throwIfPrepared();
prepare0();
prepared = true;
}

protected abstract void prepare0();
}

那现在再跟一下 prepare() ,查到 DexFile 中有调用:

public final class DexFile {
private ByteArrayAnnotatedOutput toDex0(boolean annotate, boolean verbose) {
classDefs.prepare();
classData.prepare();
wordData.prepare();
byteData.prepare();
methodIds.prepare();
fieldIds.prepare();
protoIds.prepare();
typeLists.prepare();
typeIds.prepare();
stringIds.prepare();
stringData.prepare();
header.prepare();
//blablabla…
}
}

那再看一下 toDex0() 吧,因为是 private 的,直接在类中找调用的地方就可以了:

public final class DexFile {
public byte[] toDex(Writer humanOut, boolean verbose) throws IOException {
boolean annotate = (humanOut != null);
ByteArrayAnnotatedOutput result = toDex0(annotate, verbose);

if (annotate) {
result.writeAnnotationsTo(humanOut);
}

return result.getArray();
}

public void writeTo(OutputStream out, Writer humanOut, boolean verbose) throws IOException {
boolean annotate = (humanOut != null);
ByteArrayAnnotatedOutput result = toDex0(annotate, verbose);

if (out != null) {
out.write(result.getArray());
}

if (annotate) {
result.writeAnnotationsTo(humanOut);
}
}
}

先搜搜 toDex() 方法吧,最终发现在 com.android.dx.command.dexer.Main 中:

public class Main {
private static byte[] writeDex(DexFile outputDex) {
byte[] outArray = null;
//blablabla…
if (args.methodToDump != null) {
outputDex.toDex(null, false);
dumpMethod(outputDex, args.methodToDump, humanOutWriter);
} else {
outArray = outputDex.toDex(humanOutWriter, args.verboseDump);
}
//blablabla…
return outArray;
}
//调用writeDex的地方
private static int runMonoDex() throws IOException {
//blablabla…
outArray = writeDex(outputDex);
//blablabla…
}
//调用runMonoDex的地方
public static int run(Arguments arguments) throws IOException {
if (args.multiDex) {
return runMultiDex();
} else {
return runMonoDex();
}
}
}

args.multiDex 就是是否分包的参数,那么问题找着了,如果不选择分包的情况下,引用方法数超过了 65536 的话就会抛出异常。

同样分析第二种情况,根据错误信息可以具体定位到代码,但是很奇怪的是 DexMerger ,我们没有设置分包参数或者其他参数,为什么会有 DexMerger ,而且依赖工程最终不都是 aar 格式的吗?那我们还是来跟一跟代码吧。

public class Main {
private static byte[] mergeLibraryDexBuffers(byte[] outArray) throws IOException {
ArrayList dexes = new ArrayList();
if (outArray != null) {
dexes.add(new Dex(outArray));
}
for (byte[] libraryDex : libraryDexBuffers) {
dexes.add(new Dex(libraryDex));
}
if (dexes.isEmpty()) {
return null;
}
Dex
merged = new DexMerger(dexes.toArray(new Dex[dexes.size()]), CollisionPolicy.FAIL).merge();
return merged.getBytes();
}
}

这里可以看到变量 libraryDexBuffers ,是一个 List 集合,那么我们看一下这个集合在哪里添加数据的:

public class Main {
private static boolean processFileBytes(String name, long lastModified, byte[] bytes) {
boolean isClassesDex = name.equals(DexFormat.DEX_IN_JAR_NAME);
//blablabla…
} else if (isClassesDex) {
synchronized (libraryDexBuffers) {
libraryDexBuffers.add(bytes);
}
return true;
} else {
//blablabla…
}
//调用processFileBytes的地方
private static class FileBytesConsumer implements ClassPathOpener.Consumer {

@Override
public boolean processFileBytes(String name, long lastModified,
byte[] bytes) {
return Main.processFileBytes(name, lastModified, bytes);
}
//blablabla…
}
//调用FileBytesConsumer的地方
private static void processOne(String pathname, FileNameFilter filter) {
ClassPathOpener opener;

opener = new ClassPathOpener(pathname, true, filter, new FileBytesConsumer());

if (opener.process()) {
updateStatus(true);
}
}
//调用processOne的地方
private static boolean processAllFiles() {
//blablabla…
// forced in main dex
for (int i = 0; i < fileNames.length; i++) {
processOne(fileNames[i], mainPassFilter);
}
//blablabla…
}
//调用processAllFiles的地方
private static int runMonoDex() throws IOException {
//blablabla…
if (!processAllFiles()) {
return 1;
}
//blablabla…
}

}

跟了一圈又跟回来了,但是注意一个变量:fileNames[i],传进去这个变量,是个地址,最终在 processFileBytes 中处理后添加到 libraryDexBuffers 中,那跟一下这个变量:

public class Main {
private static boolean processAllFiles() {
//blablabla…
String[] fileNames = args.fileNames;
//blablabla…
}
public void parse(String[] args) {
//blablabla…
}else if(parser.isArg(INPUT_LIST_OPTION + “=”)) {
File inputListFile = new File(parser.getLastValue());
try{
inputList = new ArrayList();
readPathsFromFile(inputListFile.getAbsolutePath(), inputList);
} catch(IOException e) {
System.err.println("Unable to read input list file: " + inputListFile.getName());
throw new UsageException();
}
} else {
//blablabla…
fileNames = parser.getRemaining();
if(inputList != null && !inputList.isEmpty()) {
inputList.addAll(Arrays.asList(fileNames));
fileNames = inputList.toArray(new String[inputList.size()]);
}
}

public static void main(String[] argArray) throws IOException {
Arguments arguments = new Arguments();
arguments.parse(argArray);

int result = run(arguments);
if (result != 0) {
System.exit(result);
}
}
}

跟到这里发现是传进来的参数,那我们再看看 gradle 里面传的是什么参数吧,查看 Dex task :

public class Dex extends baseTask {
@InputFiles
Collection libraries
}
我们把这个参数打印出来:

afterevaluate {
tasks.matching {
it.name.startsWith(‘dex’)
}.each { dx ->
if (dx.additionalParameters == null) {
dx.additionalParameters = []
}
println dx.libraries
}
}

打印出来发现是 build/intermediates/pre-dexed/ 目录里面的 jar 文件,再把 jar 文件解压发现里面就是 dex 文件了。所以 DexMerger 的工作就是合并这里的 dex 。

更改编译环境

buildscript {
//…
dependencies {
classpath ‘com.android.tools.build:gradle:2.1.0-alpha3’
}
}

将 gradle 设置为 2.1.0-alpha3 之后,在项目的 build.gradle 中即使没有设置 multiDexEnabled true 也能够编译通过,但是生成的 apk 包依旧是两个 dex ,我想的是可能为了设置 instantRun 。

解决 65536

Google MultiDex 解决方案:

在 gradle 中添加 MultiDex 的依赖:

dependencies { compile ‘com.android.support:MultiDex:1.0.0’ }

在 gradle 中配置 MultiDexEnable :

android {
buildToolsVersion “21.1.0”
defaultConfig {
// Enabling MultiDex support.
MultiDexEnabled true
}
}

在 AndroidManifest.xml 的 application 中声明:


如果有自己的 Application 了,让其继承于 MultiDexApplication 。

如果继承了其他的 Application ,那么可以重写 attachbaseContext(Context):

@Override
protected void attachbaseContext(Context base) {
super.attachbaseContext(base);
MultiDex.install(this);
}

LinearAlloc

gradle:

afterevaluate {
tasks.matching {
it.name.startsWith(‘dex’)
}.each { dx ->
if (dx.additionalParameters == null) {
dx.additionalParameters = []
}
dx.additionalParameters += ‘–set-max-idx-number=48000’
}
}

–set-max-idx-number= 用于控制每一个 dex 的最大方法个数。

这个参数在查看 dx.jar 找到:

//blablabla…
} else if (parser.isArg("–set-max-idx-number=")) { // undocumented test option
maxNumberOfIdxPerDex = Integer.parseInt(parser.getLastValue());
} else if(parser.isArg(INPUT_LIST_OPTION + “=”)) {
//blablabla…

更多细节可以查看源码:Github – platform_dalvik/Main

FB 的工程师们曾经还想到过直接修改 LinearAlloc 的大小,比如从 5M 修改到 8M: Under the Hood: Dalvik patch for Facebook for Android 。

dexopt && dex2oat

dexopt

当 Android 系统安装一个应用的时候,有一步是对 Dex 进行优化,这个过程有一个专门的工具来处理,叫 DexOpt。DexOpt 是在第一次加载 Dex 文件的时候执行的,将 dex 的依赖库文件和一些辅助数据打包成 odex 文件,即 Optimised Dex,存放在 cache/dalvik_cache 目录下。保存格式为 apk路径 @ apk名 @ classes.dex 。执行 ODEX 的效率会比直接执行 Dex 文件的效率要高很多。

dex2oat

Android Runtime 的 dex2oat 是将 dex 文件编译成 oat 文件。而 oat 文件是 elf 文件,是可以在本地执行的文件,而 Android Runtime 替换掉了虚拟机读取的字节码转而用本地可执行代码,这就被叫做 AOT(ahead-of-time)。dex2oat 对所有 apk 进行编译并保存在 dalvik-cache 目录里。PackageManagerService 会持续扫描安装目录,如果有新的 App 安装则马上调用 dex2oat 进行编译。

NoClassDefFoundError

现在 INSTALL_FAILED_DEXOPT 问题是解决了,但是有时候编译完运行的时候一打开 App 就 crash 了,查看 log 发现是某个类找不到引用。

Build Tool 是如何分包的
为什么会这样呢?是因为 build-tool 在分包的时候只判断了直接引用类。什么是直接引用类呢?举个栗子:

public class MainActivity extends Activity {
protected void onCreate(Bundle savedInstanceState) {
DirectReferenceClass test = new DirectReferenceClass();
}
}

public class DirectReferenceClass {
public DirectReferenceClass() {
InDirectReferenceClass test = new InDirectReferenceClass();
}
}

public class InDirectReferenceClass {
public InDirectReferenceClass() {

}
}

上面有 MainActivity、DirectReferenceClass 、InDirectReferenceClass 三个类,其中 DirectReferenceClass 是 MainActivity 的直接引用类,InDirectReferenceClass 是 DirectReferenceClass 的直接引用类。而 InDirectReferenceClass 是 MainActivity 的间接引用类(即直接引用类的所有直接引用类)。

如果我们代码是这样写的:

public class HelloMultiDexApplication extends Application {
renceClass {
public DirectReferenceClass() {
InDirectReferenceClass test = new InDirectReferenceClass();
}
}

public class InDirectReferenceClass {
public InDirectReferenceClass() {

}
}

上面有 MainActivity、DirectReferenceClass 、InDirectReferenceClass 三个类,其中 DirectReferenceClass 是 MainActivity 的直接引用类,InDirectReferenceClass 是 DirectReferenceClass 的直接引用类。而 InDirectReferenceClass 是 MainActivity 的间接引用类(即直接引用类的所有直接引用类)。

如果我们代码是这样写的:

public class HelloMultiDexApplication extends Application {

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5716454.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存