- 支持Ant和Gradle打包方式,可以自由切換。
- 支持離線模式,更貼合SDK的使用場景。
- JaCoCo文檔比較全面,還在持續維護,有問題便於解決。
//原始java方法 public static int Test1(int a, int b) { int c = a + b; int d = c + a; return d; } //--------------------------我是分割線--------------------------------------------// //jacoco處理後的方法 private static transient /* synthetic */ boolean[] $jacocoData; public static int Test1(final int a, final int b) { final boolean[] $jacocoInit = $jacocoInit(); final int c = a + b; final int n; final int d = n = c + a; $jacocoInit[3] = true; return n; } private static boolean[] $jacocoInit() { boolean[] $jacocoData; if (($jacocoData = TestInstrument.$jacocoData) == null) { $jacocoData = (TestInstrument.$jacocoData = Offline.getProbes(-6846167369868599525L, "com/jacoco/test/TestInstrument", 4)); } return $jacocoData; }
ALOAD probearray xPUSH probeid ICONST_1 BASTORE
-
統計方法的執行情況。
-
統計分支語句的執行情況。
-
統計普通代碼塊的執行情況。
-
方法尾加: 能說明方法被執行過, 且說明了探針上面的方法被執行了,但是這種處理比較麻煩, 可能有多個return或者throw。
-
方法頭加: 處理簡單, 但只能說明方法有進去過。
public void visitInsn(final int opcode) { switch (opcode) { case Opcodes.IRETURN: case Opcodes.LRETURN: case Opcodes.FRETURN: case Opcodes.DRETURN: case Opcodes.ARETURN: case Opcodes.RETURN: case Opcodes.ATHROW: probesVisitor.visitInsnWithProbe(opcode, idGenerator.nextId()); break; default: probesVisitor.visitInsn(opcode); break; } }
-
無條件Jump (goto)
//源碼 public static void Test4(int a) { if(a>10){ a=a+10; } a=a+12; } //jacoco處理後的字節碼 public static void Test4(int a) { boolean[] var1 = $jacocoInit(); if (a <= 10) { var1[11] = true; } else { a += 10; var1[12] = true; } a += 12; var1[13] = true; }
//源碼,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; }
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; }
@Override public 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); }
public static boolean needsProbe(final Label label) { final LabelInfo info = get(label); return info != null && info.successor && (info.multiTarget || info.methodInvocationLine); }
boolean successor = false;//默認是false boolean first = true; //默認是true @Override public 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; } @Override public 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; } @Override public void visitLabel(final Label label) { if (first) { LabelInfo.setTarget(label); } if (successor) {//這裏設置當前指令是不是上一條指令的繼任者, //源碼中,只有這一個地方地方會觸發這個條件賦值,也就是訪問每個label的第一條指令。 LabelInfo.setSuccessor(label); } }
@Override public void visitLineNumber(final int line, final Label start) { lineStart = start; } @Override public 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就是當前這個Lable LabelInfo.setMethodInvocationLine(lineStart); } } LabelInfo.java類 public static void setMethodInvocationLine(final Label label) { create(label).methodInvocationLine = true; }
public void visitJumpInsn(final int opcode, final Label label) { LabelInfo.setTarget(label);//Jump語句 將Lable標記一次爲true if (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; } }
-
return和throw之前插入探針。
-
複雜if語句,爲統計分支覆蓋情況,會進行反轉成if not,再對個分支插入探針。
-
當前指令是上一條指令的連續,並且當前指令是觸發方法調用,則插入探針。
-
當前指令和上一條指令是連續的,並且是有多個來源的時候,則插入探針。
-
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>
testCoverageEnabled = true //開啓代碼染色覆蓋率統計
destfile=/sdcard/jacoco/coverage.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(); } } }
<jacoco:merge destfile="merged.exec"> <fileset dir="executionData" includes="*.exec"/> </jacoco:merge>
<jacoco:report> <executiondata> <file file="jacoco.exec"/> </executiondata> <structure name="Example Project"> <classfiles> <fileset dir="classes"/> </classfiles> <sourcefiles encoding="UTF-8"> <fileset dir="src"/> </sourcefiles> </structure> <html destdir="report"/> </jacoco:report>