Android Gradle插件开发基础实例

    作为一名Android码农,相信大家也有跟我一样的感觉,很多框架或者第三方的SDK我们是只会用,但是很少去了解如何实现和它的原理是什么。主要也不是我比较懒,而是工作环境的影响很少有时间去研究。不过想成为一名技术资深的码农,了解原理并且学会自己造轮子是必走的路,我也开始反思了。

    好了言归正传,  相信很多同僚都知道build.gradle的作用,负责构建我们的App,语言用的是Groovy,但是其实里面真正起作用的是Plugin插件,如下图

Android Gradle插件开发基础实例_第1张图片

导入了一个id为xxx的插件,其中android{}都是这个插件的Task,gradle作用就是把一个一个Plugin一次执行。

    我这次要演示的批量给每个Activity的进入和退出插入Toast(第三方统计的方案有类似实现)。

    一般我们如果需要App每个页面的进入和退出加入埋点,我们初级版本会每个类的onCreate和onDestroy加入自己的埋点代码,高级版本可以自己封装一个BaseActivity,所有Activity继承它。而我今天要使用的是一个便于维护在各个App的方案,后续还可以看自己需要放到maven库。

    我们需要在java文件生成Class到Dex文件之间去修改类的Class文件,这样就能够不修改源码的情况下无感知的加入了我们的代码。

 

  • 首先使用AndroidStudio新建一个自己作为Demo的工程

Android Gradle插件开发基础实例_第2张图片Android Gradle插件开发基础实例_第3张图片

我这个叫

  • 然后创建一个Module

删除掉所有文件,只保留main目录(目录下文件清空)和build.gradle

Android Gradle插件开发基础实例_第4张图片

main\groovy目录下创建自己的包路径,我这里是cn.berfy.gradle.plugin。

在main目录下创建resources文件夹,新建META-INF\gradle-plugins目录,这里是存放最终给其他build.gradle调用apply的路径名。

新建cn.berfy.gradle.plugin.properties文件,编写

implementation-class=cn.berfy.gradle.plugin.TestPlugi

创建自己的包路径,我这里是cn.berfy.gradle.plugin

 

  • 再然后就是一步步写代码,有点长,耐心看完。

编写build.gradle文件

开头导入groovy,如果想用kotlin开发插件也可以导入。

apply plugin: 'maven-publish'

加入maven发布功能

dependencies 下引入gradleApi(gradleApi)和localGroovy(groovy Api)

引入com.android.tools.build:gradle支持

引入org.ow2.asm:asm:7.1(这个是真正修改字节码的Api)

groupId 你的maven库要显示的包名

artifactId  你的库:后面的名字

version 版本

repositories 是maven生成的repo文件路径,可以自定义,这里稍加修改可以自动上传到maven仓库

apply plugin: 'groovy'
apply plugin: 'kotlin'
//apply plugin: 'java-gradle-plugin'
//apply plugin: 'cn.berfy.gradle.plugin.TestPlugin'

dependencies {
    //gradle sdk
    implementation gradleApi()
    //groovy sdk
    implementation localGroovy()
    implementation 'com.android.tools.build:gradle:3.4.1'
    implementation 'org.ow2.asm:asm:7.1'
}

apply plugin: 'maven-publish'

sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8

publishing {
    publications {
        mavenJava(MavenPublication) {

            groupId 'cn.berfy.gradle.plugin'
            artifactId 'berfy'
            version '1.0.0'

            from components.java

        }
    }
}

publishing {
    repositories {
        maven {
            // change to point to your repo, e.g. http://my.org/repo
            url uri('src/main/repos')
        }
    }
}

----------------------------------------------------------------

groovy目录下你的包名根路径新建一个TestPlugin.groovy文件,内容为

AppExtension就是apply plugin: 'com.android.application'这个插件的扩展。

我们检测到这个Plugin就传递给MyClassTransform负责拦截App编译后的操作。

 

package cn.berfy.gradle.plugin

import org.gradle.api.Plugin
import org.gradle.api.Project
import com.android.build.gradle.AppExtension

class TestPlugin implements Plugin {
    void apply(Project project) {
        def android = project.extensions.getByType(AppExtension)
        //注册一个Transform
        def logTransform = new MyClassTransform(project)
        android.registerTransform(logTransform)
    }
}

新建MyClassTransform.groovy,负责class文件和jar文件的拦截,便于我们队class字节码修改。
getInputType这里用的是Class的枚举,拦截编译到class这步。

package cn.berfy.gradle.plugin;

import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.io.FileUtils
import org.gradle.api.Project
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassWriter

import java.io.IOException
import java.lang.reflect.Field;
import java.util.Collection;
import java.util.Set;

/**
 * Created by 刘镓旗 on 2017/8/30.
 */

public class MyClassTransform extends Transform {

    private Project mProject;

    public MyClassTransform(Project p) {
        this.mProject = p;
    }

    //transform的名称
    //transformClassesWithMyClassTransformForDebug 运行时的名字
    //transformClassesWith + getName() + For + Debug或Release
    @Override
    public String getName() {
        return "MyClassTransform";
    }

    //需要处理的数据类型,有两种枚举类型
    //CLASSES和RESOURCES,CLASSES代表处理的java的class文件,RESOURCES代表要处理java的资源
    @Override
    public Set getInputTypes() {
        return TransformManager.CONTENT_CLASS;
    }

//    指Transform要操作内容的范围,官方文档Scope有7种类型:
//
//    EXTERNAL_LIBRARIES        只有外部库
//    PROJECT                       只有项目内容
//    PROJECT_LOCAL_DEPS            只有项目的本地依赖(本地jar)
//    PROVIDED_ONLY                 只提供本地或远程依赖项
//    SUB_PROJECTS              只有子项目。
//    SUB_PROJECTS_LOCAL_DEPS   只有子项目的本地依赖项(本地jar)。
//    TESTED_CODE                   由当前变量(包括依赖项)测试的代码
    @Override
    public Set getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT;
    }

    //指明当前Transform是否支持增量编译
    @Override
    public boolean isIncremental() {
        return false;
    }

//    Transform中的核心方法,
//    inputs中是传过来的输入流,其中有两种格式,一种是jar包格式一种是目录格式。
//    outputProvider 获取到输出目录,最后将修改的文件复制到输出目录,这一步必须做不然编译会报错
//    @Override
//    public void transform(Context context,
//                          Collection inputs,
//                          Collection referencedInputs,
//                          TransformOutputProvider outputProvider,
//                          boolean isIncremental) throws IOException, TransformException, InterruptedException {
//        super.transform(context, inputs, referencedInputs, outputProvider, isIncremental)
//        println("字节码插桩的地方 哈哈哈哈")
//    }

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation)
        println("==============================TracePlugin visit start========================================")
        def isIncremental = transformInvocation.isIncremental()
        //OutputProvider管理输出路径,如果消费型输入为空,你会发现OutputProvider == null
        def outputProvider = transformInvocation.outputProvider
        if (!isIncremental) {
            //不需要增量编译,先清除全部
            outputProvider.deleteAll()
        }
        transformInvocation.inputs.forEach { input ->
            input.jarInputs.forEach { jarInput ->
                //处理Jar
                println("fix Jar name=" + jarInput.name)
                processJarInput(jarInput, outputProvider, isIncremental)
            }

            input.directoryInputs.forEach { directoryInput ->
                //处理文件
                println("fix file name=" + directoryInput.name)
                processDirectoryInput(directoryInput, outputProvider, isIncremental)
            }
        }
        println("==============================TracePlugin visit end========================================")
    }

    //============================================jar文件修改总入口=======================================================================
    //jar输入文件 修改
    void processJarInput(JarInput jarInput, TransformOutputProvider outputProvider, boolean isIncremental) {

        def dest = outputProvider.getContentLocation(
                jarInput.file.absolutePath,
                jarInput.contentTypes,
                jarInput.scopes,
                Format.JAR)
        if (isIncremental) {
            //处理增量编译
            processJarInputIncremental(jarInput, dest)
        } else {
            //不处理增量编译
            processJarInputNoIncremental(jarInput, dest)
        }
    }

    //jar 没有增量的修改
    void processJarInputNoIncremental(JarInput jarInput, File dest) {
        transformJarInput(jarInput, dest)
    }

    //jar 增量的修改
    void processJarInputIncremental(JarInput jarInput, File dest) {

        switch (jarInput.status) {
            case Status.NOTCHANGED:
                break;
            case Status.ADDED:
                //真正transform的地方
                transformJarInput(jarInput, dest)
                break;
            case Status.CHANGED:
                //Changed的状态需要先删除之前的
                if (dest.exists()) {
                    FileUtils.forceDelete(dest)
                }
                //真正transform的地方
                transformJarInput(jarInput, dest)
                break;
            case Status.REMOVED:
                //移除Removed
                if (dest.exists()) {
                    FileUtils.forceDelete(dest)
                }
                break;
        }
    }


    //真正执行jar修改的函数
    void transformJarInput(JarInput jarInput, File dest) {
        FileUtils.copyFile(jarInput.file, dest)

    }

    //============================================================文件及文件夹修改总入口======================================================================
    void processDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider, boolean isIncremental) {
        def dest = outputProvider.getContentLocation(
                directoryInput.file.absolutePath,
                directoryInput.contentTypes,
                directoryInput.scopes,
                Format.DIRECTORY)
        println("fix processDirectoryInput isIncremental=" + isIncremental)
        if (isIncremental) {
            //处理增量编译
            processDirectoryInputIncremental(directoryInput, dest)
        } else {
            processDirectoryInputNoIncremental(directoryInput, dest)
        }
    }

    //文件无增量修改
    void processDirectoryInputNoIncremental(DirectoryInput directoryInput, File dest) {
        println("fix processDirectoryInputNoIncremental ")
        transformDirectoryInput(directoryInput, dest)
    }

    //文件增量修改
    void processDirectoryInputIncremental(DirectoryInput directoryInput, File dest) {
        println("fix processDirectoryInputIncremental ")
        FileUtils.forceMkdir(dest)
        def srcDirPath = directoryInput.file.absolutePath
        def destDirPath = dest.absolutePath
        def fileStatusMap = directoryInput.changedFiles
        fileStatusMap.forEach { entry ->
            val inputFile = entry.key
            val status = entry.value
            val destFilePath = inputFile.absolutePath.replace(srcDirPath, destDirPath)
            val destFile = File(destFilePath)

            switch (status) {
                case Status.NOTCHANGED:

                    break;
                case Status.ADDED:
                    //真正transform的地方
                    transformDirectoryInput(directoryInput, dest)
                    break;
                case Status.CHANGED:
                    //处理有变化的
                    FileUtils.touch(destFile)
                    //Changed的状态需要先删除之前的
                    if (dest.exists()) {
                        FileUtils.forceDelete(dest)
                    }
                    //真正transform的地方
                    transformDirectoryInput(directoryInput, dest)
                    break;
                case Status.REMOVED:
                    //移除Removed
                    if (destFile.exists()) {
                        FileUtils.forceDelete(destFile)
                    }
                    break;
            }
        }
    }

    //真正执行文件修改的地方
    void transformDirectoryInput(DirectoryInput directoryInput, File dest) {
        println("fix transformDirectoryInput ")
//        directoryInput.forEach { directoryInput: DirectoryInput? ->
        //是否是目录
        if (directoryInput.file.isDirectory()) {
            println("fix transformDirectoryInput isDirectory ")
            List files = new ArrayList<>()
            findAllFiles(directoryInput.file.listFiles(), files)
            for (File file : files) {
                def name = file.name
                //在这里进行代码处理
                if (name.endsWith(".class") && !name.startsWith("R\$")
                        && "R.class" != name && "BuildConfig.class" != name) {

                    ClassReader classReader = new ClassReader(file.readBytes())
                    ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                    def className = name.split(".class")[0]
                    println("class fix chazhuang  " + className)
                    def classVisitor = new TraceVisitor(className, classWriter)
                    classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
                    def code = classWriter.toByteArray()
                    FileOutputStream fos = new FileOutputStream(file.parentFile.absoluteFile.toString() + File.separator + name)
                    fos.write(code)
                    fos.close()
                }
            }
        } else {
            println("fix transformDirectoryInput isFile ")
            def name = directoryInput.file.name
            //在这里进行代码处理
            if (name.endsWith(".class") && !name.startsWith("R\$")
                    && "R.class" != name && "BuildConfig.class" != name) {

//                    ClassReader classReader = ClassReader(file.readBytes())
//                    ClassWriter classWriter = ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
//                    def className = name.split(".class")[0]
                println("class fix hahahaha  " + name)
            }
        }

        //将修改过的字节码copy到dest,就可以实现编译期间干预字节码的目的了
        FileUtils.copyDirectory(directoryInput.file, dest)
    }

    void findAllFiles(File[] files, List outFiles) {
        if (null == outFiles) {
            return
        }
        for (File out : files) {
            if (out.isDirectory()) {
                findAllFiles(out.listFiles(), outFiles)
            } else {
                outFiles.add(out)
            }
        }
    }
}

transform(TransformInvocation transformInvocation)方法就是拦截到的class文件和jar包。

通过获取到class文件,修改,然后修改后的文件复制到输出路径中完成class的替换。这其中字节码的修改就是ASM的操作了,操作类是TraceVisitor.java。

创建TraceVisitor.java

package cn.berfy.gradle.plugin;

import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.commons.AdviceAdapter;

/**
 * 对继承自AppCompatActivity的Activity进行插桩
 */

public class TraceVisitor extends ClassVisitor {

    /**
     * 类名
     */
    private String className;

    /**
     * 父类名
     */
    private String superName;

    /**
     * 该类实现的接口
     */
    private String[] interfaces;

    public TraceVisitor(String className, ClassVisitor classVisitor) {
        super(Opcodes.ASM7, classVisitor);
    }

    /**
     * ASM进入到类的方法时进行回调
     *
     * @param access
     * @param name       方法名
     * @param desc
     * @param signature
     * @param exceptions
     * @return
     */
    @Override
    public MethodVisitor visitMethod(final int access, final String name, final String desc, final String signature,
                                     String[] exceptions) {
        MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions);

        methodVisitor = new AdviceAdapter(Opcodes.ASM5, methodVisitor, access, name, desc) {

            private boolean isInject() {
                //如果父类名是AppCompatActivity则拦截这个方法,实际应用中可以换成自己的父类例如BaseActivity
                if (superName.contains("AppCompatActivity")) {
                    return true;
                }
                return false;
            }

            @Override
            public void visitCode() {
                super.visitCode();

            }

            @Override
            public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
                return super.visitAnnotation(desc, visible);
            }

            @Override
            public void visitFieldInsn(int opcode, String owner, String name, String desc) {
                super.visitFieldInsn(opcode, owner, name, desc);
            }


            /**
             * 方法开始之前回调
             */
            @Override
            protected void onMethodEnter() {
                if (isInject()) {
                    if ("onCreate".equals(name)) {
                        mv.visitVarInsn(ALOAD, 0);
                        mv.visitMethodInsn(INVOKESTATIC,
                                "cn/berfy/demo/gradleplugin/asm/TraceUtil",
                                "onActivityCreate", "(Landroid/app/Activity;)V",
                                false);
                    } else if ("onDestroy".equals(name)) {
                        mv.visitVarInsn(ALOAD, 0);
                        mv.visitMethodInsn(INVOKESTATIC, "com/xuexuan/androidaop/traceutils/TraceUtil"
                                , "onActivityDestroy", "(Landroid/app/Activity;)V", false);
                    }
                }
            }

            /**
             * 方法结束时回调
             * @param i
             */
            @Override
            protected void onMethodExit(int i) {
                super.onMethodExit(i);
            }
        };
        return methodVisitor;

    }

    /**
     * 当ASM进入类时回调
     *
     * @param version
     * @param access
     * @param name       类名
     * @param signature
     * @param superName  父类名
     * @param interfaces 实现的接口名
     */
    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces);
        this.className = name;
        this.superName = superName;
        this.interfaces = interfaces;
    }
}

ClassVisitor是对类的字节码操作,MethodVisitor是方法。

isInject()拦截类的类型,这里我们只处理AppCompatActivity。

onMethodEnter()方法执行的开始。我们在这里增加自己的代码。

Android Gradle插件开发基础实例_第5张图片

具体的代码意思就是在onCreate方法开始执行的时候,插入cn.berfy.demo.gradleplugin/asm/TraceUtil.class类中的onActivityCreate的方法中的内容。具体可以学习下ASM语法和JVM字节码说明。

TraceUtil.class类可以写在module或者App module中,只要保证运行的class包名正确即可。

这样每个Acitivity就会在onCreate生命周期是调用我们自己类的方法(也不是调用,是插桩,可以叫代码复制)。

我的App module下的TraceUtil.class

package cn.berfy.demo.gradleplugin.asm;

import android.app.Activity;
import android.widget.Toast;

import cn.berfy.demo.gradleplugin.MainActivity;

/**
 * Created by will on 2018/3/9.
 */

public class TraceUtil
{
    private final String TAG = "TraceUtil";

    /**
     * 当Activity执行了onCreate时触发
     *
     * @param activity
     */
    public static void onActivityCreate(Activity activity)
    {
        Toast.makeText(activity, MainActivity.class.getName() + " 我是插桩的检测代码", Toast.LENGTH_LONG).show();
//        System.out.println(MainActivity.class);
    }


    /**
     * 当Activity执行了onDestroy时触发
     *
     * @param activity
     */
    public static void onActivityDestroy(Activity activity)
    {
        Toast.makeText(activity, activity.getClass().getName() + "call onDestroy", Toast.LENGTH_LONG).show();
    }
}

插件到这里就编写好了

我们到terminal中执行gradlew publish等待成功。如果失败看一眼log,很好理解。

成功之后main\repo\目录就会生成对应你plugin下的build.gradle中的groupId\artifactId\version\的pom源文件。

 

  • 最后

关键的关键

项目根目录build.gradle加入maven库和classPath

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
    repositories {
        google()
        jcenter()
        maven {//local maven repo path
            url uri('plugin/src/main/repos')
        }
    }
    dependencies {
        classpath "com.android.tools.build:gradle:4.1.3"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.72"
        classpath 'cn.berfy.gradle.plugin:berfy:1.0.0'
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

App module的build.gradle

plugins {
    id 'com.android.application'
}
apply plugin: 'cn.berfy.gradle.plugin.TestPlugin'

最终运行Android Gradle插件开发基础实例_第6张图片

 

你可能感兴趣的:(android,android)