APKChecker分析
介绍
Matrix是腾讯开源的一个APM项目,对于android优化方面无从下手的话,感觉可以研究一下
这里主要分析其中一个模块APKChecker,这个模块和其它模块的关联性不是很大,可以单独提取出来、主要就是检测apk内重复资源文件,大小,未使用文件,
stl重复引入等功能。如果有自己的想法也可以进行扩展.
使用流程
- 指定配置文件运行
./app/build/install/app/bin/app --config ./apk-checker-config.json
配置文件解析
1. 配置文件参数解析
- –input 指定一个文件夹路径 apk文件的目录,–apk没有指定默认在这个目录里搜第一个后缀是apk的文件
- –unzip 解压apk后的目录路径,不指定,默认是apk所在的路径
- –apk 指定apk文件路径
- –mappingTxt 指定类的混淆映射文件路径
- –resMappingTxt 指定资源混淆映射文件路径
- –output 输出结果文件路径,不指定默认是apk路径
- –format 输出结果的形式.支持json 和html格式
- –formatJar 这个比较吊了,可以拓展TaskJsonResult,TaskHtmlResult,这两个输出结果形式,配置一个jar包给一个注册类,动态加载.(感觉边角功能做的有点多了。)
- –formatConfig 一个数组干嘛的呢
- –log 指定日志等级 V D I W E,指定一个等级 小于这个等级的日志就没办法输出了
- –options 一个数组JsonArray 每一个JsonObject必须指定一个name,对应一个ApkTask的任务名称。剩余的属性,是作为这ApkTask的参数,根据name利用TaskFactory创建任务清单
- –rTxt -options里unUsedResource任务参数.指定的资源映射文件路径
2. 配置文件解析代码主要在ApkJob类 readConfigFile方法里
- 你也可以通过命令行的形式指定这些参数
ApkTask
- ApkTask主要就是具体apk检测项目的执行基类
- ApkTask实现Callable接口,可以线程池执行得到执行结果TaskResult
- TaskResult最后通过JobResult写入文件
- ApkTask主要通过TaskFactory集中生成.
- ApkTask相关的结构图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2W0Ae2hy-1631069452664)(./apkarch.png)]
UnzipTask
- 负责解压apk,读取类混淆文件和,资源混淆文件
ManifestAnalyzeTask
- 负责Manifest.xml解析工作,主要利用apktool.jar里的 AXmlResourceParser类解析
- 最后以键值对的形式保存到TaskJsonResult里面
CountClassTask
- 对所有dex文件进行类分析
- 解析每一个dex文件,以dex文件名为基础,解析dex里的包名,包对应的类的层次去解析
- 这里注意代码混淆文件的利用,获取真正的类名。
CountRTask
- 统计所有R类的数量,和R里面的资源数量
- 解析每一个dex文件,先找到R类(也有可能是资源混淆之后的.R结尾),统计里面的R类字段的数量
DuplicateFileTask
- 重复文件检测
- 遍历解压后的文件夹,对每一个文件内容求md5值,md5作为key,文件名集合作为value
- (感觉可以优化一下,没必要对所有文件内容都求md5,可以先对比收尾等字节的数据.过滤掉一部分)然后再,通过md5求剩下的文件
FindNonAlphaPngTask
- 找到非透明的png文件
- 主要遍历png 和.9.png文件
- bufferedImage.getColorModel().hasAlpha()通过这个函数检测
MethodCountTask
- 解析外部类对应的方法数
- 解析内部类对应的方法数
MultiLibCheckTask
- 多架构so库检测
- 主要检测lib文件夹下是否存在多个目录
MultiSTLCheckTask
- 检测so库是否重复引入stl库
- nm命令对符号表进行过滤检测std::
/**
* -C, --demangle[=STYLE] 将低级符号名解码(demangle)成用户级名字,比如去除编译时添加的前置下划线,这样可以使得 C++ 函数名具有可读性。不同的编译器符号修饰风格不同,可以使用 =STYLE 参数来选择合适的解码风格
* -D, --dynamic:显示动态符号。该任选项仅对于动态目标(例如特定类型的共享库)有意义
*
* */
private boolean isStlLinked(File libFile) throws IOException, InterruptedException {
ProcessBuilder processBuilder = new ProcessBuilder(toolnmPath, "-D", "-C", libFile.getAbsolutePath());
Process process = processBuilder.start();
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line = reader.readLine();
while (line != null) {
String[] columns = line.split(" ");
Log.d(TAG, "%s", line);
if (columns.length >= 3 && columns[1].equals("T") && columns[2].startsWith("std::")) {
return true;
}
line = reader.readLine();
}
reader.close();
process.waitFor();
return false;
}
ResProguardCheckTask
- 检测资源是否混淆
- 检测是否配置资源混淆目录
- 检测资源文件名称是否混淆过后的,不是混淆的就没用资源混淆
ShowFileSizeTask
- 统计解压后的目录各个文件条目的大小
UnCompressedFileTask
- 根据后缀名统计文件大小
- 可以配置–suffix参数,指定后缀名
UnstrippedSoCheckTask
- 检测是否剥离so文件的符号表
- 通过nm命令查看符号表,是否存在符号表
private boolean isSoStripped(File libFile) throws IOException, InterruptedException {
ProcessBuilder processBuilder = new ProcessBuilder(toolnmPath, libFile.getAbsolutePath());
Process process = processBuilder.start();
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
String line = reader.readLine();
boolean result = false;
if (!Util.isNullOrNil(line)) {
Log.d(TAG, "%s", line);
String[] columns = line.split(":");
if (columns.length == 3 && columns[2].trim().equalsIgnoreCase("no symbols")) {
result = true;
}
}
reader.close();
process.waitFor();
return result;
}
UnusedAssetsTask
- 查找未使用的asset 文件
- 先找到所有asset文件存储绝对路径
- 读取smali代码,查找const-string 声明的字符串,找到asset文件名结尾的assets路径
- 所有assets文件路径 删除引用的路径 剩下的就是未引用的assets路径
UnusedResourceTask
- 找到未使用的资源文件
- 这里思路和UnusedAssetsTask思路一样
- 就是smali找引用的时候,不仅仅通过const-string声明的变量,还有sget sput,和array-data指令去寻找资源id