Android AOP(三):在Android中Javassist动态编译代码

Android AOP(三):在Android中Plugin Transform Javassist操作Class文件

Javassist作用是在编译器间修改class文件,与之相似的ASM(热修复框架女娲)也有这个功能,可以让我们直接修改编译后的class二进制代码,首先我们得知道什么时候编译完成,并且我们要赶在class文件被转化为dex文件之前去修改。在Transfrom这个api出来之前,想要在项目被打包成dex之前对class进行操作,必须自定义一个Task,然后插入到predex或者dex之前,在自定义的Task中可以使用javassist或者asm对class进行操作。而Transform则更为方便,Transfrom会有他自己的执行时机,不需要我们插入到某个Task前面。Tranfrom一经注册便会自动添加到Task执行序列中,并且正好是项目被打包成dex之前。


原理解释

1. Transfrom
Gradle是通过一个一个Task执行完成整个流程的,其中肯定也有将所有class打包成dex的task。
(在gradle plugin 1.5 以上和以下版本有些不同)

1.5以下,preDex这个task会将依赖的module编译后的class打包成jar,然后dex这个task则会将所有class打包成dex
1.5以上,preDex和Dex这两个task已经消失,取而代之的是TransfromClassesWithDexForDebug

2. 列表项
Transfrom是Gradle 1.5以上新出的一个api,其实它也是Task,不过定义方式和Task有点区别。
对于热补丁来说,Transfrom反而比原先的Task更好用。

在Transfrom这个api出来之前,想要在项目被打包成dex之前对class进行操作,必须自定义一个Task,然后插入到predex或者dex之前,在自定义的Task中可以使用javassist或者asm对class进行操作。

而Transform则更为方便,Transfrom会有他自己的执行时机,不需要我们插入到某个Task前面。Tranfrom一经注册便会自动添加到Task执行序列中,并且正好是项目被打包成dex之前。

3. Task的inputs和outputs
Gradle可以看做是一个脚本,包含一系列的Task,依次执行这些task后,项目就打包成功了。
而Task有一个重要的概念,那就是inputs和outputs。
Task通过inputs拿到一些东西,处理完毕之后就输出outputs,而下一个Task的inputs则是上一个Task的outputs。

例如:一个Task的作用是将java编译成class,这个Task的inputs就是java文件的保存目录,outputs这是编译后的class的输出目录,它的下一个Task的inputs就会是编译后的class的保存目录了。

4. Plugin
Gradle中除了Task这个重要的api,还有一个就是Plugin。
Plugin的作用是什么呢,这一两句话比较难以说明。
Gralde只能算是一个构建框架,里面的那么多Task是怎么来的呢,谁定义的呢?
是Plugin,细心的网友会发现,在module下的build.gradle文件中的第一行,往往会有apply plugin : ‘com.android.application’亦或者apply plugin : ‘com.android.library’。
com.android.application:这是app module下Build.gradle的
com.android.library:这是app依赖的module中的Builde.gradle的
就是这些Plugin为项目构建提供了Task,使用不同的plugin,module的功能也就不一样。
可以简单的理解为: Gradle只是一个框架,真正起作用的是plugin。而plugin的主要作用是往Gradle脚本中添加Task。 当然,实际上这些是很复杂的东西,plugin还有其他作用这里用不上。

使用步骤:

开发plugin,并应用到module

  • 新建一个module,选择library module
  • 删除module下的所有文件,除了build.gradle,清空build.gradle中的内容
  • 然后新建以下目录 src/main/groovy
  • 修改build.gradle如下
apply plugin: 'groovy'
apply plugin: 'maven'

dependencies {
    compile gradleApi()
    compile localGroovy()

    compile 'com.android.tools.build:gradle:3.0.0'
    compile 'org.javassist:javassist:3.22.0-GA'
    compile 'org.aspectj:aspectjtools:1.8.1'
}


repositories {
    mavenCentral()
}

//发布到本地
uploadArchives {
    repositories.mavenDeployer {
        repository(url: uri('../repo')) //仓库的路径,此处是项目根目录下的 repo 的文件夹
        pom.groupId = 'com.example'  //groupId ,自行定义,一般是包名
        pom.artifactId = 'plugin' //artifactId ,自行定义
        pom.version = '2.0.0' //version 版本号
    }
}
  • 开发插件MyPlugin
  • 开发MyClassTransform
  • 在transform()里面对目录和class注入操作
    代码如下:
package com.example

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

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 {


        System.out.println("----------------进入transform了--------------")

        //遍历input
        inputs.each { TransformInput input ->
            //遍历文件夹
            input.directoryInputs.each { DirectoryInput directoryInput ->
                //注入代码
                MyInjects.inject(directoryInput.file.absolutePath, mProject)

                // 获取output目录
                def dest = outputProvider.getContentLocation(directoryInput.name,
                        directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)//这里写代码片

                // 将input的目录复制到output指定目录
                FileUtils.copyDirectory(directoryInput.file, dest)
            }

            遍历jar文件 对jar不操作,但是要输出到out路径
            input.jarInputs.each { JarInput jarInput ->
                // 重命名输出文件(同目录copyFile会冲突)
                def jarName = jarInput.name
                println("jar = " + jarInput.file.getAbsolutePath())
                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)
            }
        }
        System.out.println("--------------结束transform了----------------")
    }

}
package com.example

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


public class MyPlugin implements Plugin<Project> {

    void apply(Project project) {
        System.out.println("------------------开始----------------------");
        System.out.println("这是我们的自定义插件!");
        //AppExtension就是build.gradle中android{...}这一块
        def android = project.extensions.getByType(AppExtension)

        //注册一个Transform
        def classTransform = new MyClassTransform(project);
        android.registerTransform(classTransform);

        //创建一个Extension,名字叫做testCreatJavaConfig 里面可配置的属性参照MyPlguinTestClass
        project.extensions.create("testCreatJavaConfig", MyPlguinTestClass)

        //生产一个类
        if (project.plugins.hasPlugin(AppPlugin)) {
            //获取到Extension,Extension就是 build.gradle中的{}闭包
            android.applicationVariants.all { variant ->
                //获取到scope,作用域
                def variantData = variant.variantData
                def scope = variantData.scope

                //拿到build.gradle中创建的Extension的值
                def config = project.extensions.getByName("testCreatJavaConfig");

                //创建一个task
                def createTaskName = scope.getTaskName("CeShi", "MyTestPlugin")
                def createTask = project.task(createTaskName)
                //设置task要执行的任务
                createTask.doLast {
                    //生成java类
                    createJavaTest(variant, config)
                }
                //设置task依赖于生成BuildConfig的task,然后在生成BuildConfig后生成我们的类
                String generateBuildConfigTaskName = variant.getVariantData().getScope().getGenerateBuildConfigTask().name
                def generateBuildConfigTask = project.tasks.getByName(generateBuildConfigTaskName)
                if (generateBuildConfigTask) {
                    createTask.dependsOn generateBuildConfigTask
                    generateBuildConfigTask.finalizedBy createTask
                }
            }

        }
        System.out.println("------------------结束了吗----------------------");
    }

    static def void createJavaTest(variant, config) {
        //要生成的内容
        def content = """package tv.danmaku.ijk.media.sample;

                        public class MyPlguinTestClass {
                            public static final String str = "${config.str}";
                        }
                        """;
        //获取到BuildConfig类的路径
        File outputDir = variant.getVariantData().getScope().getBuildConfigSourceOutputDir()

        def javaFile = new File(outputDir, "MyPlguinTestClass.java")

        javaFile.write(content, 'UTF-8');
    }
}
//
class MyPlguinTestClass {
    def str = "默认值";
}
package com.example

import javassist.ClassPool
import javassist.CtClass
import javassist.CtMethod
import org.gradle.api.Project

public class MyInjects {
    //初始化类池
    private final static ClassPool pool = ClassPool.getDefault();

    public static void inject(String path,Project project) {
        //将当前路径加入类池,不然找不到这个类
        pool.appendClassPath(path);
        //project.android.bootClasspath 加入android.jar,不然找不到android相关的所有类
        pool.appendClassPath(project.android.bootClasspath[0].toString());
        //引入android.os.Bundle包,因为onCreate方法参数有Bundle
        pool.importPackage("android.os.Bundle");

        File dir = new File(path);
        if (dir.isDirectory()) {
            //遍历文件夹
            dir.eachFileRecurse { File file ->
                String filePath = file.absolutePath
                println("filePath = " + filePath)
                if (file.getName().equals("FileListActivity.class")) {

                    //获取MainActivity.class
                    CtClass ctClass = pool.getCtClass("tv.danmaku.ijk.media.sample.FileListActivity");
                    println("ctClass = " + ctClass)
                    //解冻
                    if (ctClass.isFrozen())
                        ctClass.defrost()

                    //获取到OnCreate方法
                    CtMethod ctMethod = ctClass.getDeclaredMethod("onCreate")

                    println("方法名 = " + ctMethod)

                    String insetBeforeStr = """ android.widget.Toast.makeText(this,"我是被插入的Toast代码~!!",android.widget.Toast.LENGTH_SHORT).show();
                                                """
                    //在方法开头插入代码
                    ctMethod.insertBefore(insetBeforeStr);
                    ctClass.writeFile(path)
                    ctClass.detach()//释放
                }
            }
        }

    }
}

其实作用就是在找到tv.danmaku.ijk.media.sample.FileListActivity的oncreate方法插入一个toast。
然后新建main/resources/META-INF/gradle-plugins/plugin.test.properties

implementation-class=com.example.MyPlugin

此处文件名module里面会用到,class名对应plugin名。

执行gradle projects里面plugin的task upload,会在本地生成一个repo目录,保存生成的jar包

module 里同apply 插件名plugin.test

    ...
    apply plugin: 'plugin.test'
    ...
    dependencies {
    ...
        classpath 'com.example:plugin:2.0.0'
    }

sync project并make project后在module的build/intermediates/classes/debug…对应包名目录下生成
MyPlguinTestClass.java上面的如下代码实现。

 File outputDir = variant.getVariantData().getScope().getBuildConfigSourceOutputDir()

        def javaFile = new File(outputDir, "MyPlguinTestClass.java")

        javaFile.write(content, 'UTF-8');

同时安装APP后,会在弹出toast上面的如下代码实现。

                    //获取MainActivity.class
                    CtClass ctClass = pool.getCtClass("tv.danmaku.ijk.media.sample.FileListActivity");
                    println("ctClass = " + ctClass)
                    //解冻
                    if (ctClass.isFrozen())
                        ctClass.defrost()

                    //获取到OnCreate方法
                    CtMethod ctMethod = ctClass.getDeclaredMethod("onCreate")

                    println("方法名 = " + ctMethod)

                    String insetBeforeStr = """ android.widget.Toast.makeText(this,"我是被插入的Toast代码~!!",android.widget.Toast.LENGTH_SHORT).show();
                                                """
                    //在方法开头插入代码
                    ctMethod.insertBefore(insetBeforeStr);
                    ctClass.writeFile(path)
                    ctClass.detach()//释放

以上就是javaassit动态编译代码简单实现。

感谢,参考:
Android热补丁动态修复技术(三)—— 使用Javassist注入字节码,完成热补丁框架雏形(可使用)
Android动态编译技术:Plugin Transform Javassist操作Class文件
安卓AOP三剑客:APT,AspectJ,Javassist

你可能感兴趣的:(Android AOP(三):在Android中Javassist动态编译代码)