1. 需求驱动 ---精准监控统计apk包体积
目前各应用APP 包体积,日渐增大,各个公司都在提出精简包体积的要求。这个过程中,除了图片,so库,第三方library,混淆效率这些维度进行分析,统计,代码层面,统计的力度比较低,本博文重点从代码文件角度,分析统计。
2. 背景:大部分公司都是各部门之间分工合作完成整个apk的开发。如果部门老大希望精确知道每个部门或模块,到底为最终版apk贡献了多少分量。你该怎么做呢?
首先想到的是解压最终apk包,看看到底各部分之间占了多少。资源跟so库,基本上可以根据对应索引,写脚本精确找到各个图片,.xml这些到底属于哪个模块,甚至哪位程序员所贡献。
代码如何统计呢,有人会想到反编译,或者直接dx工具编译class文件。但问题是dex文件最终会经过系统的多种处理,混淆,对齐,最关键是压缩(比如我们的apk看压缩比1:3)。这方法粒度就太粗了。
Android系统提供的multidex工具,是自动分配dex文件。
如果能够手动指定每个class类最终进到某个dex中,这个就能够精确的知道每个模块的代码在apk中,究竟占了多了百分比。以便建立长期机制,定期去监控各模块代码增长情况了。如何做呢?
3.先来看下,android官方给出的编译流程图
基本概念:dex分包前提,即内置的java编译器已经将java文件编译成为.class文件(当然还有第三方库及.class文件)。再通过dx工具或gradle task编译生成dex文件。
3. multidex 分包源码研究
源码下载地址:https://android.googlesource.com/platform/tools/base/+/gradle_2.2.2/build-system/
3.1.生成maindexlist核心源码
MainDexListBuilder.java
This is a command line tool used by mainDexClasses script to build a main dex classes list.
这个类主要用来生成MaindexList
public MainDexListBuilder(boolean keepAnnotated, String rootJar, String pathString)
throws IOException {
ZipFile jarOfRoots = null;
Path path = null;
try {
try {
jarOfRoots = new ZipFile(rootJar);
} catch (IOException e) {
throw new IOException("\"" + rootJar + "\" can not be read as a zip archive. ("
+ e.getMessage() + ")", e);
}
path = new Path(pathString);
ClassReferenceListBuilder mainListBuilder = new ClassReferenceListBuilder(path);
mainListBuilder.addRoots(jarOfRoots);
for (String className : mainListBuilder.getClassNames()) {
filesToKeep.add(className + CLASS_EXTENSION);
}
if (keepAnnotated) {
keepAnnotated(path);
}
} finally {
try {
jarOfRoots.close();
} catch (IOException e) {
// ignore
}
if (path != null) {
for (ClassPathElement element : path.elements) {
try {
element.close();
} catch (IOException e) {
// keep going, lets do our best.
}
}
}
}
}
使用到的action如下:
MultiDexTransform
// Inputs
@NonNull
private final File manifestKeepListProguardFile;
@Nullable
private final File userMainDexKeepProguard;
@Nullable
private final File userMainDexKeepFile;
@NonNull
private final VariantScope variantScope;
@Nullable
private final File includeInMainDexJarFile;
private final boolean keepRuntimeAnnotatedClasses;
// Outputs
@NonNull
private final File configFileOut;
@NonNull
private final File mainDexListFile;
3.2分dex生成的核心代码分析
主要属性:
生成规律的classes2.dex,classes3.dex......classesN.dex 的代码
4 hack的主要思路
很多人优先想到的是hook进某task,但前提是Option属性要支持,注意,build.gradle 1.X 版本与2.x版本差距比较大,网上流传的很多配置都过时了。
DxOptions{
dexInProcess = false
additionalParameters + = “--main-dex-list=$projectDir/maindexlist.txt”.toString()
additionalParameters + = “--secondary-dexes-list=$projectDir/secondarydexes.txt”.toString()
additionalParameters + = “--minimal-main-dex”
}
1)其中maindexlist.txt,secondarydxeslist.txt在住工程文件root目录下。
2)将dexInProcess 设置为false 才能够强制将dx生成过程,指向本地Build.tools中的dx.jar
3)dx.jar的生成过程,是整个hack过程的关键。
3.hack核心源码
这里与上面dxOption属性对应
这里就是生成的hack版dx.jar中生成secondclasses.dex的核心代码,理论上是可以直接指定每一个dex的名称的。但系统重新加载时,直接忽略了dex名称,而仅仅遍历dex文件目录,所以即使修改了dex文件名,也不会最终生效。
生成dex文件后,其余混淆,对齐,签名,这些与原有逻辑一致,至此dex分拆工作就完成了。