Gradle系列 (中篇) —在自定义Gradle插件中使用javassist往class中注入代码

上一篇我们已经详细讲了如何自定义Gradle插件,没有学习的小伙伴可以链接过去学习哦:Gradle系列 (上篇) —Android自定义Gradle插件并在项目中使用,那么今天我们就来讲一下如何在已完成的自定义插件中完成对class文件代码的注入。

Transform API

如何将指定代码注入到class文件中?Google专门提供了Transform API来解决这类问题。

Starting with 1.5.0-beta1, the Gradle plugin includes a Transform API allowing 3rd party plugins to manipulate compiled class files before they are converted to dex files.
(The API existed in 1.4.0-beta2 but it's been completely revamped in 1.5.0-beta1)

The goal of this API is to simplify injecting custom class manipulations without having to deal with tasks, and to offer more flexibility on what is manipulated. The internal code processing (jacoco, progard, multi-dex) have all moved to this new mechanism already in 1.5.0-beta1.
Note: this applies only to the javac/dx code path. Jack does not use this API at the moment.

The API doc is http://google.github.io/android-gradle-dsl/javadoc/.

To insert a transform into a build, you simply create a new class implementing one of the Transform interfaces, and register it with android.registerTransform(theTransform) or android.registerTransform(theTransform, dependencies).

大概意思就是:
从1.5.0-beta1开始,Gradle插件包含一个Transform API,允许第三方插件在将已编译的类文件转换为dex文件之前对其进行操作,也就是允许第三方 Plugin 在打包 dex 文件之前的编译过程中操作 .class 文件(该API已存在于1.4.0-beta2中,但已在1.5.0-beta1中进行了彻底修改)

该API的目标是简化注入自定义类的操作而不必处理任务,并为操作内容提供更大的灵活性。内部代码处理(jacoco,progard,multi-dex)都已在1.5.0-beta1中移至此新机制。该API文档为:Gradle Android Plugin API Javadoc

要将转换插入到构建中,只需创建一个实现Transform接口之一的新类,并向android.registerTransform(theTransform)或android.registerTransform(theTransform,依赖项)注册即可。

接下来看一下Transform的工作流程

Transform 将输入进行处理,然后写入到指定的目录下作为下一个 Transform 的输入源,整个过程是连续不间断的,否则就会报错。

Javassist

Java 字节码以二进制的形式存储在 .class 文件中,每一个 .class 文件包含一个 Java 类或接口。Javaassist 就是一个用来 处理 Java 字节码的类库。它可以在一个已经编译好的类中添加新的方法,或者是修改已有的方法,并且不需要对字节码方面有深入的了解。关于Javassist可以参考学习下面这个链接,它是对javassist官方文档的中文翻译:Javassist官方文档中文翻译

注入代码

为了使用Transform和Javassist,我们需要先依赖进来,在myplugin的build.gradle中添加以下代码:

implementation 'com.android.tools.build:gradle:3.6.1'
implementation 'org.javassist:javassist:3.24.0-GA'

截图如下:

然后新建MyTransform.groovy文件,代码如下:

package com.zhuyong.myplugin

import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import com.android.utils.FileUtils
import org.apache.commons.codec.digest.DigestUtils
import org.gradle.api.Project

/**
 * 向calss文件中注入代码
 */
public class MyTransform extends Transform {

    private static final String DEFAULT_NAME = "__MyTransformEditClasses__"

    private static final Set SCOPES = new HashSet<>();

    static {
        SCOPES.add(QualifiedContent.Scope.PROJECT);
        SCOPES.add(QualifiedContent.Scope.SUB_PROJECTS);
        SCOPES.add(QualifiedContent.Scope.EXTERNAL_LIBRARIES);
    }

    private Project project

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

    /**
     * 设置自定义的Transform对应的Task名称
     * @return Task名称
     */
    @Override
    public String getName() {
        return DEFAULT_NAME
    }

    /**
     * 需要处理的数据类型,CONTENT_CLASS代表处理class文件
     * @return
     */
    @Override
    public Set getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }

    /**
     * 指定Transform的作用范围
     * @return
     */
    @Override
    public Set getScopes() {
        return SCOPES
    }

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

    @Override
    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        TransformOutputProvider outputProvider = transformInvocation.outputProvider;

        for (TransformInput input : transformInvocation.inputs) {

            if (null == input) continue
            //遍历文件夹
            for (DirectoryInput directoryInput : input.directoryInputs) {
                // 获取output目录
                def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
                // 将input的目录复制到output指定目录
                FileUtils.copyDirectory(directoryInput.file, dest)
            }

            //遍历jar文件 对jar不操作,但是要输出到out路径
            for (JarInput jarInput : input.jarInputs) {
                // 重命名输出文件
                def jarName = jarInput.name
                def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
                if (jarName.endsWith(".jar")) {
                    jarName = jarName.substring(0, jarName.length() - 4)
                }
                def dest = outputProvider.getContentLocation(jarName + md5Name, jarInput.contentTypes, jarInput.scopes, Format.JAR)
                FileUtils.copyFile(jarInput.file, dest)
            }
        }
    }
}

OK ,Transform已经新建好了,接下来只需要在MyPlugin中注册即可。

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

class MyPlugin implements Plugin {

    @Override
    void apply(Project project) {
        println("=================================")
        println("======这是我的自定义Gradle插件======")
        println("=================================")

        //AppExtension对应build.gradle中android{...}
        def android = project.extensions.getByType(AppExtension.class)
        //注册一个Transform
        def classTransform = new MyTransform(project)
        android.registerTransform(classTransform)
    }
}

写到这里就完成了注册,试一下能不能重新生成maven包,执行uploadArchives发现没有问题。接下来就来编写注入代码的代码:假设我们要在MainActivity的showToast()方法中插入一个Toast代码,MainActivity如下:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        showToast();
    }

    /**
     * 弹出一个Toast
     */
    public void showToast() {

    }
}

我们先来新建一个类,专门用来处理代码的注入:

package com.zhuyong.myplugin

import javassist.*
import org.gradle.api.Project

class InjectClass {

    //初始化类池,以单例模式获取
    private final static ClassPool pool = ClassPool.getDefault()

    static void inject(String path, Project project, String injectCode) throws NotFoundException, CannotCompileException {
        println("filePath = " + path)
        //将当前路径加入类池,不然找不到这个类
        pool.appendClassPath(path)
        //为了能找到android相关的所有类,添加project.android.bootClasspath 加入android.jar,
        pool.appendClassPath(project.android.bootClasspath[0].toString())

        File dir = new File(path)
        //判断如果是文件夹,则遍历文件夹
        if (dir.isDirectory()) {
            //开始遍历
            dir.eachFileRecurse { File file ->
                if (file.getName().equals("MainActivity.class")) {
                    //获取到要修改的class文件
                    CtClass ctClass = pool.getCtClass("com.zhuyong.gradledemo.MainActivity")
                    if (null != ctClass) {
                        println "正在操作的路径 = " + file.getAbsolutePath()
                        //判断一个类是否已被冻结,如果被冻结,则进行解冻,使其可以被修改
                        if (ctClass.isFrozen()) ctClass.defrost()
                        //获取到方法
                        CtMethod ctMethod = ctClass.getDeclaredMethod("showToast")
                        println "要插入的代码 = " + injectCode

                        ctMethod.insertBefore(injectCode)//在方法开始注入代码
//                        ctMethod.insertAfter(injectCode)//在方法结尾注入代码
//                        ctMethod.insertAt(18, injectCode)//在class文件的某一行插入代码,前提是class包含行号信息

                        ctClass.writeFile(path)//根据CtClass生成.class文件;
                        /**
                         * 将该class从ClassPool中删除
                         *
                         * ClassPool 会在内存中维护所有被它创建过的 CtClass,当 CtClass 数量过多时,会占用大量的内存,
                         * API中给出的解决方案是 有意识的调用CtClass的detach()方法以释放内存。
                         */
                        ctClass.detach()
                    }
                }
            }
        }
    }
}

接下来就是使用在MyTransform这个文件遍历文件夹的方法中使用InjectClass这个方法,代码如下:(以下代码是添加到MyTransform这个文件中)

def injectCode = "android.widget.Toast.makeText(this,\"这是我插入的代码\",android.widget.Toast.LENGTH_SHORT).show();"
    
//遍历文件夹
for (DirectoryInput directoryInput : input.directoryInputs) {
    //注入代码
    InjectClass.inject(directoryInput.file.absolutePath, project, injectCode)
    // 获取output目录
    def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
    // 将input的目录复制到output指定目录
    FileUtils.copyDirectory(directoryInput.file, dest)
}
          

OK,到这里就可以重新执行uploadArchives打包了,然后clean一下整个项目,再执行make project就可以了。此时可以看到Task执行的过程中包含了transformClassesWith__MyTransformEditClasses__ForDebug,这个是注册MyTransform生成的对应的Task。

打开以下文件夹即可看到如下代码已经插入:

OK,我们安装一下生成的debugAPK,看一下会不会弹出Toast。

安装到手机上,执行效果如下:

OK,Toast已经弹出来了,这代表被打到apk包里的dex文件中的class文件,已经成功的被注入了代码,到这里基本上已经完成了本篇文章的目的,但是我们再来完善一下。我们的代码injectCode变量是写死在MyTransform类中的,显示这不符合我们maven包的要求,无法满足依赖以后的自定义效果,最完美的效果应该是注入的代码是可以在app中进行配置的(要注入的类、or类中的方法、or注入到什么位置,都是可以配置的,这里以注入代码内容为例)。

Gradle脚本中是通过Extension传递一些配置参数给自定义插件的,就像上面我们用到的AppExtension,有兴趣的可以到这个类以及父类中去看一下,有些代码变量是我们在build.gradle中熟悉的不能再熟悉的了。所以,我们可以自定义一个extension,通过extension对象传递要注入的代码。首先创建一个类为InjectCodeToClass,用来声明参数(被注入的代码):

package com.zhuyong.myplugin

class InjectCodeExtension {
    def injectCode
}

然后在extensions容器中添加一个名称为InjectCodeToClass,类型为InjectCodeExtension的对象,代码如下:

package com.zhuyong.myplugin

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

class MyPlugin implements Plugin {

    @Override
    void apply(Project project) {
        println("=================================")
        println("======这是我的自定义Gradle插件======")
        println("=================================")

        //AppExtension对应build.gradle中android{...}
        def android = project.extensions.getByType(AppExtension.class)
        //注册一个Transform
        def classTransform = new MyTransform(project)
        android.registerTransform(classTransform)

        // 通过Extension的方式传递将要被注入的自定义代码
        def extension = project.extensions.create("InjectCodeToClass", InjectCodeExtension)
        project.afterEvaluate {
            classTransform.injectCode = extension.injectCode
        }
    }
}

至此已经完成了插件的修改,再次执行uploadArchives进行重新打包(注意:每次修改完插件里的代码都要重新执行uploadArchives),然后就可以在apply该插件的地方进行赋值了。现在,在app的build.gradle中添加以下代码,就完成了对InjectCodeToClass对象的赋值,执行完成后将要注入的代码传递给Transform对象:

apply plugin: 'custom-gradle-plugin'

InjectCodeToClass {
    injectCode = "android.widget.Toast.makeText(this,\"这是我第二次插入的代码\",android.widget.Toast.LENGTH_SHORT).show();"
}

截图如下:

执行Sync即完成了传值。我们再来验证一下即可:

大功告成,项目代码已上传至 Github。

下一篇我们来讲如何在module(Android Library,非app)中依赖自定义插件并完成修改module中的class文件,它和在app module中依赖有什么区别呢?会踩到什么坑呢?请看Gradle系列 (下篇) —在Android Library中依赖自定义Gradle插件并往class中注入代码(暂未发布)!!!

你可能感兴趣的:(Gradle系列 (中篇) —在自定义Gradle插件中使用javassist往class中注入代码)