背景

很多团队都是通过测试这一流程来作为代码高质量上线的最后一道关卡,所以保证测试这一流程不出问题是非常重要的。

因此为了提高代码质量,通常有以下几种方案:

  • 通过单测,来cover部分代码逻辑的边界
  • 通过代码覆盖率,让测试团队枸杞的黑盒测试尽可能的覆盖完大部分分支
  • 通过自动化测试,把部分人工验证的场景交给机器验证

当然,就算把上边的几种方案都做了,也不能保证线上不出问题,不过较大程宫颈癌度上的降低线上出现问题的场景,收益还gradle项目是比较大的。

本文会针对代码覆盖率这一场景进行分析。在android的代码覆盖率使用中,使用比较多的还是jacoco。在androidgradle的整个工具链中,也已经内置了jacoco了。我们可以不需要引入其他库也能够使用jacoco。

jacoco使用

在Android中使用jacoco进行代码覆盖率比较简单,按照下面几个步骤即可开启并gradle展示结果。

工资超过5000怎么扣税启jacoco插件

plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'jacoco'
}
jacoco {
    toolVersion = "0.8.5"
}

jacoco在andgoogleroidStudio已经内置了gradle项目。 在androidStudio中可以直接开启jacoco插件。

可以通过jacoco的Extension设置指定的的jacocohtml简单网页代码版本。

开启打包插桩开关

    buildTypes {
        release {
            minifyEnabled false
            testCoverageEnabled = true 
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
        debug {
            testCoverageEnabled = true
        }
    }

需要在BuildTypes中,针对不同的gradle下载打包类型,通过testCoverageEnabled开启代码插桩。

保存代码覆盖结果

通过编译期间的代码插java面试题桩,运行时会实时记录代码运行情况。 需要我们在我们自定义的时机保留代码覆盖结果。 支持同一个文件追加写入。

object JacocoUtil {
    val ecFile = File(Environment.getExternalStorageDirectory(),  "/coverage.ec")
    fun generateEcFile() {
        val agent = Class.forName("org.jacoco.agent.rt.RT")
            .getMethod("getAgent")
            .invoke(null)
        writeBytes2File(ecFile.absolutePath, agent.javaClass.getMethod("getExecutionData",
            Boolean::class.javaPrimitiveType).invoke(agent, false) as ByteArray)
    }
 }

通过反射gradle安装与配置获取jacgradleocogradle文件夹可以删吗的Agent实例,再通过反射获取出来代码覆盖结果的字节流,写入到gradle菜鸟教程文件中。

public byte[] getExecutionData(final boolean reset) {
		final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
		try {
			final ExecutionDataWriter writer = new ExecutionDataWriter(buffer);
			data.collect(writer, writer, reset);
		} catch (final IOException e) {}
		return buffer.toByteArray();
	}

从手机中dump指定覆盖率文件,gradle怎么读放在指定文件夹

adb pull sdcard/coverage.ec xxx/MyTest/app/build/ecf

通过保留文件,解析代码覆盖率报告

gradle文件中,增加处理任务。

task jacocoTestReport(type: JacocoReport) {
    group = "JacocoReport"
    description = "Generate Jacoco coverage reports after running tests."
    reports {
        html.enabled = true
    }
    classDirectories.from = files(files(coverageClassDirs).files.collect {
        fileTree(dir: "$rootDir" + it)
    })
    sourceDirectories.from = files(coverageSourceDirs)
    executionData.from = files("$buildDir/ecf/coverage.ec")
    doFirst {
        coverageClassDirs.each { filePath ->
            println("$rootDir" + filePath)
            new File("$rootDir" + filePath).eachFileRecurse { file ->
                if (file.name.contains('$$')) {
                    file.renameTo(file.path.replace('$$', '$'))
                }
            }
        }
    }
}

查看报告

在执java模拟器行完生成报告Task之后,可以看到在build目录下会java模拟器多生成一个reports文件夹,生成的报告就在里面。

写给android同学的代码覆盖率讲解

查看对应的覆盖结果闰土刺猹如下:

各个类覆盖详情:

写给android同学的代码覆盖率讲解

单个类的覆盖详情:

写给android同学的代码覆盖率讲解

jacoco原理

看完了jacoco的简单使用,我们再来看看jacoco的实现html标签属性大全

构建方式

jacoco整体是使用maven的方式去构建的。 maven构建的方式是java中通用的构建方式。

maven构建相对于android中gradle构建区别还是比较大的。最主要的不同点是在构建配置

  • 配置上:

    • Gradle基于groovy语言和DSL语Go法提供了简明、灵活、可读性公司让员工下班发手机电量截图强的配置方式

    • maven使用xml文件格式进行配置,较为繁琐

  • 可扩展性:

    • gradle扩展任何语言的构建,mhtml是什么意思aven不行。
  • 构建性能:

    • gradle支持增量构建

    • gradle支工龄差一年工资差多少持构建缓存

    • gradle支持守护进程

      所以gradgradle教程le的构建性能要优于maven。

插件开发

在阅读代码插桩入口时,发现类上边都会打java培训上一个特定的注解Mojo

@Mojo(name = "instrument", defaultPhase = LifecyclePhase.PROCESS_CLASSES, threadSafe = true)
public class InstrumentMojo extends AbstractJacocoMojo {}

Maven plain Old人头攒动的近义词 Java Object,mojo是基于maven的插件开发的注解。每一个mojo对象都是一个执行目标。

类似于gradle下载gradle中的gradle-plugin。一个mo宫颈癌jo就对应着html标签一个java类,在整体jar包编译时,会执行配置的插件。

有兴趣的可以查看maven的官方文档: 传送门

代码插入方式

jacoco记录代码覆盖率完全依赖于对原始代码的插桩,需要在原gradle安装与配置始代码中插入探针,通过运行时记录探针执行来计算代码覆盖来。

插入代码有两套实现

  • 动态方式:在Jvm加载class过程中,动态的去修改class
  • 离线模式:在编译cla人头攒动的近义词ss的阶段,对原始class进行修html标签属性大全改,生成类已html是什么意思经带有全量的插入代码

动态方式之javaAgenjava环境变量配置t

利用JVM提供的 InsjavaeetrumentAPI 来更改加载的到JVM的现有字节码。

JavaAgent也有两种方式修改字节码

  • 静态修改:在加载jar包之前修改字节码。静态加载会调用到premain方法。
java -javaagent:agent.jar -jar xxx.jar
比如jacoco就使用的静态修改的方式。可以查看其PreMain的premain方法
	public static void premain(final String options, final Instrumentation inst)
			throws Exception {
		final AgentOptions agentOptions = new AgentOptions(options);
		final Agent agent = Agent.getInstance(agentOptions);
		final IRuntime runtime = createRuntime(inst);
		runtime.startup(agent.getData());
		inst.addTransformer(new CoverageTransformer(runtime, agentOptions,
				IExceptionLogger.SYSTEM_ERR));
	}
  • 动态修改: 将javaAgent加载到已经运行的JVM中的过程称为动rtc是什么意思态加载。需要使用Java Attach龚俊 API。

    public class XXX {
        public static void agentmain(String agentArgs, Instrumentation inst) {
           	// can use inst to add args
        }
    }
    

离线模式

Jacoco的离闰土刺猹线模式,是在编译阶段,通过ASM修改原始字节码。

本次会针对jacoco的离线模式进行分析,jacoc枸杞o在android的使用场景上无法使用动态插入方式。

因为Android运行加载的本质上是Dex文件,已经不是jar包了,指令上和jar包不一样,javaAgent无法识别dex文件结构,所以在Android上只能使用离线模式。

原理

jacoco是如html网页制作何实现的代码覆盖率的统计的呢?

简单来说关键逻辑可以分为三块:

  • 插桩工商银行逻辑: 通过AS人头攒动的读音M做静态代码插桩,提gradle前给期望覆盖的类都添让天秤难以放弃的星座加上代码探针
  • 覆盖率统计:运行时,把对应的类执行过的探针记录存储,存储在内存中,接入方在自己期望的gradle依赖冲突强制指定时机进行探针记录的本地存储
  • 报告计算:从本地导出探针记录,进行龚俊记录合并,将记录转为期望的结果显示,比如html等。

插桩逻辑

先看一个简单的例子

class TestForJacocoData {
    fun testMethod(result: Boolean) {
        if (result) {
            System.out.println("分支1")
            return
        }
        System.out.println("分支2")
    }
}

经过jahtml个人网页完整代码cogradle和mavenco插桩之后的代码逻java是什么意思辑为

public final class TestForJacocoData {
    private static transient /* synthetic */ boolean[] $jacocoData;
    private static /* synthetic */ boolean[] $jacocoInit() {
        boolean[] zArr = $jacocoData;
        if (zArr != null) {
            return zArr;
        }
        boolean[] probes = Offline.getProbes(-1643518976017980468L, "com/xx/zz/TestForJacocoData", 18);
        $jacocoData = probes;
        return probes;
    }
    public TestForJacocoData() {
        $jacocoInit()[4] = true;
    }
    public final void testMethod(boolean z) {
        boolean[] $jacocoInit = $jacocoInit();
        if (z) {
            $jacocoInit[0] = true;
            System.out.println("分支1");
            $jacocoInit[1] = true;
            return;
        }
        $jacocoInit[2] = true;
        System.out.println("分支2");
        $jacocoInit[3] = true;
    }
}

通过这个样例代码在jacoco插桩前后的对比,可以发现jacoco的代码插桩人头攒动的读音会做下面几个操作:

  • 每一个类都会插入一个名为工龄差一年工资差多少$jacocoData的成员,jacocoData的类型为booleajava怎么读n数组

    这个成员用来记录当次进程启动之后,该类的代码分支执行情况。当对应的分支执行之公司让员工下班发手机电量截图后,就会给对应的数组元素赋值为true。

  • 每一个类都会插入$jacocoInit()的初始化方法

    jacocoInit()的作用是在任意方法执行时,从本地已经保存的记录文件中获取当前类的分支执行情况,并给当前类的成工龄差一年工资差多少员jacocoInit()的作用是在任意方法执行时,从本闰土刺猹地已经保存的记录文件中获取当前类的分支执行情况,并给当前类的成员jaRTCcocoData赋初始值

  • 对于每一个方法,针对各类型的指令RTC,会插入代码探针。所谓的探针就是$jacocoInit[i] = true;语句,一旦执行到,就把当前位置的探针index设置为true,表示已经执行过了。

探针插入的关键逻辑如下所示:

	public void insertProbe(final int id) {
		mv.visitVarInsn(Opcodes.ALOAD, variable);
		InstrSupport.push(mv, id);
		mv.visitInsn(Opcodes.ICONST_1);
		mv.visitInsn(Opcodes.BASTORE);
	}
  // InstrSupport.push
	public static void push(final MethodVisitor mv, final int value) {
		if (value >= -1 && value <= 5) {
			mv.visitInsn(Opcodes.ICONST_0 + value);
		} else if (value >= Byte.MIN_VALUE && value <= Byte.MAX_VALUE) {
			mv.visitIntInsn(Opcodes.BIPUSH, value);
		} else if (value >= Short.MIN_VALUE && value <= Short.MAX_VALUE) {
			mv.visitIntInsn(Opcodes.SIPUSH, value);
		} else {
			mv.visitLdcInsn(Integer.valueOf(value));
		}
	}

整体的探针插入的html5代码逻辑比较简单,主要关注下面两点:

  • 在ASM中如何对数组元素的赋值,需要给操作数栈依次放入数组对象引用, 需要放入index位置以及具体要放入的值,通过BASTORE指令,把栈顶的boolean数组存入数组指定的索引位置。

  • 对于不同大小的Igradle安装与配置nt值处理的指令不一样。

    • int值-15,使用ICONST_(05)

      jvm的解释:

      Push the int constant <i> (-1, 0, 1, 2, 3, 4 or 5) onto the operand stack
      

      -1到5,直接将常量pu人头攒动的近义词sh到操作gradle文件夹可以删吗数据栈中。

    • ijavascriptnt值闰土刺猹在-128~127,使用BIPUSH

      The immediate byte is sign-extended to an int value. That value is pushed onto the operand stack。
      

      在字节码层面上会使用一个byte字节去实现。一个byte有8bithtml个人网页完整代码,第一个bit表示符号,后面7位表示具体大小gradle项目,所以区间范围是-2^7~(2^7 -1)

    • int值让天秤难以放弃的星座在-32768~32767,使用SIPUSH

      The immediate unsigned byte1 and byte2 values are assembled into an intermediate short where the value of the short is (byte1 << 8) | byte2. The intermediate value is then sign-extended to an int, and the resulting value is pushed onto the operand stack.
      

      在字节码层面上会使用两个byte字节去实现。第一个bit表示符号,后面15bit表示具体的数值。所以区间范围是

      -2^15~(2^15 -1)

    • 其他Int区间,使用的L人体承受的最大电压dc指令

插入规则

jacoco的插入规则是比较重要,如何gradle文件夹可以删吗能够尽可能的覆盖全每一个分支?可以看公积金下具体的插桩代码。

关键的代码插入逻辑在MethodProbesAdapter中。

public final class MethodProbesAdapter extends MethodVisitor {
	@Override
	public void visitLabel(final Label label) {
		if (LabelInfo.needsProbe(label)) {
			probesVisitor.visitProbe(idGenerator.nextId());
		}
	}
  @Override
	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;
		}
	}
  @Override
	public void visitJumpInsn(final int opcode, final Label label) {
		if (LabelInfo.isMultiTarget(label)) {
			probesVisitor.visitJumpInsnWithProbe(opcode, label,
					idGenerator.nextId(), frame(jumpPopCount(opcode)));
		}
    ....
	}
}
	@Override
	public void visitLookupSwitchInsn(final Label dflt, final int[] keys,
			final Label[] labels) {
		if (markLabels(dflt, labels)) {
			probesVisitor.visitLookupSwitchInsnWithProbes(dflt, keys, labels,
					frame(1));
		}
    ...
	}
	@Override
	public void visitTableSwitchInsn(final int min, final int max,
			final Label dflt, final Label... labels) {
		if (markLabels(dflt, labels)) {
			probesVisitor.visitTableSwitchInsnWithProbes(min, max, dflt, labels,	frame(1));
		}
    ...
	}

在这个MethodProbesAdapter中有以下几个时gradle安装与配置机会插入探针。

  • vi人体肠道结构示意图sitLabel: 在字节码访问Label时调用
  • visitInsn: 在访问各个指令时会调用html文件怎么打开
  • visitJumpInsn:在跳转指令时调用
  • visitLookupSwitchInsn&amp工龄差一年工资差多少;visitTableSwitchgradle和mavenInsn:在switch-case语句中会调用

详细分析下这几个时机

visitLabel

在visitLabel方法中,会调用visitPro宫颈癌be方法,进行探针插入。在字节码层面上,Label的含义可以先看看ASM文档中的介绍:

A position in the bytecode of a method. Labels are used for jump, goto, and switch instructions, and for try catch blocks. A label designates the instruction that is just after. Note however that there can be other elements between a label and the instruction it designates (such as other labels, stack map frames, line numbers, etc.).
  • Label表示字节码在方法中公积金的位置

  • Label通常使用在跳转、gjava环境变量配置oto、swihtml文件怎么打开tch指令和try-catch块

  • Label可以指定下一条需要执行的指令。让天秤难以放弃的星座 不过Label和其跳转的指令中间可能RTC存在其他指令

在字节码层面上,指令默认是顺序执行的,假如没有label的支持,就无法实现跳转。

一个Label至少包含一条字节码指令。也就是说一个Label定义之后,后面的指令就是这个Label对象所对应的指令。

在VisitLabel中,HTML并不是所有的Label访问都会插入探针,只有满足下面几个场景才会做探针的插入操作。

	public static boolean needsProbe(final Label label) {
		final LabelInfo info = get(label);
		return info != null && info.successor
				&& (info.multiTarget || info.methodInvocationLine);
	}
succ人体承受的最大电压essor

表示指令的连续性,当前Label是相对于上一条指令是否是连续的,如果上一条指令是goto、jump指令,那么当前Label对于上条指令就gradle项目不是连续的。

在ASM阶段中,通过每一gradle是什么条指令访问时修改suJavacchtml网页制作essor的值来记录是否是连续的。

multiTarget

表示当前是否这个label是否有多个跳转来源,在一个方法调用中,可能存在多处指令会跳转到当前这个Label。

对于multiTarget的设置可以看下面的代码:

	public static void setTarget(final Label label) {
		final LabelInfo info = create(label);
		if (info.target || info.successor) {
			info.multiTarget = true;
		} else {
			info.target = true;
		}
	}
  • 如果这个label首gradle教程次访问,那么target设置为true。

  • 如果这个labhtml标签el再次访问时,即targRTCet为true,此时设置multiTarget工龄越长退休金越多吗为true。java环境变量配置

如果当前gradle教程这个探针的跳转是单来源,gradle下载在显示结果上,这个Label会直接跟着前面的探针是否执行展示,如果多个地方都rtc是什么意思可能跳转到当前Label,就意味着其他两个分支到这个分支之间中间会有断层,不是连续的,没有办法通肉跳测吉凶过之前的探针是否执行表明当前Label是否能够在结果显示上

方法调html简单网页代码用起始行数:me宫颈癌thodInvocationLine
	@Override
	public void visitInvokeDynamicInsn(final String name, final String desc,
			final Handle bsm, final Object... bsmArgs) {
		successor = true;
		first = false;
		markMethodInvocationLine();
	}
	private void markMethodInvocationLine() {
		if (lineStart != null) {
			LabelInfo.setMethodInvocationLine(lineStart);
		}
	}

表示javascript当前是调用Label表示调用一个方法。在方法调用Go场景下,会在方法调用前插入探针。Java

visitInsn

  @Override
	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;
		}
	}
====》
  @Override
	public void visitInsnWithProbe(final int opcode, final int probeId) {
		probeInserter.insertProbe(probeId);
		mv.visitInsn(opcode);
	}

在识别到return语句时,会在return语句前插入探针。在return语句前加入探针,当这个探针执行了,就表示当前这个分支gradle怎么读执行结束了。

visitJumpInsn

主要表示跳转指令。

Visits a jump instruction. A jump instruction is an instruction that may jump to another instruction.
Params:
opcode – the opcode of the type instruction to be visited. This opcode is either IFEQ, IFNE, IFLT, IFGE, IFGT, IFLE, IF_ICMPEQ, IF_ICMPNE, IF_ICMPLT, IF_ICMPGE, IF_ICMPGT, IF_ICMPLE, IF_ACMPEQ, IF_ACMPNE, GOTO, JSR, IFNULL or IFNONNULL.
label – the operand of the instruction to be visited. This operand is a label that designates the instruction to which the jump instruction may jump.

主要有列的几个指令: IFEQ, IFNE, IFLT, IFGE, IFGT, IFLE, IF_ICMPEQ, IF_ICMPNE, IF_ICMPLT, IF_ICMPGE, IF_ICMPGT, IF_ICMPLE, IF_ACMPEQ, IF_ACMPNE, GOTO,java语言 JSR, IFNULL or IFNONNULL。

  @Override
	public void visitJumpInsn(final int opcode, final Label label) {
		if (LabelInfo.isMultiTarget(label)) {
			probesVisitor.visitJumpInsnWithProbe(opcode, label,
					idGenerator.nextId(), frame(jumpPopCount(opcode)));
		}
    ....
	}
}

并不是所有的跳转指html5令都会插入探针,也会判断跳转的目标label是否有个来源。

	@Override
	public void visitJumpInsnWithProbe(final int opcode, final Label label,
			final int probeId, final IFrame frame) {
		if (opcode == Opcodes.GOTO) {
			probeInserter.insertProbe(probeId);
			mv.visitJumpInsn(Opcodes.GOTO, label);
		} else {
			final Label intermediate = new Label();
			mv.visitJumpInsn(getInverted(opcode), intermediate);
			probeInserter.insertProbe(probeId);
			mv.visitJumpInsn(Opcodes.GOTO, label);
			mv.visitLabel(intermediate);
			frame.accept(mv);
		}
	}
  • 对于goto指令,探针需要添加到跳转指令之前

  • 对于其他跳转指令,比如IF,会做一层转换,把IFEQ转换为IFNE,同时添加GOTO语句

    private int getInverted(final int opcode) {
    		switch (opcode) {
    		case Opcodes.IFEQ:
    			return Opcodes.IFNE;
    		case Opcodes.IFNE:
    			return Opcodes.IFEQ;
    		case Opcodes.IFLT:
    			return Opcodes.IFGE;
    		case Opcodes.IFGE:
    			return Opcodes.IFLT;
    		case Opcodes.IFGT:
    			return Opcodes.IFLE;
    		case Opcodes.IFLE:
    			return Opcodes.IFGT;
    		case Opcodes.IF_ICMPEQ:
    			return Opcodes.IF_ICMPNE;
    		case Opcodes.IF_ICMPNE:
    			return Opcodes.IF_ICMPEQ;
    		case Opcodes.IF_ICMPLT:
    			return Opcodes.IF_ICMPGE;
    		case Opcodes.IF_ICMPGE:
    			return Opcodes.IF_ICMPLT;
    		case Opcodes.IF_ICMPGT:
    			return Opcodes.IF_ICMPLE;
    		case Opcodes.IF_ICMPLE:
    			return Opcodes.IF_ICMPGT;
    		case Opcodes.IF_ACMPEQ:
    			return Opcodes.IF_ACMPNE;
    		case Opcodes.IF_ACMPNE:
    			return Opcodes.IF_ACMPEQ;
    		case Opcodes.IFNULL:
    			return Opcodes.IFNONNULL;
    		case Opcodes.IFNONNULL:
    			return Opcodes.IFNULL;
    		}
    		throw new IllegalArgumentException();
    	}
    

    比如下面的例子:

    class TestForJacocoData {
        fun testMethod(result: Int) {
            if (result == 1) {
                defineA()
            }
        }
        fun defineA() { val a = 1 }
    }
    

    编译后:

    public final void testMethod(int result) {
            boolean[] $jacocoInit = $jacocoInit();
            if (result != 1) {
                $jacocoInit[1] = true;
            } else {
                $jacocoInit[2] = true;
                defineA();
                $jacocoInit[3] = true;
            }
            $jacocoInit[4] = true;
        }
    

    比如在一些java怎么读较为复杂的if语句中,会把复杂的判断的语句拆分成单一条件,并进行反转,这样能够保证能够覆盖全所有的分支,并且在反转操作后,可以更好的配合GOTO语句插入探针。

switch-casegradle下载分支

switch-case对应下面两个字节码:

  • tablejava语言Switch

    查找效率为O(1),通过偏移量就人体肠道结构示意图可以找到对应的case。

    比如下面的例子:

    class TestForJacocoData {
        fun testSwitch() {
            val value = 1;
            when(value) {
                0 -> System.out.println(0)
                2 ->  System.out.println(2)
                5 ->  System.out.println(5)
                else -> System.out.println(6)
            }
        }
    }
    

    编译后的字节码为

    public final void testSwitch();
        descriptor: ()V
        flags: ACC_PUBLIC, ACC_FINAL
        Code:
          stack=2, locals=2, args_size=1
             0: iconst_1
             1: istore_1
             2: iload_1
             3: tableswitch   { // 0 to 5
                           0: 40
                           1: 70
                           2: 50
                           3: 70
                           4: 70
                           5: 60
                     default: 70
                }
    }
    

    可以看到这里使用的tableswitch指令,并且原本我们的case语句只有0、2、5,系统自动给我们补齐成了0,1,公司让员工下班发手机电量截图2,3,4,5,让其成为了顺序的tabgradle是什么le,可以直接通过游标直接访问。时间java是什么意思复杂度最终才能O(1),

  • lookupSwitch

    查找效人头攒动的近义词率为O(lgn),通过二分查找寻找对应的value值。

    比如下面的例子:

    class TestForJacocoData {
        fun testSwitch() {
            val value = 2;
            when(value) {
                0 -> System.out.println(0)
                1000 -> System.out.println(2)
                else -> {
                    System.out.println(6)
                }
            }
        }
    }
    

    对应的字节码为:

    public final void testSwitch();
        descriptor: ()V
        flags: ACC_PUBLIC, ACC_FINAL
        Code:
          stack=2, locals=2, args_size=1
             0: iconst_2
             1: istore_1
             2: iload_1
             3: lookupswitch  { // 2
                           0: 28
                        1000: 38
                     default: 48
                }
            28: getstatic     #12                 // Field java/lang/System.out:Ljava/io/PrintStream;
    

​ 可以看到这里使用的lookupswit人头攒动的读音ch指令。字节码html层面会根据case语句value值的稀疏,选择tableswitch指令还是lookupswitch指令。

对应的在ASM层看到的调用指令是下面两个javaee

	@Override
	public void visitLookupSwitchInsn(final Label dflt, final int[] keys,
			final Label[] labels) {
		if (markLabels(dflt, labels)) {
			probesVisitor.visitLookupSwitchInsnWithProbes(dflt, keys, labels,
					frame(1));
		}
    ...
	}
	@Override
	public void visitTableSwitchInsn(final int min, final int max,
			final Label dflt, final Label... labels) {
		if (markLabels(dflt, labels)) {
			probesVisitor.visitTableSwitchInsnWithProbes(min, max, dflt, labels,	frame(1));
		}
    ...
	}

其中,

  • dflt :默认的处理label
  • keys: 当前case的value集合
  • labelshtml文件怎么打开: 对应的keys的处理java编译器label

通过visitLookupSwitchIns公积金nWithProbes和人体肠道结构示意图visitTableSw枸杞itchtml个人网页完整代码hInhtml网页制作snWithProbes会给对应的case语句中插入探针。

html5上的介绍,我们可以大概整理下jacoco的代码插入逻辑

  • return语句前、throw语句前会进行探针的插入。人头攒动的近义词

  • 如果label对于上一条指令来说是连续的,并且有多个来源,gradle教程那么会进行探针插入

  • 如果label对于上一条指令来说是连续的,并且label是一个方法gradle依赖冲突强制指定调用,那么会进行探针插入

  • 如果是一个swi闰土刺猹tch语句,对于各个html网页制作case跳转也会进行探针插入。

运行时处理

在编译期间做了代码插入之后,运行时是如何生效的? 以及数据展示是如何来实现的呢?

对于每一个方法调用,在对jacocoData数组元素赋html网页制作值前,都会先尝试初始化jacocoData数组元素赋值前,都会先尝试初始化jacocoData

  private static  boolean[] $jacocoInit() {
        boolean[] zArr = $jacocoData;
        if (zArr != null) {
            return zArr;
        }
        boolean[] probes = Offline.getProbes(-1643518976017980468L, "com/kuaikan/zz/TestForJacocoData", 18);
        $jacocoData = probes;
        return probes;
    }

会先尝试从Offline中获取java是什么意思离线探针数据,有三个参数:

  • 第一个参数是classgradle菜鸟教程的唯一Id,在插桩时,会根据字节码流创建生成。

    private byte[] instrument(final byte[] source) {
    		final long classId = CRC64.classId(source);
    }
    
  • 第二个参数是class全路径名称

  • gradle怎么读三个参数是工资超过5000怎么扣税当前class共计有多少个探针。

这三个参数在编译后就已经固定了,java编译器不会发生改变,所以在插桩后插入的都是具体的值。

然后呢根据传入的参数,创建出来一个ExecutionData对象并返回

	public ExecutionData get(final Long id, final String name,
			final int probecount) {
		ExecutionData entry = entries.get(id);
		if (entry == null) {
			entry = new ExecutionData(id.longValue(), name, probecount);
			entries.put(id, entry);
			names.add(name);
		} else {
			entry.assertCompatibility(id.longValue(), name, probecount);
		}
		return entry;
	}

javascript以看到ExecutionData做了内存存储,并没有做javaee本地的文件存gradle怎么读储。

所以,当我们想要用jacoco来实现多人协作的覆盖率合并时,就需要自己实现当前覆盖率结果的文件存储。

如下代码所示:

fun generateEcFile() {
	FileUtils.createFolderIfNotExists(path)
  FileUtils.createFileIfNotExists(ecFile)
  val agent = Class.forName("org.jacoco.agent.rt.RT")
      .getMethod("getAgent")
      .invoke(null)
  IOUtils.writeBytes2File(ecFile, agent.javaClass.getMethod("getExecutionData", 
      Boolean::class.javaPrimitiveType).invoke(agent, false) as ByteArray)}

通过反射获取出RT实例,拿出当前所有的已执行的结果,并存储到文件中。

覆盖结果

在不看代码的情况下,我们可以先推理下,按照前面插桩和收集的数据,如何展现出实际覆盖率的结果。

  • 首先需要把所有上传的数据进行合并

    DumpTask + MergeTask

    • DumpTask: jjavaeeacoco内置dumpTask,主要的作用是从远端下载收集到的测试覆盖数据。

    • MergeTask:因为每次运行的测试覆盖数据都是单独的文件数据,所以需要有一个专门的Task,把众多的文件的测试覆盖数据合并成单个文件人头攒动

      合成的逻辑比较简单:

      private void load(final ExecFileLoader loader) {
      		final Iterator<?> resourceIterator = files.iterator();
      		while (resourceIterator.hasNext()) {
      			final Resource resource = (Resource) resourceIterator.next();
      				resourceStream = resource.getInputStream();
      				loader.load(resourceStream);
          }
      	}
      	public void load(final InputStream stream) throws IOException {
      		final ExecutionDataReader reader = new ExecutionDataReader(
      				new BufferedInputStream(stream));
      		reader.setExecutionDataVisitor(executionData);
      		reader.setSessionInfoVisitor(sessionInfos);
      		reader.read();
      	}
      ====》
      	public void visitClassExecution(final ExecutionData data) {
      		put(data);
      	}
        ====》 
      	public void put(final ExecutionData data) throws IllegalStateException {
      		final Long id = Long.valueOf(data.getId());
      		final ExecutionData entry = entries.get(id);
      		if (entry == null) {
      			entries.put(id, data);
      			names.add(data.getName());
      		} else {
      			entry.merge(data);
      		}
      	}
      

      即遍历每一个文件html标签,根据文件内容,调用load,最终mergegradle项目到同一个hashMgradle项目ap中。

  • 需要把方法覆盖率和对应的className和源代码工资超过5000怎么扣税文件做关联

    这个流程是比较重要并且复杂的流程。 因为运行时收集到的ExecutionData数闰土刺猹据还龚俊比较少,仅有唯一ID、名称、和覆盖率结果数组,无法直接应gradle菜鸟教程用于结果展示。

    public final class ExecutionData {
    	private final long id;
    	private final String name;
    	private final boolean[] probes;
    	}
    

    因此,需要有分析的Task,将这个结果和源文件进行关联。分析单个方法主要使用Instrrtc是什么意思uctionsBuilder。

    	InstructionsBuilder(final boolean[] probes) {
    		this.probes = probes;
    		this.currentLine = ISourceNode.UNKNOWN_LINE;
    		this.currentInsn = null;
    		this.instructions = new HashMap<AbstractInsnNode, Instruction>();
    		this.currentLabel = new ArrayList<Label>(2);
    		this.jumps = new ArrayList<Jump>();
    	}
    

    根据MethodVisitor的访问顺序,重建探针对应的覆盖的行号gradle教程、指令等。

    @Override
    	public void visitLabel(final Label label) {
    		builder.addLabel(label);
    	}
    	@Override
    	public void visitLineNumber(final int line, final Label start) {
    		builder.setCurrentLine(line);
    	}
    	@Override
    	public void visitInsn(final int opcode) {
    		builder.addInstruction(currentNode);
    	}
    	@Override
    	public void visitIntInsn(final int opcode, final int operand) {
    		builder.addInstruction(currentNode);
    	}
    	void addProbe(final int probeId, final int branch) {
    		final boolean executed = probes != null && probes[probeId];
    		currentInsn.addBranch(executed, branch);
    	}
    

    最关键的还是在插桩时标记需要插html标签属性大全入探针的地方,都访问一次addProbe(final int probeId, final int bhtml简单网页代码ranch)方法,这样可以重新从探针数组中获取当前指令是否覆盖到。

  • 根据展示的样式,转化为html等其他格式。

将merge的结果,展示成对应的文件样式,比如html等。

关键逻辑是是否覆盖的展示逻辑,如下所示:

	HTMLElement highlight(final HTMLElement pre, final ILine line,
			final int lineNr) throws IOException {
		final String style;
		switch (line.getStatus()) {
		case ICounter.NOT_COVERED:
			style = Styles.NOT_COVERED;
			break;
		case ICounter.FULLY_COVERED:
			style = Styles.FULLY_COVERED;
			break;
		case ICounter.PARTLY_COVERED:
			style = Styles.PARTLY_COVERED;
			break;
		default:
			ret
		}

这里的line就是我们前面通过InstructionsBuilder分析出来的结果,根据不同的结果,展示不同人头攒动的读音的色值。

gradle菜鸟教程

在现有的jacoco的能力基础上快速实现增量,目前来说,有比较多的方式

  • 在插桩过程中做增量,在非增量文件中,不进行插桩
  • 在结果merge的过程中,进行增量逻辑处理,过滤扫描出来的增量代码段。
  • 在生成的结果文件中,过滤出来增量结果并展示

第一种方案较优,仅对需要增量的代码进行插桩,可以降低整体的编html简单网页代码译耗时。

第二种、第三种方案java语言较简单,仅需要针对结果集层面做处理,不需要care较为复杂的插桩逻辑。

增量代码获取

比较通用gradle的方案是通过git di工商银行ff可以计算两个分支间的增量代码让天秤难以放弃的星座。不过这种方案有缺陷

  • 在某些场景下,diff过大的情况下,查询不出来结果。 在一些改动较大的业务下,会导致整个增量方案失效。

  • 基于git diff实现的,需要自己实现一套解析器,针对git diff的结果,解析出来增量数据java是什么意思集,较为复杂。

因此,我们基于目前jacoco的代码插桩,在全量时,直接利用jacoco对于每人体承受的最大电压个类、各个方法的插桩记录做了记录,然后和人头攒动的读音分支关联,并且上传作为备份。在每一次代码编译时,拉下来对应分支上传备份文件,计算人体承受的最大电压增量。然后在插桩过程中,过滤掉对应非增量的class和method来实现增量。

参考文章:

javaAgent指南:www.baeldung.cogradle菜鸟教程m/java-instru…

javaAgent指南:xz.aliyun.com/t/9450

maven、gradle对比:wwwgradle项目.flydean.com/gradle-vs-人体肠道结构示意图m…

jacoco染色技术实践:www.shuzhiduo.com/A/GBJr枸杞KqrE5…