Matrix-APKChecker源码分析(1)

工具简介

微信自研 APM 利器,Matrix 正式开源了

Matrix-ApkChecker — Apk 分析减包利器

Matrix是微信开源的APM工具,APKChecker是其中针对APK文件做静态分析的工具,是Matrix的一部分。上面两篇腾讯云的相关文章,介绍了Matrix&ApkChecker的一些基本功能。本文针对APKChecker的源码做一个简单的分析,聊一下该工具各个功能的实现原理。采用的是Matrix工程,master分支4月16号的代码为分析源码。

依赖库

简单看一下工程目录和gradle配置的依赖关系。显然,前文链接中的ApkChecker的代码目录是matrix-apk-canary。

  • libs文件夹中,有apktool的jar包,反编译工具在这里也是意料之中
  • resources目录中,有android的framework jar包,分析APK文件一些基础数据类型在Android framework层中定义
  • gradle配置中,可以看到依赖了常用的gson工具类、Android的common工具包、本地工程matrix-common
  • 走读一下matrix-common,基本上可以判定该module主要是matrix公用的数据结构与工具组件

代码结构

从代码目录上看,它的包名目录定义的十分清晰,基本上可以一目了然:

  • exception:异常定义
  • job:工作任务,包含任务管理、配置和常量定义
  • output:输出
  • result:分析结果相关
  • task:分析任务,基本可以通过任务命名直接对应上相关文档中的功能
  • ApkChecker类:最后输出jar包的程序入口

简单梳理一下类图关系,能够进一步了解源码的设计思路。

可以看出,基本上可以分为四部分:

  • 左侧是入口类和核心任务类,里面封装了主要的分析实现过程
  • 上侧是基础的任务实现部分,图中省略了很多功能任务类,只列了三个,可以看到一个简单的继承关系,和一个工厂模式的处理
  • 右侧是任务结果部分,可以看到主要有Json和html两种格式。这部分主要是针对每个任务的结果输出,同样是一个继承关系和相应的工厂处理
  • 下侧是整个分析任务的结果输出,可以简单认为它是对TaskResult的整理和真正的结果文件化输出。依然是两种格式的继承关系和工厂处理

综合起来看,如果对执行流程感兴趣,去看ApkJob类的实现就好了;对每个分析功能感兴趣,去task包目录下找对应的功能实现类就好了;对最后分析结果是如何输出的感兴趣,可以查一下TaskResult和JobResult相关的实现就好了。

核心流程走读

这里简单分析入口类ApkChecker和任务管理类ApkJob。

ApkChecker

入口类只有不到一百行的代码,十分简洁。主要除了封装了main函数入口,还处理了输入参数异常情况下输出HELP提示的过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String... args) {
if (args.length > 0) {
ApkChecker m = new ApkChecker();
m.run(args);
} else {
System.out.println(INTRODUCT + HELP);
System.exit(0);
}
}
private void run(String[] args) {
ApkJob job = new ApkJob(args);
try {
job.run();
} catch (Exception e) {
e.printStackTrace();
System.exit(-1);
}
}

很明显,main函数封装的主要是ApkJob,核心函数是它的run接口。此处注意,输入参数args,作为ApkJob的构造函数参数传入,想来是作为了该类的成员变量处理了。

ApkJob

该类有530行左右的代码,如前文类图描述的,成员属性包括了两个分析任务的ApkTask的列表、一个输出结果工具JobResult的列表。除此之外,还有前面传进来的参数args和相关的配置参数描述类JobConfig、一个多线程执行器ExecutorService。构造方法中基本上做一些初始化工作。

run函数比较函数流程比较易懂,只有十几行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void run() throws Exception {
if (parseParams()) {
ApkTask unzipTask = TaskFactory.factory(TaskFactory.TASK_TYPE_UNZIP, jobConfig, new HashMap<String, String>());
preTasks.add(unzipTask);
for (String format : jobConfig.getOutputFormatList()) {
JobResult result = JobResultFactory.factory(format, jobConfig);
if (result != null) {
jobResults.add(result);
} else {
Log.w(TAG, "Unknown output format name '%s' !", format);
}
}
execute();
} else {
ApkChecker.printHelp();
}
}
  • 解析参数

    解析参数函数涉及到代码量,比较大。因为支持配置文件和参数两种形式的参数,核心解析函数parseGlobalParams()长达126行,经过解析和校验后,在成功的情况下,jobConfig成员被参数设置好,用于后续的分析任务。

  • 在预处理任务列表中添加一个解压任务

    预处理任务列表在整个类中,只有此处添加了一个解压任务。之所以设计成列表,应该是考虑代码的可拓展性。从业务上说,在每项检查点的任务执行之前,要先把目标APK解压出来,也是应有之意。

  • 处理配置的输出格式,将所有格式加入输出结果工具列表

    可以理解是参数解析好之后,通过传入参数做的第一件事。就是先把输出工具处理好,为后续分析结果的输出做好准备。

  • 执行execute 函数

    预处理执行完,就是正经的任务分析流程了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    private void execute() throws Exception {
    try {
    for (ApkTask preTask : preTasks) {
    preTask.init();
    TaskResult taskResult = preTask.call();
    if (taskResult != null) {
    TaskResult formatResult = null;
    for (JobResult jobResult : jobResults) {
    formatResult = TaskResultFactory.transferTaskResult(taskResult.taskType, taskResult, jobResult.getFormat(), jobConfig);
    if (formatResult != null) {
    jobResult.addTaskResult(formatResult);
    }
    }
    }
    }
    for (ApkTask task : taskList) {
    task.init();
    }
    List<Future<TaskResult>> futures = executor.invokeAll(taskList, timeoutSeconds, TimeUnit.SECONDS);
    for (Future<TaskResult> future : futures) {
    TaskResult taskResult = future.get();
    if (taskResult != null) {
    TaskResult formatResult = null;
    for (JobResult jobResult : jobResults) {
    formatResult = TaskResultFactory.transferTaskResult(taskResult.taskType, taskResult, jobResult.getFormat(), jobConfig);
    if (formatResult != null) {
    jobResult.addTaskResult(formatResult);
    }
    }
    }
    }
    executor.shutdownNow();
    for (JobResult jobResult : jobResults) {
    jobResult.output();
    }
    Log.d(TAG, "parse apk end, try to delete tmp un zip files");
    FileUtils.deleteDirectory(new File(jobConfig.getUnzipPath()));
    } catch (Exception e) {
    Log.e(TAG, "Task executor execute with error:" + e.getMessage());
    throw e;
    }
    }

    该函数45行,分开看还是很好理解的。

    • 第一个for循环。执行预处理列表任务,主要是解压目标APK的任务,并输出结果
    • 中间两个for循环及相关部分。初始化参数配置指定的每个检查任务;executor多线程执行每个任务;将每个任务的检查结果依次添加到输出结果中;关闭executor
    • 第三个for循环及相关部分。结果输出工具把分析结果按照参数指定的格式依次输出到文件中;删除解压的APK文件。

小结

这里主要分析了Matrix/ApkChecker的代码结构和主要执行流程。可以大致总结出几点:

  • 总体上,代码在各层级命名、包划分、结构设计、函数实现等各方面可读性都很强
  • 利用继承和组合特性,使用工厂模式,让代码可拓展性也不错。比如新增一个分析功能,只需要实现一个ApkTask和相关参数类型即可;新增一种输出格式,只需要新增JobResult/TaskResult相关子类和关系即可
  • 参数解析部分代码有些冗余,不太利于拓展和阅读

下一篇会学习一下具体检查任务的实现。

感谢您赏个荷包蛋~