1 探针策略


  • 在一串字节码指令中插入这些探针,只要该探针被执行了,说明其之前的指令都被执行了
    JaCoCo探针策略原理及案例总结_数据
    JaCoCo探针策略原理及案例总结_analyzer_02
  • 注意方法结束了是在 return 指令前放置探针哦
    JaCoCo探针策略原理及案例总结_数据_03
  • 跳转语句的记录
    JaCoCo探针策略原理及案例总结_analyzer_04
  • 条件语句
    JaCoCo探针策略原理及案例总结_字节码_05
    JaCoCo探针策略原理及案例总结_数据_06

2 探针特点

探测的唯一目的是记录它至少执行过一次。探测器不记录它被调用的次数或收集任何时间信息。后者超出了代码覆盖率分析的范围,更多的是在性能分析工具的目标中


  • 最小的运行时间开销
  • 对应用程序代码无副作用
  • 线程安全
  • 记录字节码的执行
  • 标识不同类型探针
    使用的 boolean 数组记录对应的指令是否被执行

3 为什么最小的性能开销?

JaCoCo探针策略原理及案例总结_字节码_07

javap -c Fun

...
0: getstatic #2 // Field $assertionsDisabled:Z
3: ifne 18
6: iload_1
7: ifne 18
10: new #3 // class java/lang/AssertionError
13: dup
14: invokespecial #4 // Method java/lang/AssertionError."<init>":()V
17: athrow
18: return
...

  • 其实翻译过来代码就是这样子
    JaCoCo探针策略原理及案例总结_数据_08
  • 通过将这种原理用于 jacoco,降低了性能开销
    JaCoCo探针策略原理及案例总结_analyzer_09

4 如何实现代码注入

JaCoCo探针策略原理及案例总结_字节码_10

JaCoCo通过ASM在字节码中插入Probe指针(探测指针),每个探测指针都是一个BOOL变量(true表示执行、false表示没有执行),程序运行时通过改变指针的结果来检测代码的执行情况(不会改变原代码的行为).

增量注入

JaCoCo默认全量注入.

源码中注入的逻辑主要在ClassProbesAdapter

JaCoCo探针策略原理及案例总结_数据_11

ASM在遍历字节码时,每次访问一个方法定义,都会回调这个类的​​visitMethod​​方法 ,在​​visitMethod​​方法中再调用​​ClassProbeVisitor​​的​​visitMethod​​方法,并最终调用​​MethodInstrumenter​​完成注入。部分代码片段如下:

@Override
public final MethodVisitor visitMethod(final int access, final String name,
final String desc, final String signature,
final String[] exceptions) {
final MethodProbesVisitor methodProbes;
final MethodProbesVisitor mv = cv.visitMethod(access, name, desc,
signature, exceptions);
if (mv == null) {
// 无论如何,我们都需要访问该方法,否则探针的ID无法重现
methodProbes = EMPTY_METHOD_PROBES_VISITOR;
} else {
methodProbes = mv;
}
return new MethodSanitizer(null, access, name, desc, signature,
exceptions) {

@Override
public void visitEnd() {
super.visitEnd();
LabelFlowAnalyzer.markLabels(this);
final MethodProbesAdapter probesAdapter = new MethodProbesAdapter(
methodProbes, ClassProbesAdapter.this);
if (trackFrames) {
final AnalyzerAdapter analyzer = new AnalyzerAdapter(
, access, name, desc,
probesAdapter);
probesAdapter.setAnalyzer(analyzer);
methodProbes.accept(this, analyzer);
} else {
methodProbes.accept(this, probesAdapter);
}
}
};
}

自动获取运行时数据

代码中通过反射执行下面的函数来获取运行时数据,并保存到当前执行代码的设备中:

org.jacoco.agent.rt.RT.getAgent().getExecutionData(false)

生成报告时需要用到运行时数据,为了生成的覆盖率报告更准确、开发同学用起来更方便,分别在如下时机把运行时数据保存到当前设备中:


  • 每个页面执行onDestory时
  • 程序发生崩溃时
  • 收到特定广播(一个自定义的广播,在执行生成覆盖率报告的task前发送)时

并在生成覆盖率报告之前把设备中的运行时数据同步到本地开发环境中。

上面可以看到,因为获取时机比较多,可能会得到多份运行时数据,对于这些数据,可以通过JaCoCo的mergeTask把ClassId相同的运行时数据进行merge。如下图所示,JaCoCo会对ClassId相同的运行时数据进行merge,并对相同位置的probe指针取或:

JaCoCo探针策略原理及案例总结_数据_12