Android端代码染色原理及技术实践( 二 )

 
Test5方法是一个多条件的if语句,可以看出来将两个组合条件拆分成单一条件,并进行反转 。
这样做的好处:可以完整统计到每个条件分支的执行情况,各种条件都会插入探针,保证了完整的覆盖,而反转操作再配合GOTO指令可以更简单的插入探针,这里可以看出JaCoCo的处理非常巧妙 。
//源码,if有多个条件public static void Test5(int a,int b) {if(a>10 || b>10){a=a+10;}a=a+12;}?//jacoco处理后的字节码 。public static void Test5(int a, int b) {boolean[] var2;label15: {var2 = $jacocoInit();if (a > 10) {var2[14] = true;} else {if (b <= 10) {var2[15] = true;break label15;}var2[16] = true;}a += 10;var2[17] = true;}a += 12;var2[18] = true;} 
可以通过测试报告看出来,标记为黄色代表分支执行情况覆盖不完整,标记为绿色代表分支所有条件都执行完整了 。

Android端代码染色原理及技术实践

文章插图
 

Android端代码染色原理及技术实践

文章插图
 
代码块的执行情况
理论上只要在每行代码前都插入探针即可, 但这样会有性能问题 。JaCoCo考虑到非方法调用的指令基本都是按顺序执行的,因此对非方法调用的指令不插入探针, 而对方法调用的指令之前都插入探针 。
Test6方法内在调用Test方法前都插入了探针 。
public static void Test6(int a, int b) {boolean[] var2 = $jacocoInit();a += b;b = a + a;var2[19] = true;Test();int var10000 = a + b;var2[20] = true;Test();var2[21] = true;} 
源码解析
通过上面的示例,我们暂时通过表面现象理解了探针插入策略 。知其然不知其所以然,我们通过源码分析论证一下JaCoCo的真实逻辑,看看JaCoCo是如何通过ASM,来实现探针插入策略的 。
源码MethodProbesAdapter.java类中,通过needsProbe方法判断Lable前面是否需要插入探针 。
@Overridepublic void visitLabel(final Label label) {if (LabelInfo.needsProbe(label)) {if (tryCatchProbeLabels.containsKey(label)) {probesVisitor.visitLabel(tryCatchProbeLabels.get(label));}probesVisitor.visitProbe(idGenerator.nextId());}probesVisitor.visitLabel(label);} 
下面看一下needsProbe方法,主要的限制条件有三个successor、multiTarget、methodInvocationLine 。
public static boolean needsProbe(final Label label) {final LabelInfo info = get(label);return info != null && info.successor&& (info.multiTarget || info.methodInvocationLine);} 
先看到successor属性 。顾名思义,表示当前的Lable是否是前一条Lable的继任者,也就是说当前指令和上一条指令是否是连续的,两条指令中间没有插入GOTO或者return.
LabelFlowAnalyzer.java类中,对每行指令进行流程分析,对successor属性赋值 。
boolean successor = false;//默认是falseboolean first = true; //默认是true?@Overridepublic void visitJumpInsn(final int opcode, final Label label) {LabelInfo.setTarget(label);if (opcode == Opcodes.JSR) {throw new AssertionError("Subroutines not supported.");}//如果是GOTO指令,successor=false,表示前后两条指令是断开的 。successor = opcode != Opcodes.GOTO;first = false;}?@Overridepublic void visitInsn(final int opcode) {switch (opcode) {case Opcodes.RET:throw new AssertionError("Subroutines not supported.");case Opcodes.IRETURN:case Opcodes.LRETURN:case Opcodes.FRETURN:case Opcodes.DRETURN:case Opcodes.ARETURN:case Opcodes.RETURN:case Opcodes.ATHROW:successor = false; //return或者throw,表示两条指令是断开的break;default:successor = true; //普通指令的话,表示前后两条指令是连续的break;}first = false;}?@Overridepublic void visitLabel(final Label label) {if (first) {LabelInfo.setTarget(label);}if (successor) {//这里设置当前指令是不是上一条指令的继任者,//源码中,只有这一个地方地方会触发这个条件赋值,也就是访问每个label的第一条指令 。LabelInfo.setSuccessor(label);}} 
再看一下methodInvocationLine属性,当ASM访问到visitMethodInsn方法的时候,就标记当前Lable代表调用一个方法,将methodInvocationLine赋值为True
@Overridepublic void visitLineNumber(final int line, final Label start) {lineStart = start;}?@Overridepublic void visitMethodInsn(final int opcode, final String owner,final String name, final String desc, final boolean itf) {successor = true;first = false;markMethodInvocationLine();}?private void markMethodInvocationLine() {if (lineStart != null) {//lineStart就是当前这个LableLabelInfo.setMethodInvocationLine(lineStart);}}?LabelInfo.java类public static void setMethodInvocationLine(final Label label) {create(label).methodInvocationLine = true;}


推荐阅读