静态分析
静态分析能做什么?
经过大量实践发现,很多问题其实是可以在产品发布或上线之前就能发现的,然而,由于缺乏相应的工具,导致很多问题被隐藏,并带到了线上,直到在用户侧发生,如:卡顿、崩溃、安全问题等等,通过静态分析,我们可以尽可能早的找出这些潜在的问题和风险,在上线之前将其修复,这也是创立 Booster 这个项目的初衷。
Booster 的静态分析解决了什么问题?
booster-task-analyser在新窗口打开 通过黑/白名单的方式对 APP 进行扫描,并生成相应的分析报告,使得开发者对 APP 的质量有一个更全面和深入的了解,并为更深层次的优化提供思路,包括但不限于:
- 发现潜在的性能问题,如:可能阻塞主线程/ UI 线程的 API 调用;
- 发现风险 API 调用;
- 分析依赖关系;
Analyser 的实现思路
独立的 Task
Booster 的静态分析采用独立的 task 来执行,之所以这样设计,主要有几个方面的考虑:
- 对应用进行静态分析的频率不像构建那么频繁,所以,Task 比 Transformer 更合适;
- CHA (Class Hierarchy Analysis) 需要提前拿到所有类信息,而 Transformer 是流水线处理,也不太合适;
- 静态分析的过程可能会比较慢,作为 Transformer 可能会严重影响构建效率,而且应用的构建并不依赖静态分析的产出物;
Analyser Task 的依赖关系如下图所示:
类继承分析
类继承关系分析对于静态分析至关重要,它决定了分析结果的准确性和全面性,在 Transform 中 CHA 是通过 ClassLoader 来实现的,相对来说比较简单,参见:KlassPool在新窗口打开 & Klass在新窗口打开,主要是解决如何判断两个类型是否有继承关系的问题,Analyser 的 CHA 采用的方式是提前加载所有 Class,然后进行分析,主要有以下几个方面的原因:
- ClassLoader 加载 Class 时,虽然可以不对类进行初始化,但是 ClassLoader 会对 bytecode 进行 verify ,可能会抛出 VerifyError 导致整个分析过程失败;
- 性能开销 —— ClassLoader 加载 Class 的性能跟 ASM 对比相差甚远;
- 除了分析类的继承关系外,还需要分析字段和方法以及注解,通过 Class 反射得到的信息有限;
- 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在新窗口打开 标注过,则可以认为该类或者方法就是线程入口类或者方法。
考虑到一些主流的应用框架也有线程注解,因此,Analyser 对 Event 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"
}
}
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")
}
}
2
3
4
5
6
7
8
9
然后在命令行执行 analyse 任务:
$ ./gradlew analyse
执行成功之后,在 build/reports/ 目录中会生成相应的 dot 格式的报告,可以通过 dot 工具,将 dot 文件转换成 png 格式:
$ find build/reports -name '*.dot' | xargs -t -I{} dot -O -Tpng {}
白名单与黑名单
「白名单」是分析过程中忽略的 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
2
通过命令行指定黑/白名单
$ ./gradlew assembleDebug \
-Pbooster.task.analyser.whitelist=file:///Users/booster/whitelist.txt \
-Pbooster.task.analyser.blacklist=file:///Users/booster/blacklist.txt
2
3
whitelist 和 blacklist 可以是远程的 URL,如:
./gradlew assembleDebug \
-Pbooster.task.analyser.whitelist=https://booster.johnsonlee.io/analyser/whitelist.txt
2