9

从精准化测试看ASM在Android中的强势插入-JaCoco初探

 2 years ago
source link: https://xuyisheng.top/jacoco/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

在Java技术栈上,基本上提到覆盖率,大家就会想到JaCoco「Java Code Coverage的缩写」,几乎所有的覆盖率项目,都是使用JaCoco,可想而知它的影响力有多大,我们在Android项目中,也集成了JaCoco,官网文档如下。

https://docs.gradle.org/current/userguide/jacoco_plugin.html

但是这里的JaCoco是与单元测试配合使用的,与一般的业务测试场景不太一样,所以,我们需要自己依赖JaCoco来做改造。

https://www.eclemma.org/jacoco/

从官网上就能看出这是一个极具历史感的项目。最后生成的覆盖率文件,是在 源代码的基础上,用颜色标记不同的执行状态。

image-20210716171811946

在上面这张图中,绿色代表已执行, 红色代表未执行, 黄色代表执行了一部分,这样就可以算出代码的覆盖率数据。

使用全量报表

JaCoco默认的插桩方式是全部插桩,在Android项目中,要使用JaCoco的全量报表功能非常简单,因为JaCoco插件已经集成在Gradle中了,所以我们只需要开启JaCoco即可。

首先,在根目录gradle文件中加入JaCoco的依赖

classpath "org.jacoco:org.jacoco.core:0.8.4"

然后在App的gradle文件中增加插件的依赖。

apply plugin: 'jacoco'

并在android标签中,增加开关。

testCoverageEnabled = true

接下来引入JaCoco的Report模块,同时exclude掉core,因为其在gradle中已经有依赖了。

implementation('org.jacoco:org.jacoco.report:0.8.4') {
    exclude group: 'org.jacoco', module: 'org.jacoco.core'
}

创建生成Report的Task

def coverageSourceDirs = ['../xxxx/src/main/java']

task jacocoTestReport(type: JacocoReport) {
        group = "Reporting"
        description = "Generate Jacoco coverage reports after running tests."
        reports {
            xml.enabled = true
            html.enabled = true
        }
        classDirectories.setFrom(fileTree(
                dir: './build/intermediates/javac/xxxxx', 
                excludes: ['**/R*.class']))
        sourceDirectories.setFrom(files(coverageSourceDirs))
        executionData.setFrom(files("$buildDir/outputs/code-coverage/connected/coverage.exec"))
        doFirst {new File("$buildDir/intermediates/javac/masterDebug/classes/com/qidian/QDReader").eachFileRecurse { file ->
                        if (file.name.contains('$$')) {
                                file.renameTo(file.path.replace('$$', '$'))
                        }
                }
        }
}

在项目中合适的地方来调用这两个方法,分别用来创建JaCoco的Exec文件和写入Exec文件。

private void createExecFile() {
    String DEFAULT_COVERAGE_FILE_PATH = "/mnt/sdcard/" + getPackageName();
    String DEFAULT_COVERAGE_FILE = DEFAULT_COVERAGE_FILE_PATH + "/coverage.ec";
    File file_path = new File(DEFAULT_COVERAGE_FILE_PATH);
    File file = new File(DEFAULT_COVERAGE_FILE);
    Log.d(TAG, "file_path = " + file_path);
    if (!file.exists()) {
        try {
            file_path.mkdirs();
            file.createNewFile();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

private void writeExecFile() {
    OutputStream out = null;
    try {
        out = new FileOutputStream("/mnt/sdcard/" + getPackageName() + "/coverage.ec", 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));
    } catch (Exception e) {
        Log.d(TAG, e.toString(), e);
        e.printStackTrace();
    } finally {
        if (out != null) {
            try {
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

在创建Exec文件后,进行测试,然后写入Exec文件,等测试完毕后,把生成的Exec文件通过ADB pull到本地,再执行jacocoTestReport这个Task即可生成全量的JaCoco覆盖率报告。

花了这么长时间写了这么多,其实并没什么卵用,只是让大家看下如何来使用JaCoco的标准用法。

JaCoco插桩原理

JaCoco在Android上只能使用Offline mode,它的实现机制其实很简单,我们反编译一下它插入的代码。

image-20210617135224018

可以发现,实际上JaCoco就是用一个Boolean数组来标记每句可执行代码,只要执行过相应的语句,当前位就被标记为True,这个标记,官方称之为「探针」(Probe)。

JaCoco对代码的修改主要体现在下面几个地方:

  • 在Class中增加jacocoData属性和jacocoData属性和jacocoData属性和jacocoInit方法
  • 在Method中增加了$jacocoInit数字并初始化
  • 增加了对数组的修改

当然,这只是JaCoco最基本的原理,实际的实现细节会更加复杂,例如条件、选择语句、方法函数的探针插入等等,这里不详细深入讨论,感兴趣的朋友可以参考JaCoco的源码:

https://github.com/jacoco/jacoco

由于JaCoco只是插入一个探针数组,所以对代码执行的性能开销影响不大,但是由于插入大量的探针代码,所以代码体积会增大不少,一般情况下,Android会在测试包中做插入,而在正式包中去除插入逻辑。

当然,借助JaCoco还能玩一些骚操作,比如发到线上,实时统计代码中有哪些代码从未执行过,用于发现潜在的垃圾代码。

探针插桩策略

JaCoco的核心逻辑就是要决定,到底在哪插入探针代码。官网文档上对插桩策略写的比较清楚,涉及到字节码的一些原理,所以这里就不深入讲解了,感兴趣的朋友可以通过下面的链接查看。

https://www.jacoco.org/jacoco/trunk/doc/flow.html

关键代码类

JaCoco对代码的探针插入分析,主要是利用了下面这些计数器:

  • 指令计数器(CounterImpl)
  • 行计数器(LineImpl)
  • 方法计算节点(MethodCoverageImpl)
  • 类计算节点(ClassCoverageImpl)
  • Package计算节点(PackageCoverageImpl)
  • Module计算节点(BundleCoverageImpl)

这里面包含了JaCoco的覆盖率数据。

JaCoco的使用其实非常简单,原理也很简单,但要做的好,稳定运行这么多年没有Bug,还是很难的,所以现在市面上做覆盖率的很多软件都逐渐被历史所淘汰了,而剩下的就是经历过时间检验的真金。

向大家推荐下我的网站 https://xuyisheng.top/ 专注 Android-Kotlin-Flutter 欢迎大家访问


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK