再看一下multiTarget属性,它表示当前指令是否可能从多个来源跳转过来 。源码在下面 。
当执行到一条Jump语句时,第二个参数表示要跳转到的Label,这时就会标记一次来源,后续分析流到了该Lable,如果它还是一条继任者指令,那么就将它标记为多来源指令 。
public void visitJumpInsn(final int opcode, final Label label) {LabelInfo.setTarget(label);//Jump语句 将Lable标记一次为trueif (opcode == Opcodes.JSR) {throw new AssertionError("Subroutines not supported.");}successor = opcode != Opcodes.GOTO;first = false;}?//如果当设置它是否是上一条指令的后续指令时,再一次设置它为multiTarget=true,表示至少有2个来源public static void setSuccessor(final Label label) {final LabelInfo info = create(label);info.successor = true;if (info.target) {info.multiTarget = true;}}
特殊问题解答
有了前面对源码的分析,再来看一些特殊情况 。
问:else块结尾为什么会插入探针?
答:L3的来源有两处,一处是GOTO来的,一处是L1顺序执行来的,使得multiTarget = true条件成立,所以在L3之前插入探针,表现在Java代码中就是在else块结尾增加了探针 。

文章插图
问:为什么case 1条件里第一个Test方法前不插入探针?
答:L1上一条是指GOTO指令,使得successor = false,所以该方法调用前无需插入探针 。

文章插图
探针插桩结论
通过以上分析得出结论,代码块中探针的插入策略:
- return和throw之前插入探针 。
- 复杂if语句,为统计分支覆盖情况,会进行反转成if not,再对个分支插入探针 。
- 当前指令是上一条指令的连续,并且当前指令是触发方法调用,则插入探针 。
- 当前指令和上一条指令是连续的,并且是有多个来源的时候,则插入探针 。
利用JaCoCo提供的Ant插件,在原有打包脚本上进行修改 。
- Ant脚本根节点增加JaCoCo声明 。
- 引入jacocoant 自定义task 。
- 在compile task完成之后,运行instrument任务,对原始classes文件进行插桩,生成新的classes文件 。
- 将插桩后的classes打包成jar包,不需要混淆,就完成了染色包的构建 。
<project name="Example" xmlns:jacoco="antlib:org.jacoco.ant"> //增加jacoco声明//引入自定义task<taskdef uri="antlib:org.jacoco.ant" resource="org/jacoco/ant/antlib.xml"><classpath path="path_to_jacoco/lib/jacocoant.jar"/></taskdef>?...//对classes插桩<jacoco:instrument destdir="target/classes-instr" depends="compile"><fileset dir="target/classes" includes="**/*.class"/></jacoco:instrument>?</project> 测试工程配置
将生成的染色包放入测试工程lib库中,测试工程build.gradle配置中开启覆盖率统计开关 。
官方gradle插件默认自带JaCoCo支持,需要开启开关 。
testCoverageEnabled = true //开启代码染色覆盖率统计 收集覆盖率报告的方式有两种,一种是用官方文档里介绍的:配置jacoco-agent.properties文件,放Demo的resources资源目录下 。

文章插图
文件配置生成覆盖率产物的路径,然后测试完Demo,在终止JVM也就是退出应用的时候,会自动将覆盖率数据写入,这种方式不方便对覆盖率文件命名自定义,多轮测试产物不明确 。
destfile=/sdcard/jacoco/coverage.ec 另一种方式是利用反射技术:反射调用jacoco.agent.rt.RT类的getExecutionData方法,获取上文中探针的执行数据,将数据写入sdcard中,生成ec文件 。这段代码可以在应用合适位置触发,推荐退出之前调用 。
/*** 生成ec文件*/public static void generateEcFile(boolean isNew, Context context) {File file = new File(DEFAULT_COVERAGE_FILE_PATH);if(!file.exists()){file.mkdir();}DEFAULT_COVERAGE_FILE = DEFAULT_COVERAGE_FILE_PATH + File.separator+ "coverage-"+getDate()+".ec";Log.d(TAG, "生成覆盖率文件: " + DEFAULT_COVERAGE_FILE);OutputStream out = null;File mCoverageFilePath = new File(DEFAULT_COVERAGE_FILE);try {if (!mCoverageFilePath.exists()) {mCoverageFilePath.createNewFile();}out = new FileOutputStream(mCoverageFilePath.getPath(), true);?Object agent = Class.forName("org.jacoco.agent.rt.RT").getMethod("getAgent").invoke(null);?out.write((byte[]) agent.getClass().getMethod("getExecutionData", boolean.class).invoke(agent, false));Log.d(TAG,"写入" + DEFAULT_COVERAGE_FILE + "完成!" );Toast.makeText(context,"写入" + DEFAULT_COVERAGE_FILE + "完成!",Toast.LENGTH_SHORT).show();} catch (Exception e) {Log.e(TAG, "generateEcFile: " + e.getMessage());Log.e(TAG,e.toString());} finally {if (out == null)return;try {out.close();} catch (IOException e) {e.printStackTrace();?}}}
推荐阅读
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ueditor 百度编辑器的代码块显示功能的设置
- 你的接口参数怎么接收的
- JetPack现在都成了Android开发必备技能嘛?
- 我用过的几款SSH客户端工具
- VueJS中使用前端虚拟接口Mock.js
- 如何使用VSCode调试JS?
- 避免开源代码漏洞的4个最佳实践
- Nignx的安装和使用
- Vuex 映射完全指南
- EditPlus——一款小巧功能强大的老牌代码文本编辑器
