Android Gradle高级用法,动态编译技术:Plugin Transform Javassist操作Class文件

动态编译技术在开源框架中的应用非常的广泛,现在市面上的插件化框架,热修复框架几乎都使用了动态编译技术,原理几乎都是在编译期间动态的在class文件中注入代码或者或修改。那就让我们来了解一下这高大上的技术吧。

本章会以完成一个demo的形式来揭开动态编译的神秘面纱,将会分成三步

一、自定义Gradle插件

二、介绍Transform API 及 实现步骤 

三、实现在编译的过程中操作.class文件,对原有代码或者说逻辑进 行一些处理,修改原class代码和动态生成java类

一、自定义gradle插件:在我们app下的build.gradle中的第一行是apply plugin: ‘com.android.application’ ,其实这就是一个插件,是google写好的插件,我们自己写插一个插件也是这样引入,引入后我们的插件就会得到执行。

1.1 创建一个module,什么样的都可以,不管是Phone&Tablet Module或Android Librarty都可以,然后只留下src/main和build.gradle,其他的文件全部删除 
这里写图片描述

1.2 在main目录下创建groovy文件夹,然后在groovy目录下就可以创建我们的包名和groovy文件了,记得后缀要已.groovy结尾。在这个文件中引入创建的包名,然后写一个Class继承于Plugin< Project > 并重写apply方法。例如:

package com.ljq.myreplugin.gradle

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("这是我们的自定义插件!");
      System.out.println("------------------结束----------------------->");
  }
}

1.3 在main目录下创建resources文件夹,继续在resources下创建META-INF文件夹,继续在META-INF文件夹下创建gradle-plugins文件夹,最后在gradle-plugins文件夹下创建一个xxx.properties文件,注意:这个xxx就是在app下的build.gradle中引入时的名字,例如:apply plugin: ‘xxx’。在文件中写implementation-class=com.ljq.myreplugin.gradle.MyPlugin。

1.4 打开build.gradle 删除里面所有的内容。然后格式按这个写,uploadArchives是上传到maven库,然后执行uploadArchives这个task,就将我们的这个插件打包上传到了本地maven中,可以去本地的Maven库中查看

apply plugin: 'groovy'
apply plugin: 'maven'


dependencies {
  //gradle sdk
  compile gradleApi()
  //groovy sdk
  compile localGroovy()
}


repositories {
  mavenCentral()
}
//以上都为固定写法

//group和version
group='com.ljq.myreplugin.gradle'
version='1.0.0'

//打包到本地或者远程Maven库
uploadArchives {
  repositories {
      mavenDeployer {
          //提交到远程服务器:
          // repository(url: "http://www.xxx.com/repos") {
          //    authentication(userName: "admin", password: "admin")
          // }
          //本地的Maven地址设置为E:/Maven
          repository(url: uri('E:/repo'))
      }
  }
}

这里写图片描述

这里写图片描述

1.5 应用gradle插件:在项目下的build.gradle(也可以在module中)中的repositories模块中定义本地maven库地址。在dependencies 模块中引入我们的插件的路径,例:

这里写图片描述

1.6 最后在module(app下的)下的build.gradle中引入插件,apply plugin: ‘my_replugin_gradle’ 这个名字是我们定义的resource文件下的那个.properties文件的名字,现在可以clean一下工程,然后在Make一下 ,可以在Gradle Console中看到我们的插件得到了执行,说明我们自定义的Plugin成功了.

这里写图片描述

1.7 还有一种简单的方式创建gradle插件,新建module时必须要把module的名称叫做buildsrc,不需要再main下创建resources文件,在gradle插件的build.gradle中也不需要加入上传maven的代码,因为没有上传到maven中,所以在引入的时候项目build.gradle中也不需要加入maven库地址的代码。在app的module引入时直接写插件全类名就好,例如:apply plugin: com.ljq.myreplugin.gradle.MyPlugin。其他都和上边相同就好。

二、利用Google提供的Transform API 在编译的过程中操作.class文件。先说一下Transform是什么:

gradle从1.5开始,gradle插件包含了一个叫Transform的API,这个API允许第三方插件在class文件转为为dex文件前操作编译好的class文件,这个API的目标是简化自定义类操作,而不必处理Task,并且在操作上提供更大的灵活性。并且可以更加灵活地进行操作。 
官方文档:http://google.github.io/android-gradle-dsl/javadoc/ 
我们接着在上面的demo中继续完成使用Transform API,

2.1 在我们自定义的gradle插件的build.gradle中引入transform的包,下面会进行代码注入,就一起引入的其他包

这里写图片描述

2.2 创建一个类继承Transform 并实现其方法:

package com.ljq.myreplugin.gradle

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

/**
 * 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<QualifiedContent.ContentType> 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<QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT;
    }

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

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

    }

}
  • >2.3 在我们自定义的gradle插件的apply方法中注册自定义的Transform,
    def android = project.extensions.getByType(AppExtension)
    //注册一个Transform
    def classTransform = new MyClassTransform(project);
    android.registerTransform(classTransform);

**三、使用Javassist实现在编译的过程中操作.class文件,动态注入代码Javassist是一个可以用来分析、编辑和创建Java字节码的开源类库 对于语法和使用,请自行查阅资料。**

我们知道studio会给我们创建一个BuildConfig的类,但是是否知道这个类是怎么生成的呢?下面我们来模拟一下 在app下的build.gradle下可以创建参数列表,然后将参数生成一个java类,在代码中就可以使用了 >

//我们自定义的
testCreatJavaConfig{
    str = "动态生成java类的字符串"
}
  • 然后回到我们的自定义的Plugin中,贴一下整个代码

  1. package com.ljq.myreplugin.gradle  
  2.   
  3. import com.android.build.gradle.AppExtension  
  4. import com.android.build.gradle.AppPlugin  
  5. import org.gradle.api.Plugin  
  6. import org.gradle.api.Project  
  7.   
  8. public class MyPlugin implements Plugin {  
  9.   
  10.     void apply(Project project) {  
  11.         System.out.println("------------------开始----------------------");  
  12.         System.out.println("这是我们的自定义插件!");  
  13.         //AppExtension就是build.gradle中android{...}这一块  
  14.         def android = project.extensions.getByType(AppExtension)  
  15.   
  16.         //注册一个Transform  
  17.         def classTransform = new MyClassTransform(project);  
  18.         android.registerTransform(classTransform);  
  19.   
  20.         //创建一个Extension,名字叫做testCreatJavaConfig 里面可配置的属性参照MyPlguinTestClass  
  21.         project.extensions.create("testCreatJavaConfig", MyPlguinTestClass)  
  22.   
  23.         //生产一个类  
  24.         if (project.plugins.hasPlugin(AppPlugin)) {  
  25.             //获取到Extension,Extension就是 build.gradle中的{}闭包  
  26.             android.applicationVariants.all { variant ->  
  27.                 //获取到scope,作用域  
  28.                 def variantData = variant.variantData  
  29.                 def scope = variantData.scope  
  30.   
  31.                 //拿到build.gradle中创建的Extension的值  
  32.                 def config = project.extensions.getByName("testCreatJavaConfig");  
  33.   
  34.                 //创建一个task  
  35.                 def createTaskName = scope.getTaskName("CeShi", "MyTestPlugin")  
  36.                 def createTask = project.task(createTaskName)  
  37.                 //设置task要执行的任务  
  38.                 createTask.doLast {  
  39.                     //生成java类  
  40.                     createJavaTest(variant, config)  
  41.                 }  
  42.                 //设置task依赖于生成BuildConfig的task,然后在生成BuildConfig后生成我们的类  
  43.                 String generateBuildConfigTaskName = variant.getVariantData().getScope().getGenerateBuildConfigTask().name  
  44.                 def generateBuildConfigTask = project.tasks.getByName(generateBuildConfigTaskName)  
  45.                 if (generateBuildConfigTask) {  
  46.                     createTask.dependsOn generateBuildConfigTask  
  47.                     generateBuildConfigTask.finalizedBy createTask  
  48.                 }  
  49.             }  
  50.   
  51.         }  
  52.         System.out.println("------------------结束了吗----------------------");  
  53.     }  
  54.   
  55.     static def void createJavaTest(variant, config) {  
  56.         //要生成的内容  
  57.         def content = """package com.ljq.myreplugindemo;  
  58.   
  59.                         /** 
  60.                          * Created by 刘镓旗 on 2017/8/30. 
  61.                          */  
  62.   
  63.                         public class MyPlguinTestClass {  
  64.                             public static final String str = "${config.str}";  
  65.                         }  
  66.                         """;  
  67.         //获取到BuildConfig类的路径  
  68.         File outputDir = variant.getVariantData().getScope().getBuildConfigSourceOutputDir()  
  69.   
  70.         def javaFile = new File(outputDir, "MyPlguinTestClass.java")  
  71.   
  72.         javaFile.write(content, 'UTF-8');  
  73.     }  
  74. }  
  75.   
  76. class MyPlguinTestClass {  
  77.     def str = "默认值";  
  78. }  


 

 

来看一下效果

这里写图片描述

下面我们利用Transform在MainActivity中动态的插入代码,先看一下现在的MainAcitivity

这里写图片描述

可以看到上面的setText中使用的是我们上面动态生成的类中的字段,看一下怎么利用Transform插入代码,先看一下Transform中代码

 

[java] view plain copy

  1. @Override  
  2. public void transform(Context context,  
  3.                       Collection inputs,  
  4.                       Collection referencedInputs,  
  5.                       TransformOutputProvider outputProvider,  
  6.                       boolean isIncremental) throws IOException, TransformException, InterruptedException {  
  7.     System.out.println("----------------进入transform了--------------")  
  8.   
  9.     //遍历input  
  10.     inputs.each { TransformInput input ->  
  11.         //遍历文件夹  
  12.         input.directoryInputs.each { DirectoryInput directoryInput ->  
  13.             //注入代码  
  14.             MyInjects.inject(directoryInput.file.absolutePath, mProject)  
  15.   
  16.             // 获取output目录  
  17.             def dest = outputProvider.getContentLocation(directoryInput.name,  
  18.                     directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)这里写代码片  
  19.   
  20.             // 将input的目录复制到output指定目录  
  21.             FileUtils.copyDirectory(directoryInput.file, dest)  
  22.         }  
  23.   
  24.         遍历jar文件 对jar不操作,但是要输出到out路径  
  25.         input.jarInputs.each { JarInput jarInput ->  
  26.             // 重命名输出文件(同目录copyFile会冲突)  
  27.             def jarName = jarInput.name  
  28.             println("jar = " + jarInput.file.getAbsolutePath())  
  29.             def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())  
  30.             if (jarName.endsWith(".jar")) {  
  31.                 jarName = jarName.substring(0, jarName.length() - 4)  
  32.             }  
  33.             def dest = outputProvider.getContentLocation(jarName + md5Name, jarInput.contentTypes, jarInput.scopes, Format.JAR)  
  34.             FileUtils.copyFile(jarInput.file, dest)  
  35.         }  
  36.     }  
  37.     System.out.println("--------------结束transform了----------------")  
  38. }  


 

 

最重要生成代码在MyInjects类中,在这个类中我们传入了两个参数,一个是当前变量的文件夹,一个是当前的工程对象,来看一下代码

 

[java] view plain copy

  1. package com.ljq.myreplugin.gradle  
  2.   
  3. import javassist.ClassPool  
  4. import javassist.CtClass  
  5. import javassist.CtMethod  
  6. import org.gradle.api.Project  
  7. /** 
  8.  * Created by 刘镓旗 on 2017/8/31. 
  9.  */  
  10.   
  11. public class MyInjects {  
  12.     //初始化类池  
  13.     private final static ClassPool pool = ClassPool.getDefault();  
  14.   
  15.     public static void inject(String path,Project project) {  
  16.         //将当前路径加入类池,不然找不到这个类  
  17.         pool.appendClassPath(path);  
  18.         //project.android.bootClasspath 加入android.jar,不然找不到android相关的所有类  
  19.         pool.appendClassPath(project.android.bootClasspath[0].toString());  
  20.         //引入android.os.Bundle包,因为onCreate方法参数有Bundle  
  21.         pool.importPackage("android.os.Bundle");  
  22.   
  23.         File dir = new File(path);  
  24.         if (dir.isDirectory()) {  
  25.             //遍历文件夹  
  26.             dir.eachFileRecurse { File file ->  
  27.                 String filePath = file.absolutePath  
  28.                 println("filePath = " + filePath)  
  29.                 if (file.getName().equals("MainActivity.class")) {  
  30.   
  31.                     //获取MainActivity.class  
  32.                     CtClass ctClass = pool.getCtClass("com.ljq.myreplugindemo.MainActivity");  
  33.                     println("ctClass = " + ctClass)  
  34.                     //解冻  
  35.                     if (ctClass.isFrozen())  
  36.                         ctClass.defrost()  
  37.   
  38.                     //获取到OnCreate方法  
  39.                     CtMethod ctMethod = ctClass.getDeclaredMethod("onCreate")  
  40.   
  41.                     println("方法名 = " + ctMethod)  
  42.   
  43.                     String insetBeforeStr = """ android.widget.Toast.makeText(this,"我是被插入的Toast代码~!!",android.widget.Toast.LENGTH_SHORT).show();  
  44.                                                 """  
  45.                     //在方法开头插入代码  
  46.                     ctMethod.insertBefore(insetBeforeStr);  
  47.                     ctClass.writeFile(path)  
  48.                     ctClass.detach()//释放  
  49.                 }  
  50.             }  
  51.         }  
  52.   
  53.     }  
  54. }  


 

 

现在先clean一下项目,然后运行,代码就被插入了,看一下效果

这里写图片描述

到这里就完成了动态注入代码和动态生成Java类了,本文只是梳理原理和思路,至于高级的用法还请自行查阅资料

转载于:https://my.oschina.net/JiangTun/blog/1538924

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值