通過自定義Gradle插件編譯時修改jar包完成Android組件覆蓋率

前言

本次做jacoco覆蓋率發(fā)現(xiàn)典徊,主工程依賴的jar包都未產(chǎn)生覆蓋率數(shù)據(jù)淌实,熟悉jar包方式組件化架構(gòu)的同學(xué)應(yīng)該明白主工程僅是空殼,若按照網(wǎng)上教程做則壓根覆蓋不到業(yè)務(wù)組件圣蝎,必須解決焚鹊,經(jīng)研究發(fā)現(xiàn)痕届,AS的Gradle build插件執(zhí)行過程僅創(chuàng)建一個jacoco Task并對當(dāng)前工程源碼的class執(zhí)行離線模式字節(jié)碼插入,并不對工程依賴的jar包進(jìn)行插樁寺旺,so必須自定義一個Gradle插件來干預(yù)編譯流程爷抓,經(jīng)探索最初擬出兩種方案:
1.獨立腳本形式势决,通過監(jiān)聽gradle task與Build執(zhí)行周期完成自定義插樁操作阻塑,具體插樁行為通過調(diào)用cmd命令的形式去做,腳本受限程度較高果复,依賴于gradle腳本的api
2.獨立構(gòu)建插件形式陈莽,自定義Transfrom,編譯時開發(fā)者自定義的Transform將置于默認(rèn)Transform之前虽抄,以此處為錨點的自定義操作可以方便的插樁走搁。
Gradle構(gòu)建插件即apply plugin: 'com.android.application'它的jar包可以查看源碼并仿寫,位置大概在.gradle\caches\modules-2\files-2.1\com.android.tools.build\com.android.tools.build

注:腳本是基于gradle開放api的迈窟,構(gòu)建插件是基于android編譯插件的并非直接基于Gradle的Plugin私植,兩種并不完全相同,此處說構(gòu)建其實就是安卓的編譯過程车酣。

概念認(rèn)識

一.jacoco兩種模式
a.在線模式曲稼,即在虛擬機(jī)類加載時進(jìn)行插樁索绪,運行結(jié)束后本地不保存插樁字節(jié)碼
b.離線模式,即直接對.class文件進(jìn)行流讀取并將插樁字節(jié)碼保存在.class文件中贫悄,屬于侵入式

注:android獨有的運行環(huán)境決定了離線模式是唯一的模式瑞驱,因為它最后是odex字節(jié)碼,jacoco本身不支持dex字節(jié)碼的插樁窄坦,就算是app有熱修復(fù)那種可以掃描jar包并自定義類加載有錨點做處理唤反,也不能總在每次打開app上做處理,手機(jī)才多少資源

二.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本身不參與編譯流程(此段描述可能有誤)
我認(rèn)為Task本身就算一個mini任務(wù)逆趋,功能單一拥刻,其是獨立的,一般我們寫完就直接在腳本文件的左側(cè)用綠三角按鈕執(zhí)行父泳,如下圖:

image.png

那么編譯流程是如何串起來這些Task的般哼,除了跟gradle本身相關(guān)外,其實還跟apply plugin: 'com.android.application'這個插件有關(guān)惠窄,這個插件是Android的官方編譯插件蒸眠,看其源碼可以學(xué)到很多自定義插件的知識,路徑大致在.gradle\caches\modules-2\files-2.1\com.android.tools.build\com.android.tools.build很多Task是這個插件動態(tài)創(chuàng)建的杆融,包括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順序的嘗試池摧,結(jié)果未知。
四.編譯過程
兩張圖須知

gradle流程圖.png

編譯流程詳情圖.png

方法一:自定義獨立腳本

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

  • 操作步驟
    1.如下圖激况,調(diào)整至Project視圖作彤,在/app文件夾下創(chuàng)建一個JacocoLibProbe.gradle文件
    image.png

    2.在JacocoLibProbe.gradle文件中添加apply plugin: 'com.android.application';(添加目的在于寫groovy代碼時可以有只能提示乌逐,書寫發(fā)現(xiàn)在腳本中添加任意一個插件以引入gradle庫竭讳,寫groovy語言時才會有智能提示,同時需注意在寫groovy時使用未導(dǎo)包的變量或者類在編譯時會報no signal...什么錯誤需留意)
    3.實現(xiàn)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的周期回調(diào)绢慢,可以在Task執(zhí)行前與后做些配置,在transformClassesWithDexBuilderForDebug前操作即class轉(zhuǎn)為dex文件前在勾子函數(shù)中完成對所有jar包的插樁即可洛波,從此處開始就開啟了一個麻煩的步驟:
1.收集工程所有在線胰舆、離線依賴的包
2.解壓.aar逻杖、解壓.jar包
3.調(diào)用jacoco的包對解壓的.class文件進(jìn)行流讀取插樁并寫出保存至.class文件中
4.壓縮插樁后的解壓文件夾重新生成.aar或.jar包
經(jīng)我研究發(fā)現(xiàn)所有的第三方包在task鏈執(zhí)行前就已下載到位,所以不存在一邊轉(zhuǎn)碼一邊下載的情況思瘟,由于此次是自定義腳本荸百,并未找到在腳本中引入第三方j(luò)ar包進(jìn)行插樁的方法,也未找到gradle版本的插樁庫(嘗試過在app build.gradle中添加implementation 'org.jacoco:org.jacoco.core:0.7.9',在獨立腳本文件頭部添加import org.jacoco.core.instr.*皆無效)故將上述四個步驟用java寫成一個外部jar包滨攻,gradle腳本通過執(zhí)行cmd命令調(diào)起jar包進(jìn)行操作够话,但在后續(xù)研究中發(fā)現(xiàn)自定義Transform更加簡便。

方法二:自定義獨立插件

自定義獨立插件有兩種方式:

  1. 發(fā)布到maven上光绕,之后被工程在線引入
    優(yōu)點:在線引用對于當(dāng)前工程來說代碼分割工程更有條理女嘲,引用也方便,最主要的是在線依賴能被多個工程同時使用诞帐。
    缺點:以在線jar包方式依賴無法即改即用欣尼,還需要依賴外部發(fā)布平臺
  2. 直接放在當(dāng)前工程中,在編譯時工程會先編譯自定義插件之后再編譯主工程
    優(yōu)點:源碼放置在工程本地停蕉,可以隨時更改愕鼓,其本質(zhì)上就是未在settings.gradle配置的一個特殊android moudle工程,慧起。
    缺點:增加本地管理工作量菇晃,無法被多個工程引用

本次選用上述的第二種,不進(jìn)行發(fā)布蚓挤,即改即用

  • 操作步驟
    1.切換至Project視圖
    創(chuàng)建固定文件夾buildSrc(官方定義名稱)等文件如下所示磺送,其中紅框部分為人工創(chuàng)建,buildSrc文件夾下的.gradle灿意、build估灿、buildSrc.iml構(gòu)建后生成非人工創(chuàng)建,其本質(zhì)就是一個moudle工程缤剧,但不同的是它不可出現(xiàn)在settings.gradle文件中配置include馅袁,在創(chuàng)建main文件夾后,可以創(chuàng)建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 {
    /**
     * 精準(zhǔn)屏蔽不需要插樁的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 關(guān)鍵字
     * @return true:需要過濾  flase:不需要過濾
     */
    public static boolean checkFilterFile(String entryName) {
        if (entryName.contains(".class")) {
            //只對類進(jìn)行判斷
            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<String> 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<String> 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    目標(biāo)文件夾
     */
    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運行觀察發(fā)現(xiàn)通不過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<Project> {
    @Override
    void apply(Project project) {
        Map<String,?> 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<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }

    @Input
    @NonNull
    public Set<? super QualifiedContent.Scope> 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)
    }

    //防止上次緩存導(dǎo)致兩個版本的包出現(xiàn)增量問題
    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.application圃阳、com.android.library厌衔、android引用方式相同,一個jar包多種引用名
4.在/app/build.gradle文件頂部添加apply plugin: 'jacoprobe'

期間遇到一個貌似關(guān)于androidx遷移的打包錯誤
在gradle.properties文件中添加轉(zhuǎn)換過濾android.jetifier.blacklist=bcprov-jdk15on
經(jīng)實際檢驗發(fā)現(xiàn)捍岳,沒問題富寿,至此結(jié)束本次處理.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市锣夹,隨后出現(xiàn)的幾起案子页徐,更是在濱河造成了極大的恐慌,老刑警劉巖银萍,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件变勇,死亡現(xiàn)場離奇詭異,居然都是意外死亡贴唇,警方通過查閱死者的電腦和手機(jī)搀绣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來戳气,“玉大人豌熄,你說我怎么就攤上這事∥锟龋” “怎么了锣险?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長览闰。 經(jīng)常有香客問我芯肤,道長,這世上最難降的妖魔是什么压鉴? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任崖咨,我火速辦了婚禮,結(jié)果婚禮上油吭,老公的妹妹穿的比我還像新娘击蹲。我一直安慰自己,他們只是感情好婉宰,可當(dāng)我...
    茶點故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布歌豺。 她就那樣靜靜地躺著,像睡著了一般心包。 火紅的嫁衣襯著肌膚如雪类咧。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天,我揣著相機(jī)與錄音痕惋,去河邊找鬼区宇。 笑死,一個胖子當(dāng)著我的面吹牛值戳,可吹牛的內(nèi)容都是我干的议谷。 我是一名探鬼主播,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼堕虹,長吁一口氣:“原來是場噩夢啊……” “哼卧晓!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起鲫凶,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤禀崖,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后螟炫,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體波附,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年昼钻,在試婚紗的時候發(fā)現(xiàn)自己被綠了掸屡。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡然评,死狀恐怖仅财,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情碗淌,我是刑警寧澤盏求,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站亿眠,受9級特大地震影響碎罚,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜纳像,卻給世界環(huán)境...
    茶點故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一荆烈、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧竟趾,春花似錦憔购、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至山卦,卻和暖如春鞋邑,著一層夾襖步出監(jiān)牢的瞬間诵次,已是汗流浹背账蓉。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工枚碗, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人铸本。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓肮雨,卻偏偏與公主長得像,于是被迫代替她去往敵國和親箱玷。 傳聞我的和親對象是個殘疾皇子怨规,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,044評論 2 355

推薦閱讀更多精彩內(nèi)容