最近一直在优化 booster-task-analyser ,一方面是功能特性的完善,另一方面是性能的优化,在此之前,静态分析的工作是由 booster-transform-lint 完成的,虽然已经很早就开源了这个模块,但是对静态分析的结果一直不是很满意,加上其它一些方面的考虑,索性重写一个,所以就有了 booster-task-analyser,用来替代 booster-transform-lint

重新设计静态分析模块主要是基于几个方面的考虑:

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

所以,booster-task-analyser 基于 Task 来实现:

digraph analyser { node [shape=rect,fillcolor="#41BBD9",style=filled,fontcolor=white,fontname=Helvetica]; analyse [color="#33330000",fillcolor="#F18F01"]; analyseDebug [color="#33330000",fillcolor="#006E90"]; analyseRelease [color="#33330000",fillcolor="#006E90"]; transformClassesWithXxxForDebug [color="#33330000",fillcolor="#99C24D"] transformClassesWithXxxForRelease [color="#33330000",fillcolor="#99C24D"] analyse -> analyseDebug [color="#555555"]; analyse -> analyseRelease [color="#555555"]; analyseDebug -> transformClassesWithXxxForDebug [color="#555555"]; analyseRelease -> transformClassesWithXxxForRelease [color="#555555"]; }

Classpath

在进行静态分析之前,首先需要拿到所有要分析的类,主要由两部分组成:

  • System classes

    也就是 Android SDK 提供的 API,直白一点,就是 ${ANDROID_HOME}/platforms/android-xx/android.jar ,在构建的时候,通过 BaseExtension.getBootClasspath() 就能获取到。

  • APP classes

    APP 的类稍微麻烦一点,有两种实现方式:

    1. 通过分析工程依赖来获取;
    2. 通过 Transform 来获取;

    目前的实现选择了第 2 种——依赖 Transform ,主要是实现起来相对比较方便,偷个懒 :P

本以为加载所有类(将 class 文件解析成 ASM ClassNode)会很耗时,结果近 10 万个类在 15s 左右就完成了,机器配置如下:

  • 处理器:2.5GHz Intel Core i7
  • 内 存:16G 2400 MHz DDR4
  • 存 储:SSD

Entry Point

任何静态分析都需要入口 (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) 标的方法会被识别为主线程入口方法。

反射

在 Android 应用中,为了解决热修复和 Main Dex 过大的问题,会采用反射来将 Application 与其它类的引用关系的打断,详见:我在滴滴做架构(一),如何将反射调用与实际的方法建立联系呢?—— 分析栈帧,当然,这又是另一个话题了,此处省略 5 千字。

Main Looper

在 Android 中,主线程的 Looper & Handler 也算是 Entry Point ,如何确定一个 HandlerLooper 是否在主线程中,也是很具有挑战性的,举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class HomeActivity extends Activit {

private Handler mHandler = new Handler();

public void onResume() {
super.onResume();
Helper.handleEvent(this.mHandler);
}

}

public class Helper {

public static void handleEvent(Handler handler) {
// ...
handler.post(() -> {
// update UI
})
}

}

Call Graph 如图所示:

digraph analyser { graph [bgcolor="transparent",pad="0.555"]; node [shape=box,fillcolor="#7f41BBD9",style=filled,fontcolor=white,fontname=Helvetica];
"HomeActivity.onResume()" [color="#33330000",fillcolor="#F18F01"];
"Activity.onResume()" [color="#33666600",fillcolor="#006E90"];
"Helper.handleEvent(Handler)" [color="#33666600",fillcolor="#99C24D"];
"Handler.post(Runnable)" [color="#33666600",fillcolor="#B5248F"];

"HomeActivity.onResume()" -> "Activity.onResume()" [color="#555555"];
"HomeActivity.onResume()" -> "Helper.handleEvent(Handler)" [color="#555555"];
"Helper.handleEvent(Handler)" -> "Handler.post(Runnable)" [color="#555555"];

}

在以上例子中,Helper.handleEvent(Handler) 方法体中调用了 Handler,但是这个 Handler 是来自其它方法,所以,要对 handler 这个参数追根溯源的话,就得根据 Call Graph 逆向分析:

  1. 找到调用该方法的上一个节点(可能是多个);
  2. 根据上一个节点,找到该节点对应的 INVOKE 指令;
  3. 然后从该指令对应的栈帧(Stack Frame)开始回溯,确定 handler 的来源;
  4. handler 来自 GETFIELD 指令,由此可以确定,该 handlerHomeAcitivity 的成员;
  5. 再从各方法中搜索对 handler 进行赋值的 SETFIELD 指令,最终可以确定是在构造方法中

由此可见,Call Graph 对于静态分析至关重要,当然,这个例子也比较简单,如果 Handler 在多个方法之间作为参数传递的话,算法复杂度就会大大增加。

ClassSet

ClassSet 主要用于实现几个方面的需求:

  1. 根据类名快速查询 ClassNode
  2. 根据类名快速定位该类所属的 classpath —— 文件夹/JAR;

出于性能和复用性角度的考虑,ClassSet 设计成如下形式:

为什么会有 CompositeClassSet 呢?因为当 Anylyser 对应用进行分析的时候,需要访问这些 ClassSet

  1. 仅包含 System classesClassSet

    用于快速查找 Application 及四大组件。

  2. 仅包括 App classesClassSet

    用于构建整个应用的 Call Graph

  3. 包含所有 classesClassSet ,即 System classesApp classes 的并集

    用于类继承关系分析(Class Hierarchy Analysis)。

为了保证查找效率,因此,ClassSet 的实现类都做了缓存。

Class Hierarchy Analysis

类继承关系分析对于静态分析至关重要,它决定了分析结果的准确性和全面性,在此之前,CHA 是通过 ClassLoader 来实现的,相对来说比较很简单,参见:KlassPool & Klass,主要是解决如何判断两个类型是否有继承关系的问题,为什么抛弃原有的方案重新设计 CHA 呢?主要有以下几个方面的原因:

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

在进行类关系分析的时候,还有一个很重要的需求 —— 多态检测,例如:

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
public interface ProfileView {
void updateUserInfo(UserInfo userInfo);
}

public interface ProfilePresenter {
void refreshUserInfo();
}

public class ProfilePresenterImpl implements ProfilePresenter {

private ProfileView mView;

private UserService mService;

public ProfilePresenterImpl(ProfileView view) {
this.mView = view;
}

public void refreshUserInfo() {
this.mService.getUserInfo(userInfo -> {
mView.updateUserInfo(userInfo);
});
}

}

public class ProfileActivity extends Activity, ProfileView {

private ProfilePresenter mPresenter;

public void onCreate(Bundle bundle) {
super.onCreate(bundle);
this.mPresenter = new ProfilePresenterImpl(this);
}

public void onResume() {
super.onResume();
this.mPresenter.refreshUserInfo();
}

public void updateUserInfo(UserInfo userInfo) {
// ...
}

}

如果将 ProfileActivity.onResume() 的代码用 Call Graph 来表示:

digraph analyser { graph [bgcolor="transparent",pad="0.555"]; node [shape=box,fillcolor="#7f41BBD9",style=filled,fontcolor=white,fontname=Helvetica];
"ProfileActivity.onResume()" [color="#33330000",fillcolor="#F18F01"];
"Activity.onResume()" [color="#33666600",fillcolor="#006E90"];
"ProfilePresenter.refreshUserInfo()" [color="#33666600",fillcolor="#99C24D"];
"ProfilePresenterImpl.refreshUserInfo()" [color="#33666600",fillcolor="#CCCCCC",fontcolor="#999999"];
"UserService.getUerInfo(Callback)" [color="#33666600",fillcolor="#CCCCCC",fontcolor="#999999"];

"ProfileActivity.onResume()" -> "Activity.onResume()" [color="#555555"];
"ProfileActivity.onResume()" -> "ProfilePresenter.refreshUserInfo()" [color="#555555"];
"ProfilePresenter.refreshUserInfo()" -> "ProfilePresenterImpl.refreshUserInfo()" [color="#555555",style="dotted"];
"ProfilePresenterImpl.refreshUserInfo()" -> "UserService.getUerInfo(Callback)" [color="#555555"];

}

这里就有一个问题,在上面的 Call Graph 中,从 ProfileActivity.onResume() 是无法到达 ProfilePresenterImpl.updateUserInfo() 的,因为在 ProfileActivity 中调用 updateUserInfo() 方法的引用类型是 ProfilePresenter 而不是 ProfilePresenterImpl,所以,Call Graph 中的每个节点都需有一个属性 —— 是否是 虚方法 ,如果是 虚方法 ,就得将这个 虚方法 与其所属类的所有派生类的 override 方法关联上,这就衍生出了一系列需求:

  1. 根据类名快速查询其派生类;
  2. 根据类名和方法名快速查询 override 了该方法的子类;

总结

从上面的各种例子可以看出,静态分析涉及到的数据结构和算法是相当的复杂,大概有:

  1. Link List
  2. Map
  3. Set
  4. Digraph
  5. Binary Tree
  6. ……

当分析的规模足够大的时候,这对 Analyser 的性能又是一项挑战,可能因为一行代码就会导致性能大相庭径。

Analyser 的第一个版本完成的时候,分析 10 万个类耗时在半小时以上,经过不断的优化,目前已经稳定在 1min 左右,内存峰值在 3G 左右,其中涉及到的优化手段又是另一个话题了,此处省略 1 万字……