dex-method-counts
Github链接: https://github.com/mihaip/dex-method-counts
README文件内的描述为:
Simple tool to output per-package method counts in an Android DEX executable grouped by package, to aid in getting under the 65,536 referenced method limit.
即它是一个用于统计Android的dex文件中方法数的工具。
启动流程
根据README文件内的使用方法可以看到,该项目入口是一个shell脚本(Windows下是.bat批处理文件)
这个shell脚本的内容不多,前面copy了一段sh-realpath开源库的代码,看名字知道是用于*nix系统查询文件真实地址的,先略过。
剩下的主体部分非常简单,基本上只做了一件事,就是运行一个可执行的jar文件:
# Alternatively, this will extract any parameter "-Jxxx" from the command line
# and pass them to Java (instead of to dexdeps).
while expr "x$1" : 'x-J' >/dev/null; do
opt=`expr "$1" : '-J\(.*\)'`
javaOpts="${javaOpts} -${opt}"
shift
done
if [ "$OSTYPE" = "cygwin" ] ; then
jarpath=`cygpath -w "$libdir/$jarfile"`
else
jarpath="$libdir/$jarfile"
fi
exec java $javaOpts -jar "$jarpath" "$@"
再看项目的结构,是一个典型的使用Gradle构建(也有Ant配置)的Java工程
那么,这个项目的主要启动流程就是:
- 使用Gradle/Ant构建项目,生成可执行的jar文件
- 使用脚本运行这个可执行文件
主要工作流程
既然是一个Java工程,那入口肯定就是main函数了,看一下里面主要做了什么:
String[] inputFileNames = parseArgs(args);
int overallCount = 0;
for (String fileName : collectFileNames(inputFileNames)) {
System.out.println("Processing " + fileName);
DexCount counts;
if (countFields) {
counts = new DexFieldCounts(outputStyle);
} else {
counts = new DexMethodCounts(outputStyle);
}
List<RandomAccessFile> dexFiles = openInputFiles(fileName);
for (RandomAccessFile dexFile : dexFiles) {
DexData dexData = new DexData(dexFile);
dexData.load();
counts.generate(dexData, includeClasses, packageFilter, maxDepth, filter);
dexFile.close();
}
counts.output();
overallCount = counts.getOverallCount();
}
System.out.println(String.format("Overall %s count: %d", countFields ? "field" : "method", overallCount));
基本流程:
- 根据输入参数找到对应的要统计的文件
- 根据输入参数配置一些环境变量(如统计属性还是方法、需不需要统计类、最大统计深度等等)
- 如果文件名后缀是.apk或.jar时先解压,提取到里面的.dex文件
对每一个.dex文件都作为RandomAccessFile打开,然后进行如下操作:
for (RandomAccessFile dexFile : dexFiles) {
DexData dexData = new DexData(dexFile); // DexData
dexData.load(); // DexData
counts.generate(dexData, includeClasses, packageFilter, maxDepth, filter); // DexData
dexFile.close(); // DexData
}
那么其中有两个重点,就是DexData这个类是如何解析.dex文件的,以及counts类进行统计的逻辑。
DexData解析.dex文件
根据README文件内的说明可以看到,DexData类是AOSP源代码中提供用于解析.dex文件的官方工具,包名也是 com.android.dexdeps。
.dex文件是有公开规范的二进制文件,简单了解其结构可以参考这篇文章:《Dex文件结构》
DexData文件代码比较清晰,load()方法的内容如下:
public void load() throws IOException {
parseHeaderItem(); // .dex
loadStrings(); //
loadTypeIds();
loadProtoIds();
loadFieldIds();
loadMethodIds(); // id
loadClassDefs(); //
markInternalClasses(); //
}
这些方法内部基本就是按照固定字节取出相应的信息,然后用数组保存起来了。
值得注意的是,methodId仅仅只是方法的序号,方法名需要用这个序号去前面String查询,其他的id也同理。
文件结构中可以看到,methodId的长度是4个字节一共32位,最多可以索引2^31也就是2147483648个方法
那么Android系统的65536(2^16)方法数上限就不是这里索引长度限制的。
统计方法数
在项目目录里面除了DexData相关工具和Main类外,就只有三个类了:DexCount、DexMethodCount和DexFieldCount
其中DexCount是一个抽象类,定义了一些数据结构、枚举类和输入输出格式,而DexMethodCount和 DexFieldCount都继承于DexCount。
以DexMethodCount类为例,看看里面做了些什么:
for (MethodRef methodRef : methodRefs) {
String classDescriptor = methodRef.getDeclClassName();
String packageName = Output.packageNameOnly(classDescriptor);
overallCount++;
if (outputStyle == OutputStyle.TREE) {
//... package
} else if (outputStyle == OutputStyle.FLAT) {
//... package
}
}
看代码,其实DexData工具解析完成后所有的方法定义就已经在dexData.mMethodIds数组里面了,数组的长度就是这个.dex文件内的方法数
DexMethodCount做了很多额外的工作,用于支持不同参数的调用。
如统计属性还是方法、需不需要统计类、最大统计深度和输出格式等等,
对我们比较有用的功能就是根据methodRef.getDeclClassName()来得到这个方法所在的包,然后进行分类统计。