静态分析

静态分析能做什么?

经过大量实践发现,很多问题其实是可以在产品发布或上线之前就能发现的,然而,由于缺乏相应的工具,导致很多问题被隐藏,并带到了线上,直到在用户侧发生,如:卡顿、崩溃、安全问题等等,通过静态分析,我们可以尽可能早的找出这些潜在的问题和风险,在上线之前将其修复,这也是创立 Booster 这个项目的初衷。

Booster 的静态分析解决了什么问题?

booster-task-analyser在新窗口打开 通过黑/白名单的方式对 APP 进行扫描,并生成相应的分析报告,使得开发者对 APP 的质量有一个更全面和深入的了解,并为更深层次的优化提供思路,包括但不限于:

  1. 发现潜在的性能问题,如:可能阻塞主线程/ UI 线程的 API 调用;
  2. 发现风险 API 调用;
  3. 分析依赖关系;

Analyser 的实现思路

独立的 Task

Booster 的静态分析采用独立的 task 来执行,之所以这样设计,主要有几个方面的考虑:

  1. 对应用进行静态分析的频率不像构建那么频繁,所以,TaskTransformer 更合适;
  2. CHA (Class Hierarchy Analysis) 需要提前拿到所有类信息,而 Transformer 是流水线处理,也不太合适;
  3. 静态分析的过程可能会比较慢,作为 Transformer 可能会严重影响构建效率,而且应用的构建并不依赖静态分析的产出物;

Analyser Task 的依赖关系如下图所示:

analyseranalyseanalyseanalyseDebuganalyseDebuganalyse->analyseDebuganalyseReleaseanalyseReleaseanalyse->analyseReleasetransformClassesWithXxxForDebugtransformClassesWithXxxForDebuganalyseDebug->transformClassesWithXxxForDebugtransformClassesWithXxxForReleasetransformClassesWithXxxForReleaseanalyseRelease->transformClassesWithXxxForRelease

类继承分析

类继承关系分析对于静态分析至关重要,它决定了分析结果的准确性和全面性,在 TransformCHA 是通过 ClassLoader 来实现的,相对来说比较简单,参见:KlassPool在新窗口打开 & Klass在新窗口打开,主要是解决如何判断两个类型是否有继承关系的问题,AnalyserCHA 采用的方式是提前加载所有 Class,然后进行分析,主要有以下几个方面的原因:

  1. ClassLoader 加载 Class 时,虽然可以不对类进行初始化,但是 ClassLoader 会对 bytecode 进行 verify ,可能会抛出 VerifyError 导致整个分析过程失败;
  2. 性能开销 —— ClassLoader 加载 Class 的性能跟 ASM 对比相差甚远;
  3. 除了分析类的继承关系外,还需要分析字段和方法以及注解,通过 Class 反射得到的信息有限;
  4. Task 相对于Transform 比较独立,如果在 Transform 的过程中加载所有的 Class ,可能导致内存吃紧,甚至 OOM

静态分析入口

任何静态分析都需要入口 (Entry Point),如果是普通的程序,一般都是 main 方法,而对于 Android 应用来说,主要是 Application 、四大组件以及 XML 布局等等,所以,首先要找到这些入口。

四大组件

Application 及四大组件都在 AndroidManifest.xml 里,通过 mergedManifests在新窗口打开 就能获取到合并后的 AndroidManifest.xml

自定义 View

查找自定义 View 最直接的方法就是解析 Layout XML ,通过 mergeRes在新窗口打开 就能获取到,只不过是 AAPT2 的产物 —— flat 文件,这也就是 booster-aapt2在新窗口打开 模块的由来。

通过实测发现:解析 flat 文件的速度不如直接解析 XML 源文件,所以,最终的实现只解析了 flat 文件的 header 部分,然后通过 header 定位到源文件的路径。

线程注释标注的方法和类

Android 本身提供了 Thread Annotations在新窗口打开,帮助编译器和静态分析工具提升代码检查的准确性,所以,只要有类或者方法用 Thread Annotations在新窗口打开 标注过,则可以认为该类或者方法就是线程入口类或者方法。

考虑到一些主流的应用框架也有线程注解,因此,AnalyserEvent Bus 做了支持,通过 @Subscribe(threadMode = MAIN) 标的方法会被识别为主线程入口方法。

如何使用

首先在 build.gradle 中引用 booster-task-analyser

buildscript {
    ext.booster_version = "4.16.3"

    dependencies {
        classpath "com.didiglobal.booster:booster-gradle-plugin:$booster_version"
        classpath "com.didiglobal.booster:booster-task-analyser:$booster_version"
    }

}
1
2
3
4
5
6
7
8
9
buildscript {
    val booster_version = "4.16.3"

    dependencies {
        classpath("com.didiglobal.booster:booster-gradle-plugin:$booster_version")
        classpath("com.didiglobal.booster:booster-task-analyser:$booster_version")
    }

}
1
2
3
4
5
6
7
8
9

然后在命令行执行 analyse 任务:

$ ./gradlew analyse
1

执行成功之后,在 build/reports/ 目录中会生成相应的 dot 格式的报告,可以通过 dot 工具,将 dot 文件转换成 png 格式:

$ find build/reports -name '*.dot' | xargs -t -I{} dot -O -Tpng {}
1

白名单与黑名单

「白名单」是分析过程中忽略的 API,「黑名单」是分析过程中要匹配的 API,Booster 内置了 whiltelist.txt在新窗口打开blacklist.txt在新窗口打开,这些都是项目实践经验所得,当然,Booster 也支持自定义「白名单」与「黑名单」。

通过 gradle.properties 指定黑/白名单

booster.task.analyser.whitelist=file:///Users/booster/whitelist.txt
booster.task.analyser.blacklist=file:///Users/booster/blacklist.txt
1
2

通过命令行指定黑/白名单

$ ./gradlew assembleDebug \
    -Pbooster.task.analyser.whitelist=file:///Users/booster/whitelist.txt \
    -Pbooster.task.analyser.blacklist=file:///Users/booster/blacklist.txt
1
2
3

whitelistblacklist 可以是远程的 URL,如:

./gradlew assembleDebug \
    -Pbooster.task.analyser.whitelist=https://booster.johnsonlee.io/analyser/whitelist.txt
1
2