dex-method-counts 工具分析

阅读时间 约 2 分钟

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工程

那么,这个项目的主要启动流程就是:

  1. 使用Gradle/Ant构建项目,生成可执行的jar文件
  2. 使用脚本运行这个可执行文件

主要工作流程

既然是一个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));

基本流程:

  1. 根据输入参数找到对应的要统计的文件
  2. 根据输入参数配置一些环境变量(如统计属性还是方法、需不需要统计类、最大统计深度等等)
  3. 如果文件名后缀是.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()来得到这个方法所在的包,然后进行分类统计。