Android-Dex分包最全总结:含Facebook解决方案,【面试总结

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 {
/** {@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();
}
}
}

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

public final class DexFormat {
/**

  • 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;
    }

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 。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
img

系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!**

因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-jmx6TlHm-1710916804831)]
[外链图片转存中…(img-Q1FRofHk-1710916804832)]
[外链图片转存中…(img-Q3kT2mF1-1710916804832)]
[外链图片转存中…(img-PGetn1bJ-1710916804833)]

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
[外链图片转存中…(img-lkYtOytc-1710916804833)]

  • 19
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值