通过自定义Gradle插件编译时修改jar包完成Android组件覆盖率

前言

本次做jacoco覆盖率发现,主工程依赖的jar包都未产生覆盖率数据,熟悉jar包方式组件化架构的同学应该明白主工程仅是空壳,若按照网上教程做则压根覆盖不到业务组件,必须解决,经研究发现,AS的Gradle build插件执行过程仅创建一个jacoco Task并对当前工程源码的class执行离线模式字节码插入,并不对工程依赖的jar包进行插桩,so必须自定义一个Gradle插件来干预编译流程,经探索最初拟出两种方案:
1.独立脚本形式,通过监听gradle task与Build执行周期完成自定义插桩操作,具体插桩行为通过调用cmd命令的形式去做,脚本受限程度较高,依赖于gradle脚本的api
2.独立构建插件形式,自定义Transfrom,编译时开发者自定义的Transform将置于默认Transform之前,以此处为锚点的自定义操作可以方便的插桩。
Gradle构建插件即apply plugin: 'com.android.application'它的jar包可以查看源码并仿写,位置大概在.gradle\caches\modules-2\files-2.1\com.android.tools.build\com.android.tools.build

注:脚本是基于gradle开放api的,构建插件是基于android编译插件的并非直接基于Gradle的Plugin,两种并不完全相同,此处说构建其实就是安卓的编译过程。

概念认识

一.jacoco两种模式
a.在线模式,即在虚拟机类加载时进行插桩,运行结束后本地不保存插桩字节码
b.离线模式,即直接对.class文件进行流读取并将插桩字节码保存在.class文件中,属于侵入式

注:android独有的运行环境决定了离线模式是唯一的模式,因为它最后是odex字节码,jacoco本身不支持dex字节码的插桩,就算是app有热修复那种可以扫描jar包并自定义类加载有锚点做处理,也不能总在每次打开app上做处理,手机才多少资源

二.Gradle中的Task
Gradle有两个重要概念,Project与Task,1个moudle工程=1个Project,1个Project含有n个Task,Project是由settings.gradle中的include解析生成的.这里的Project跟eclipse的Project层级倒是相同,android插件的若干Task中组成了我们平时的编译过程

Task :app:generateNormalDebugBuildConfig
Task :app:prepareLintJar
Task :app:prepareLintJarForPublish
Task :app:compileNormalDebugAidl
Task :app:compileNormalDebugRenderscript
Task :app:generateNormalDebugSources
Task :app:dataBindingExportBuildInfoNormalDebug
Task :app:dataBindingMergeDependencyArtifactsNormalDebug
Task :app:dataBindingMergeGenClassesNormalDebug
Task :app:generateNormalDebugResValues
Task :app:generateNormalDebugResources
Task :app:mergeNormalDebugResources
Task :app:mainApkListPersistenceNormalDebug
Task :app:createNormalDebugCompatibleScreenManifests
Task :app:processNormalDebugManifest
Task :app:dataBindingGenBaseClassesNormalDebug
Task :app:dataBindingExportFeaturePackageIdsNormalDebug
Task :app:processNormalDebugResources
Task :app:compileNormalDebugKotlin
Task :app:javaPreCompileNormalDebug
Task :app:compileNormalDebugJavaWithJavac
Task :app:compileNormalDebugSources
Task :app:checkNormalDebugDuplicateClasses
Task :app:desugarNormalDebugFileDependencies
Task :app:mergeNormalDebugShaders
Task :app:compileNormalDebugShaders
Task :app:generateNormalDebugAssets
Task :app:mergeNormalDebugAssets
Task :app:processNormalDebugJavaRes
Task :app:validateSigningNormalDebug
Task :app:signingConfigWriterNormalDebug
Task :app:mergeNormalDebugJniLibFolders
Task :app:jacocoNormalDebug
Task :app:mergeNormalDebugJavaResource
Task :app:mergeNormalDebugNativeLibs
Task :app:stripNormalDebugDebugSymbols
Task :app:stripNormalDebugDebugSymbols
Task :app:multiDexListNormalDebug
Task :app:transformClassesWithDexBuilderForNormalDebug
Task :app:mergeDexNormalDebug
Task :app:packageNormalDebug
Task :app:assembleNormalDebug
Task :app:assembleDebug

三.Task本身不参与编译流程(此段描述可能有误)
我认为Task本身就算一个mini任务,功能单一,其是独立的,一般我们写完就直接在脚本文件的左侧用绿三角按钮执行,如下图:

image.png

那么编译流程是如何串起来这些Task的,除了跟gradle本身相关外,其实还跟apply plugin: 'com.android.application'这个插件有关,这个插件是Android的官方编译插件,看其源码可以学到很多自定义插件的知识,路径大致在.gradle\caches\modules-2\files-2.1\com.android.tools.build\com.android.tools.build很多Task是这个插件动态创建的,包括build.gradle文件中的配置,即我们常见的如下配置

android {
    compileSdkVersion 30
    buildToolsVersion "30.0.2"
    defaultConfig {
        applicationId "com.biabia.testjacoco"
        ...
    }
    buildTypes {...
    }
}

,对于插件来说这些配置就算一个java类AppExtention extents BaseExtention或者说是接口AndroidConfig,所以本次我们想要将Task与编译流程绑定上就得尝试gradle脚本开放的方法(.gradle\wrapper\dists\gradle-5.4.1-all\3221gyojl5jsh0helicew7rwx\gradle-5.4.1\src\core\org\gradle\api),譬如mustRunAfter、dependsOn,绑定到这些固定Task前或后面,本次未做这种干扰官方插件编译Task顺序的尝试,结果未知。
四.编译过程
两张图须知

gradle流程图.png

编译流程详情图.png

方法一:自定义独立脚本

在app的build文件加入 apply from: 'xxxx.gradle',即可引入独立脚本

  • 操作步骤
    1.如下图,调整至Project视图,在/app文件夹下创建一个JacocoLibProbe.gradle文件
    image.png

    2.在JacocoLibProbe.gradle文件中添加apply plugin: 'com.android.application';(添加目的在于写groovy代码时可以有只能提示,书写发现在脚本中添加任意一个插件以引入gradle库,写groovy语言时才会有智能提示,同时需注意在写groovy时使用未导包的变量或者类在编译时会报no signal...什么错误需留意)
    3.实现Task及Build的接口,如下:
apply plugin: 'com.android.application'
class BuildTaskTraceListener implements TaskExecutionListener, BuildListener{
    private Gradle gradle
    private Project project

    BuildTaskTraceListener(Project project) {
        this.project = project
    }

    @Override
    void buildStarted(Gradle gradle) {
        this.gradle = gradle
    }

    @Override
    void settingsEvaluated(Settings settings) {

    }

    @Override
    void projectsLoaded(Gradle gradle) {

    }

    @Override
    void projectsEvaluated(Gradle gradle) {

    }

    @Override
    void buildFinished(BuildResult result) {

    }

    @Override
    void beforeExecute(Task task) {

    }

    @Override
    void afterExecute(Task task, TaskState state) {

    }
}
gradle.addListener(new BuildTaskTraceListener(project))

由上述代码可看到涉及Task与Build的周期回调,可以在Task执行前与后做些配置,在transformClassesWithDexBuilderForDebug前操作即class转为dex文件前在勾子函数中完成对所有jar包的插桩即可,从此处开始就开启了一个麻烦的步骤:
1.收集工程所有在线、离线依赖的包
2.解压.aar、解压.jar包
3.调用jacoco的包对解压的.class文件进行流读取插桩并写出保存至.class文件中
4.压缩插桩后的解压文件夹重新生成.aar或.jar包
经我研究发现所有的第三方包在task链执行前就已下载到位,所以不存在一边转码一边下载的情况,由于此次是自定义脚本,并未找到在脚本中引入第三方jar包进行插桩的方法,也未找到gradle版本的插桩库(尝试过在app build.gradle中添加implementation 'org.jacoco:org.jacoco.core:0.7.9',在独立脚本文件头部添加import org.jacoco.core.instr.*皆无效)故将上述四个步骤用java写成一个外部jar包,gradle脚本通过执行cmd命令调起jar包进行操作,但在后续研究中发现自定义Transform更加简便。

方法二:自定义独立插件

自定义独立插件有两种方式:

  1. 发布到maven上,之后被工程在线引入
    优点:在线引用对于当前工程来说代码分割工程更有条理,引用也方便,最主要的是在线依赖能被多个工程同时使用。
    缺点:以在线jar包方式依赖无法即改即用,还需要依赖外部发布平台
  2. 直接放在当前工程中,在编译时工程会先编译自定义插件之后再编译主工程
    优点:源码放置在工程本地,可以随时更改,其本质上就是未在settings.gradle配置的一个特殊android moudle工程,。
    缺点:增加本地管理工作量,无法被多个工程引用

本次选用上述的第二种,不进行发布,即改即用

  • 操作步骤
    1.切换至Project视图
    创建固定文件夹buildSrc(官方定义名称)等文件如下所示,其中红框部分为人工创建,buildSrc文件夹下的.gradlebuildbuildSrc.iml构建后生成非人工创建,其本质就是一个moudle工程,但不同的是它不可出现在settings.gradle文件中配置include,在创建main文件夹后,可以创建groovy、java、resources文件夹,此处使用的是groovy文件夹,因为groovy文件支持写java用它可以兼容两种写法,是最合适的
    image.png

    2.本次自定义的Transform及其处理类,如下
    image.png

FilterUtil.groovy

package com.xxx.probe;

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.List;
import java.util.Properties;

import static com.sun.org.apache.xalan.internal.xsltc.compiler.util.Util.println;

public class FilterUtil {
    /**
     * 精准屏蔽不需要插桩的jar包
     * @param jarName org.codehaus.groovy.runtime
     * @return true:需要过滤,不插桩  flase:无需过滤,
     * 需注意要插桩jar包名与使用过程中的依赖名可能不同
     */
    public static boolean checkFilterJar(String jarName) throws IOException {
        if(jarName.startsWith("SNF")||jarName.contains("_android-")){
            return false
        }else {
            return true
        }
    }

    /**
     *  过滤具体插桩文件
     * @param entryName 关键字
     * @return true:需要过滤  flase:不需要过滤
     */
    public static boolean checkFilterFile(String entryName) {
        if (entryName.contains(".class")) {
            //只对类进行判断
            if (entryName.contains("R.class")
                    || entryName.contains("BuildConfig.class")
                    || entryName.contains("R.\$")
                    || entryName.contains("com/google/common/")
                    || entryName.contains("javax/annotation/")
                    || entryName.contains("org/apache/commons")
            ) {
                return true
            } else {
                return false
            }
        } else {
            System.out.println("非.class文件:" + entryName)
            return true
        }
    }
}

JarAvatar.java文件

package com.xxx.probe;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.CRC32;
import java.util.zip.CheckedOutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

public class JarAvatar {
    /**
     * 解压jar包
     *
     * @param destDir     解压位置
     * @param jarFilePath 需要解压的jar
     */
    public static void extractJar(String destDir, String jarFilePath, ProbeListener pl) throws IOException {
        //    String jarFile = "E:/test/com.ide.core_1.0.0.jar";
//        System.out.println("解压位置:"+destDir+"\n需解的jar:"+jarFilePath);
        if ((jarFilePath == null) || (jarFilePath.equals("")) || !jarFilePath.endsWith(".jar")) {
            return;
        }
        try {
            JarFile jar = new JarFile(jarFilePath);
            Enumeration enumeration = jar.entries();
            while (enumeration.hasMoreElements()) {
                JarEntry jarEntry = (JarEntry) enumeration.nextElement();
                String[] names = jarEntry.getName().split("/");
                for (int i = 0; i < names.length; i++) {
                    String path = destDir;
                    for (int j = 0; j < (i + 1); j++) {
                        path += File.separator + names[j];
                    }
                    File file = new File(path);
//                    if (!file.exists()) {
                    if ((i == (names.length - 1)) && !jarEntry.getName().endsWith("/")) {
                        file.createNewFile();
                    } else {
                        file.mkdirs();
                        continue;
                    }
//                    } else {
//                        continue;
//                    }
                    InputStream is = jar.getInputStream(jarEntry);
                    FileOutputStream fos = new FileOutputStream(file);
                    if (pl != null) {
                        boolean writed = pl.probeFile(is, fos, jarEntry);
                        if (writed) {
                            fos.flush();
                            fos.close();
                            continue;
                        }
                    }
                    while (is.available() > 0) {
                        fos.write(is.read());
                    }
                    fos.flush();
                    fos.close();
                }
            }
            if (pl != null) {
                pl.unZipEnd(true, jar.getName());
            }
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            if (pl != null) {
                pl.unZipEnd(false, e.getMessage());
            }
        }

    }

    private static List list = new ArrayList();

    /**
     * 压缩jar包
     *
     * @param srcDir     需要压缩的文件
     * @param targetPath 压缩的jar包,存放路径
     */
    @SuppressWarnings("resource")
    public static void compressJar(String srcDir, String targetPath) {
        FileOutputStream fileOutputStream = null;
        CheckedOutputStream cs = null;
        ZipOutputStream out = null;
        InputStream ins = null;
        try{
            File file = new File(srcDir);
            //判断是否是目录
            if (file.isDirectory()) {
                if (list.size() > 0)
                    list.clear();
                byte b[] = new byte[128];
                // 压缩文件的保存路径
                System.out.println("开始压缩... " + file.getName() + "\n压缩文件保存至-> " + " " + targetPath);
                String zipFile;
                if (targetPath.endsWith(".jar")) {
                    zipFile = targetPath;
                } else {
                    zipFile = targetPath + File.separator + file.getName() + ".jar";
                    System.out.println("压缩文件:" + zipFile);
                }
                // 压缩文件目录
                //  String filepath = file.getAbsolutePath() + File.separator;
                List fileList = allFile(srcDir + File.separator);

                fileOutputStream = new FileOutputStream(zipFile);
                // 使用输出流检查
                cs = new CheckedOutputStream(fileOutputStream, new CRC32());
                // 声明输出zip流
                out = new ZipOutputStream(new BufferedOutputStream(cs));
                for (int i = 0; i < fileList.size(); i++) {
                    ins = new FileInputStream((String) fileList.get(i));
                    String fileName = ((String) (fileList.get(i))).replace(File.separatorChar, '/');
//                    System.out.println("ziping " + fileName);
                    String tmp = file.getName() + "/";
                    fileName = fileName.substring(fileName.lastIndexOf(tmp) + file.getName().length() + 1);
                    ZipEntry e = new ZipEntry(fileName);
                    out.putNextEntry(e);
                    int len = 0;
                    while ((len = ins.read(b)) != -1) {
                        out.write(b, 0, len);
                    }
                    out.closeEntry();
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (ins != null) {
                    ins.close();
                }
                if (out != null) {
                    out.close();
                }
                if (out != null) {
                    out.close();
                }
                if (cs != null) {
                    cs.close();
                }
                if (fileOutputStream != null) {
                    fileOutputStream.close();
                }
                list.clear();
                System.gc();//一定加上这个,否则源文件删除不了
            } catch (Exception e2) {
                // TODO: handle exception
            }

        }
    }

    /**
     * jar压缩
     *
     * @param parentDirPath 要压缩文件夹的父文件夹
     * @param targetPath    目标文件夹
     */
    public static void zipDirectory(String parentDirPath, String targetPath) {
        try {
            File dirFile = new File(parentDirPath);
            File[] listArr = dirFile.listFiles();
            for (File childFile : listArr) {
                if (childFile.isDirectory()) {
                    if (list.size() > 0)
                        list.clear();
                    byte b[] = new byte[128];
                    // 压缩文件的保存路径
                    String zipFile = targetPath + File.separator + childFile.getName() + ".jar";
                    // 压缩文件目录
                    String filepath = childFile.getAbsolutePath() + File.separator;
                    List fileList = allFile(filepath);
                    FileOutputStream fileOutputStream = new FileOutputStream(zipFile);
                    // 使用输出流检查
                    CheckedOutputStream cs = new CheckedOutputStream(fileOutputStream, new CRC32());
                    // 声明输出zip流
                    ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(cs));
                    for (int i = 0; i < fileList.size(); i++) {
                        InputStream in = new FileInputStream((String) fileList.get(i));
                        String fileName = ((String) (fileList.get(i))).replace(File.separatorChar, '/');
                        String tmp = childFile.getName() + "/";
                        fileName = fileName.substring(fileName.lastIndexOf(tmp) + childFile.getName().length() + 1);
                        ZipEntry e = new ZipEntry(fileName);
                        out.putNextEntry(e);
                        int len = 0;
                        while ((len = in.read(b)) != -1) {
                            out.write(b, 0, len);
                        }
                        out.closeEntry();
                    }
                    out.close();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static List allFile(String path) {
        File file = new File(path);
        String[] array = null;
        String sTemp = "";
        if (file.isDirectory()) {
        } else {
            return null;
        }
        array = file.list();
        if (array.length > 0) {
            for (int i = 0; i < array.length; i++) {
                sTemp = path + array[i];
                file = new File(sTemp);
                if (file.isDirectory()) {
                    allFile(sTemp + "/");

                } else {
                    list.add(sTemp);
                }
            }
        } else {
            return null;
        }
        return list;
    }
}

PrintUtil.java

package com.xxx.probe;

public class PrintUtil {
    public static void print(String content){
        System.out.println(content);
    }
}

ProbeListener.java

package com.xxx.probe;

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.jar.JarEntry;

public interface ProbeListener {
    boolean probeFile(InputStream is, FileOutputStream fos, JarEntry jarEntry);
    void unZipEnd(boolean result,String msg);
}

ProbePathUtil.groovy

package com.xxx.probe
import java.io.*
import java.lang.*

class ProbePathUtil {
    //G:\TEST_SPACE_xxx\MakeGradlePlugin\app\libs\baksmali-2.1.3/
    public static String getOutDirByFile(String jarFilePath){
        jarFilePath = jarFilePath.replaceAll("\\\\", "/")
        String outDir = jarFilePath.substring(0,jarFilePath.lastIndexOf("."))+"/"
        File outDirF = new File(outDir)
        if (!outDirF.exists())outDirF.mkdirs()
        return outDir
    }
    public static String getOutDirByFileNoSuffix(String jarFilePath){
        jarFilePath = jarFilePath.replaceAll("\\\\", "/")
        String outDir = jarFilePath.substring(0,jarFilePath.lastIndexOf("."))
        File outDirF = new File(outDir)
        if (!outDirF.exists())outDirF.mkdirs()
        return outDir
    }
    public static String getFileNameByPath(String path){
        String specPath = path.replaceAll("\\\\", "/")
        int pos = specPath.lastIndexOf("/")
        return path.substring(pos+1,specPath.length())
    }
    public static String getNameByPath(String path){
        String specPath = path.replaceAll("\\\\", "/")
        int pos = specPath.lastIndexOf("/")
        int lastPos = path.lastIndexOf(".")
        return path.substring(pos+1,lastPos)
    }
    //通过此方法拿出的字符串是正确的但是groovy运行观察发现通不过String.contains方法,不知为何
    @Deprecated
    public static String getSpecPath(String path){
        String specPath = path.replaceAll("\\.", "/")
        return specPath
    }
}

ProbePlugin.groovy

package com.xxx.probe

import com.android.build.gradle.AppExtension
import com.android.build.gradle.BaseExtension
import com.android.build.gradle.LibraryExtension
import org.gradle.api.Action
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.jvm.tasks.Jar;

public class ProbePlugin implements Plugin {
    @Override
    void apply(Project project) {
        Map map = project.getProperties()
        AppExtension appExtension = (AppExtension)map.get("android")
        appExtension.registerTransform(new TestProbeTransform(project))
    }
}

TestProbeTransform.groovy

import com.android.annotations.NonNull
import com.android.build.api.transform.DirectoryInput
import com.android.build.api.transform.Format
import com.android.build.api.transform.JarInput
import com.android.build.api.transform.QualifiedContent
import com.android.build.api.transform.Transform
import com.android.build.api.transform.TransformException
import com.android.build.api.transform.TransformInput
import com.android.build.api.transform.TransformInvocation
import com.android.build.api.transform.TransformOutputProvider
import com.android.build.gradle.internal.pipeline.TransformManager
import com.android.ide.common.internal.WaitableExecutor
import com.android.utils.FileUtils
import com.xxx.probe.FilterUtil
import com.xxx.probe.JarAvatar

import com.xxx.probe.ProbeListener
import com.xxx.probe.ProbePathUtil
import org.gradle.api.Project
import org.gradle.api.tasks.Input

import org.jacoco.core.instr.Instrumenter
import org.jacoco.core.runtime.OfflineInstrumentationAccessGenerator

import java.util.concurrent.Callable
import java.util.jar.JarEntry

import java.util.concurrent.Executors
import java.util.concurrent.ScheduledThreadPoolExecutor
import java.util.concurrent.atomic.AtomicInteger

class TestProbeTransform extends Transform {
    Project project

    TestProbeTransform(Project project) {
        this.project = project
    }

    @Override
    String getName() {
        return "TestProbeTransform"
    }

    @Override
    Set getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }

    @Input
    @NonNull
    public Set getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    @Override
    boolean isIncremental() {
        return false
    }

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        TransformOutputProvider outputProvider = transformInvocation.getOutputProvider()
        transformInvocation.inputs.each { TransformInput input ->
            input.jarInputs.each { JarInput jarInput ->
                handleJar(jarInput, outputProvider)
            }
            input.directoryInputs.each { DirectoryInput dirInput ->
                handleDir(dirInput, outputProvider)
            }
        }
        super.transform(transformInvocation)
    }

    void handleJar(JarInput jarInput, TransformOutputProvider outputProvider) {
        //输出文件
        File destJar = outputProvider.getContentLocation(jarInput.getName(), jarInput.getContentTypes(), jarInput.getScopes(), Format.JAR)
        //输入文件
        File srcJar = jarInput.file
        if (!FilterUtil.checkFilterJar(srcJar.name)) {
            println("【" + srcJar.name + "】插桩处理开始————————————————————————————————")
            String unzipDir = ProbePathUtil.getOutDirByFileNoSuffix(srcJar.path)
            clearUnZipCache(unzipDir)
            String jarName = ProbePathUtil.getNameByPath(srcJar.path)
            println("将解压至:" + unzipDir)
            JarAvatar.extractJar(unzipDir, srcJar.path, new ProbeListener() {
                @Override
                boolean probeFile(InputStream is, FileOutputStream fos, JarEntry jarEntry) {
                    String jarEntryName = jarEntry.name
                    if (!FilterUtil.checkFilterFile(jarEntryName)) {
                        String clazName = jarEntryName.contains("/") ? jarEntryName.substring(jarEntryName.lastIndexOf("/") + 1) : jarEntryName
//                        println("探针插入中:" + jarEntry.name + " " + clazName)
                        final Instrumenter instr = new Instrumenter(new OfflineInstrumentationAccessGenerator());
                        final byte[] instrumented = instr.instrument(is, clazName)
                        fos.write(instrumented)
                        return true
                    }
                    return false
                }

                @Override
                void unZipEnd(boolean result, String msg) {
                    JarAvatar.compressJar(unzipDir, destJar.path)
                    Thread.sleep(1000)
                    FileUtils.deleteRecursivelyIfExists(new File(unzipDir))
                    println("压缩打包完毕:成功?" + result + " \njar来源:" + msg)
                    println(">>>>>>>>插桩处理完毕————————————————————————————————\n")
                }
            })
        } else {
            println("忽略Jar包:" + srcJar.path)
            FileUtils.copyFile(srcJar, destJar)
        }
    }

    void handleDir(DirectoryInput dirInput, TransformOutputProvider outputProvider) {
        //输出文件
        File destJar = outputProvider.getContentLocation(dirInput.getName(), dirInput.getContentTypes(), dirInput.getScopes(), Format.DIRECTORY)
        //输入文件
        File srcJar = dirInput.file
        FileUtils.copyDirectory(srcJar, destJar)
    }

    //防止上次缓存导致两个版本的包出现增量问题
    private void clearUnZipCache(String unzipDir) {
        println("清除缓存差异:" + unzipDir)
        File f = new File(unzipDir)
        if (f.exists()) {
            try {
                FileUtils.deleteDirectoryContents()
            } catch (Exception e) {
            }
        }
    }
}

3.在resources/META-INF/gradle-plugins/jacoprobe.properties添加implementation-class=com.xxx.probe.ProbePlugin
此处表示的是插件的入口类
文件夹下可以定义多个.properties文件指向不同的入口类,这样就跟官方的com.android.applicationcom.android.libraryandroid引用方式相同,一个jar包多种引用名
4.在/app/build.gradle文件顶部添加apply plugin: 'jacoprobe'

期间遇到一个貌似关于androidx迁移的打包错误
在gradle.properties文件中添加转换过滤android.jetifier.blacklist=bcprov-jdk15on
经实际检验发现,没问题,至此结束本次处理.

你可能感兴趣的:(通过自定义Gradle插件编译时修改jar包完成Android组件覆盖率)