一、gradle Transform 接收一个输入input,同时需要有一组输出,作为下一个Transform的输入。
(1)最简单的一个Transform实现,需要实现
将输入数据input,原样不动输出到output
(2)Transform处理的结果,会位于工程目录/build/intermediates/transform文件夹下。
如下图XXX目录即为自定义的一个Transfrom。
由图可知除XXX外,还经过了dexBuilder、dexMerger、mergeJavaRes、mergeJniLibs、StripDebugSymbol等多个Transform处理
![](https://img.haomeiwen.com/i2718191/0a52c25d09c1e21f.png)
二、自定义gradle插件实例
1、自定义gradle插件的build.gradle
apply plugin: 'groovy'
apply plugin: 'maven'
apply plugin: 'java'
apply plugin: 'maven-publish'
dependencies {
// implementation fileTree(dir: 'libs', include: ['*.jar'])
compile gradleApi()//gradle sdk
compile localGroovy()//groovy sdk
//build tools
compile 'com.android.tools.build:gradle:3.1.2'
//transform
compile 'com.android.tools.build:transform-api:1.5.0'
//javassit
compile 'javassist:javassist:3.12.1.GA'
//commons-io
compile 'commons-io:commons-io:2.5'
}
repositories {
jcenter()
google()//加在这里
}
SecondPlugin.groovy 自定义插件,内部为android注册了一个ReClassTransform 接口。
package com.feifei.second
import com.android.build.gradle.AppExtension
import com.android.build.gradle.AppPlugin
import com.feifei.second.transform.ReClassTransform
import org.gradle.api.Plugin
import org.gradle.api.Project
public class SecondPlugin implements Plugin<Project>{
void apply(Project project){
System.out.println("==========")
System.out.println("feifei 第二个内部用插件")
System.out.println("==========")
project.extensions.create("pluginExt",PluginExtension)
project.pluginExt.extensions.create("nestExt", PluginNestExtension)
project.task('customTask',type:CustomTask)
def isApp = project.plugins.getPlugin(AppPlugin)
if(isApp){
def android = project.extensions.getByType(AppExtension)
android.registerTransform(new ReClassTransform(project))
}
}
}
最原始的Transform实现。
ReClassTransfrom.groovy
package com.feifei.second.transform
import com.android.build.api.transform.*
import com.android.build.api.transform.Context
import com.android.build.api.transform.DirectoryInput
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.TransformInput
import com.android.build.api.transform.TransformOutputProvider
import com.android.utils.FileUtils
import org.apache.commons.codec.digest.DigestUtils
import org.gradle.api.Project
import org.gradle.internal.impldep.org.apache.ivy.util.FileUtil
import org.gradle.jvm.tasks.Jar
import com.android.build.gradle.internal.pipeline.TransformManager
import javax.xml.crypto.dsig.TransformException
public class ReClassTransform extends Transform{
private Project mProject;
public ReClassTransform(Project p){
this.mProject = p;
}
//transform的名称
/**
* 最终运行的名字为 transformClassWith+getName()+For+{BuildType}+{ProductFlavor}
* 如 transformClassWithXXXForDebug
* @return
*/
@Override
String getName() {
return "XXX"
}
/**
* 需要处理的数据类型,有两种枚举类型
* CLASSES和RESOURCES,CLASSES代表处理的java的class文件;RESOURCES代表要处理java的资源.
* @return
*/
@Override
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 由当前变量(包括依赖项)测试的代码
* @return
*/
@Override
Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT;
}
//指明当前Transform是否支持增量编译
@Override
boolean isIncremental() {
return false
}
/**
* Transform中的核心方法,
*
* @param context 。
* @param inputs 传过来的输入流, 其中有两种格式,一种是jar包格式一种是目录格式
* @param referencedInputs
* @param outputProvider 获取到输出目录,最后将修改的文件复制到输出目录,这一步必须做不然编译会报错
* @param isInCremental
* @throws IOException
* @throws TransformException
*/
@Override
public void transform(Context context,
Collection<TransformInput> inputs,
Collection<TransformInput> referencedInputs,
TransformOutputProvider outputProvider,
boolean isInCremental
) throws IOException, TransformException{
welecome()
inputs.each { TransformInput input->
//遍历目录
input.directoryInputs.each { DirectoryInput directoryInput ->
println "direction = "+directoryInput.file.getAbsolutePath()
//获取输出目录
def dest = outputProvider.getContentLocation(directoryInput.name,directoryInput.contentTypes,directoryInput.scopes,Format.DIRECTORY)
//对于目录中的class文件原样输出
FileUtils.copyDirectory(directoryInput.file,dest)
}
//遍历jar文件,对jar不操作,但是要输出到out目录
input.jarInputs.each { JarInput jarInput->
// 将jar文件 重命名输出文件(同目录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)
}
}
end()
}
def welecome(){
println "----welcome to ReClassTransform"
}
def end(){
println "----ReClassTransform end"
}
}
执行./gradlew :test_gradle_use_plugin:assembleDebug
时的输出内容。
> Task :test_gradle_use_plugin:transformClassesWithXXXForDebug
----welcome to ReClassTransform
jar = /Users/feifei/.gradle/caches/transforms-1/files-1.1/constraint-layout-1.1.0.aar/ad39ea76672d18218cf29f42ea94a4d7/jars/classes.jar
jar = /Users/feifei/.gradle/caches/modules-2/files-2.1/com.android.support.constraint/constraint-layout-solver/1.1.0/931532e953a477f876f2de18c2e7f16eee01078f/constraint-layout-solver-1.1.0.jar
direction = /Users/feifei/Desktop/TM/Github/MyExampleCode/test_gradle_use_plugin/build/intermediates/classes/debug
direction = /Users/feifei/Desktop/TM/Github/MyExampleCode/test_gradle_use_plugin/build/tmp/kotlin-classes/debug
----ReClassTransform end
2、利用向文件中写入字符串的形式直接生成类文件
Hostconfig.groovy
增加HostConfig的调用
package com.feifei.second.hostconfig
public class HostConfig {
static def void createHostConfig(variant,config){
def content = """
package com.sogou.teemo.test_use_gradle_plugin;
public class TheHostConfig{
public static final String ip = "${config.param1}";
public static final String port = "5050";
}
"""
File outputDir = variant.getVariantData().getScope().getBuildConfigSourceOutputDir()
println "feifei createHostConfig outputDir:"+outputDir.getAbsolutePath()
def javaFile = new File(outputDir, "TheHostConfig.java")
javaFile.write(content,'UTF-8')
}
}
SecondPlugin.groovy
package com.feifei.second
import com.android.build.gradle.AppExtension
import com.android.build.gradle.AppPlugin
import com.android.build.gradle.api.ApplicationVariant
import com.android.repository.impl.meta.Archive
import com.feifei.second.hostconfig.HostConfig
import com.feifei.second.transform.ReClassTransform
import org.gradle.api.Plugin
import org.gradle.api.Project
public class SecondPlugin implements Plugin<Project>{
void apply(Project project){
System.out.println("==========")
System.out.println("feifei 第二个内部用插件")
System.out.println("==========")
project.extensions.create("pluginExt",PluginExtension)
project.pluginExt.extensions.create("nestExt", PluginNestExtension)
project.task('customTask',type:CustomTask)
def isApp = project.plugins.getPlugin(AppPlugin)
if(isApp){
def android = project.extensions.getByType(AppExtension)
android.registerTransform(new ReClassTransform(project))
android.applicationVariants.all { variants->
def variantData = variants.variantData
def scope = variantData.scope
println "feifei current scope:"+scope
//scope.getTaskName 的作用 就是结合当前scope 拼接人物名
def taskName = scope.getTaskName("CreateHostConfig")
def createTask = project.task(taskName)
println "feifei CreateHostConfigTaskName:"+taskName
//自定义task 增加action
createTask.doLast {
HostConfig.createHostConfig(variants,project.pluginExt)
}
String generateBuildConfigTaskName = scope.getGenerateBuildConfigTask().name
def generateBuildConfigTask = project.tasks.getByName(generateBuildConfigTaskName)
println "feifei generateBuildConfigTaskName:"+generateBuildConfigTaskName
if(generateBuildConfigTask){
createTask.dependsOn generateBuildConfigTask
generateBuildConfigTask.finalizedBy(createTask)//执行完generateBuildConfigTask之后,执行createTask任务
}
}
}
}
}
执行 ./gradlew clean :test_gradle_use_plugin:assembleDebug
输出如下:
Configure project :test_gradle_use_plugin
==========
feifei 第二个内部用插件
==========
feifei current scope:VariantScopeImpl{debug}
feifei CreateHostConfigTaskName:CreateHostConfigDebug
feifei generateBuildConfigTaskName:generateDebugBuildConfig
feifei current scope:VariantScopeImpl{release}
feifei CreateHostConfigTaskName:CreateHostConfigRelease
feifei generateBuildConfigTaskName:generateReleaseBuildConfig
> Task :test_gradle_use_plugin:transformClassesWithXXXForDebug
----welcome to ReClassTransform
jar = /Users/feifei/.gradle/caches/transforms-1/files-1.1/constraint-layout-1.1.0.aar/5ae74cdeff58ee396218df991052866b/jars/classes.jar
jar = /Users/feifei/.gradle/caches/modules-2/files-2.1/com.android.support.constraint/constraint-layout-solver/1.1.0/931532e953a477f876f2de18c2e7f16eee01078f/constraint-layout-solver-1.1.0.jar
direction = /Users/feifei/Desktop/TM/Github/MyExampleCode/test_gradle_use_plugin/build/intermediates/classes/debug
direction = /Users/feifei/Desktop/TM/Github/MyExampleCode/test_gradle_use_plugin/build/tmp/kotlin-classes/debug
----ReClassTransform end
生成类文件的位置:
![](https://img.haomeiwen.com/i2718191/ee941824fdf7733f.png)
3、利用javassist 向现有类中动态插入代码
Javassist是一个动态类库,可以用来检查、”动态”修改以及创建 Java类。其功能与jdk自带的反射功能类似,但比反射功能更强大
ClassPool:javassist的类池,使用ClassPool 类可以跟踪和控制所操作的类,它的工作方式与 JVM 类装载器非常相似,
CtClass: CtClass提供了检查类数据(如字段和方法)以及在类中添加新字段、方法和构造函数、以及改变类、父类和接口的方法。不过,Javassist 并未提供删除类中字段、方法或者构造函数的任何方法。
CtField:用来访问域
CtMethod :用来访问方法
CtConstructor:用来访问构造器
新建
CodeInjects.groovy 用于想MainActivity中动态插入代码
package com.feifei.second.codeinject
import javassist.ClassPool
import javassist.CtClass
import javassist.CtMethod
import org.gradle.api.Project
public class CodeInjects {
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())
pool.importPackage("android.os.Bundle");
pool.importPackage(" android.app.Activity")
File dir = new File(path)
if(dir.isDirectory()){
//遍历目录
dir.eachFileRecurse {File file->
String filePath = file.absolutePath
println("CodeInjects filePath:"+filePath)
if(file.getName().equals("MainActivity.class")){
//获取MainActivity.class
CtClass ctClass = pool.getCtClass("com.sogou.teemo.test_use_gradle_plugin.MainActivity");
println("CodeInjects ctClass = "+ctClass)
if(ctClass.isFrozen()){
ctClass.defrost()
}
//获取到onCreate方法
CtMethod ctMethod = ctClass.getDeclaredMethod("onCreate");
println("CodeInjects 方法名 = " + ctMethod)
String insetBeforeStr = """ android.widget.Toast.makeText(this,"插件中自动生成的代码",android.widget.Toast.LENGTH_SHORT).show();
"""
ctMethod.insertAfter(insetBeforeStr)
ctClass.writeFile(path)
ctClass.detach()//释放
}
}
}
}
}
ReClassTransform.groovy中,遍历class文件时,调用CodeInjects.inject(directoryInput.file.absolutePath,mProject)。过滤出MainActivity.class并动态修改onCreate()方法
/**
* Transform中的核心方法,
*
* @param context 。
* @param inputs 传过来的输入流, 其中有两种格式,一种是jar包格式一种是目录格式
* @param referencedInputs
* @param outputProvider 获取到输出目录,最后将修改的文件复制到输出目录,这一步必须做不然编译会报错
* @param isInCremental
* @throws IOException
* @throws TransformException
*/
@Override
public void transform(Context context,
Collection<TransformInput> inputs,
Collection<TransformInput> referencedInputs,
TransformOutputProvider outputProvider,
boolean isInCremental
) throws IOException, TransformException{
welecome()
inputs.each { TransformInput input->
//遍历目录
input.directoryInputs.each { DirectoryInput directoryInput ->
println "direction = "+directoryInput.file.getAbsolutePath()
CodeInjects.inject(directoryInput.file.absolutePath,mProject)
//获取输出目录
def dest = outputProvider.getContentLocation(directoryInput.name,directoryInput.contentTypes,directoryInput.scopes,Format.DIRECTORY)
//对于目录中的class文件原样输出
FileUtils.copyDirectory(directoryInput.file,dest)
}
//遍历jar文件,对jar不操作,但是要输出到out目录
input.jarInputs.each { JarInput jarInput->
// 将jar文件 重命名输出文件(同目录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)
}
}
end()
}
将test_gradle_use_plugin-debug.apk 反编译后,如下图所示:
![](https://img.haomeiwen.com/i2718191/2425b1d7903e6faf.png)
Github: 查看buildSrc 和test_gradle_use_plugin 两个module
四、相关知识背景
1、Transfrom API
基于Gradle的Transform API,在编译期的构建任务流中,class转为dex之前,插入一个Transform,并在此Transform流中,基于Javassist实现对字节码文件的注入。
[图片上传失败...(image-317838-1563938953106)]
http://google.github.io/android-gradle-dsl/javadoc/current/
2、javassist
Javassist是一个动态类库,可以用来检查、”动态”修改以及创建 Java类.其功能与jdk自带的反射功能类似,但比反射功能更强大.
- ClassPool:javassist的类池,使用ClassPool 类可以跟踪和控制所操作的类,它的工作方式与 JVM 类装载器非常相似。
- CtClass: CtClass提供了检查类数据(如字段和方法)以及在类中添加新字段、方法和构造函数、以及改变类、父类和接口的方法。不过,Javassist 并未提供删除类中字段、方法或者构造函数的任何方法。
- CtField:用来访问域
- CtMethod :用来访问方法
- CtConstructor:用来访问构造器
- insertClassPath:为ClassPool添加搜索路径,否则ClassPool 无法找打对应的类
classPool.insertClassPath(new ClassClassPath(String.class));
classPool.insertClassPath(new ClassClassPath(Person.class));
classPool.insertClassPath("/Users/feifei/Desktop/1");
-
classPool.get(className);加载一个类
-
classPool.makeClass(className);//创建一个类
-
CtClass.addField();CtClass.addMethod(); 添加方法和属性
CtField ageField = new CtField(CtClass.intType,"age",stuClass);
stuClass.addField(ageField);
CtMethod setMethod = CtMethod.make("public void setAge(int age) { this.age = age;}",stuClass);
stuClass.addMethod(getMethod);
- Class<?>clazz = stuClass.toClass();将CtCLass对象转化为JVM对象
创建一个类,并写入到本地文件
public static void testCreateClass(){
System.out.println("testCreateClass");
//创建ClassPool
ClassPool classPool = ClassPool.getDefault();
//添加类路径
// classPool.insertClassPath(new ClassClassPath(this.getClass()));
classPool.insertClassPath(new ClassClassPath(String.class));
//创建类
CtClass stuClass = classPool.makeClass("com.feifei.Student");
//加载类
//classPool.get(className)
try {
//添加属性
CtField idField = new CtField(CtClass.longType,"id",stuClass);
stuClass.addField(idField);
CtField nameField = new CtField(classPool.get("java.lang.String"),"name",stuClass);
stuClass.addField(nameField);
CtField ageField = new CtField(CtClass.intType,"age",stuClass);
stuClass.addField(ageField);
//添加方法
CtMethod getMethod = CtMethod.make("public int getAge(){return this.age;}",stuClass);
CtMethod setMethod = CtMethod.make("public void setAge(int age) { this.age = age;}",stuClass);
stuClass.addMethod(getMethod);
stuClass.addMethod(setMethod);
//toClass 将CtClass 转换为java.lang.class
Class<?>clazz = stuClass.toClass();
System.out.println("testCreateClass clazz:"+clazz);
System.out.println("testCreateClas ------ 属性列表 -----");
Field[] fields = clazz.getDeclaredFields();
for(Field field:fields){
System.out.println("testCreateClass"+field.getType()+"\t"+field.getName());
}
System.out.println("testCreateClass ------ 方法列表 -----");
Method[] methods = clazz.getDeclaredMethods();
for(Method method:methods){
System.out.println("feifei "+method.getReturnType()+"\t"+method.getName()+"\t"+ Arrays.toString(method.getParameterTypes()));
}
stuClass.writeFile("/Users/feifei/Desktop/1");
} catch (CannotCompileException e) {
e.printStackTrace();
} catch (NotFoundException e) {
e.printStackTrace();
}catch (Exception e){
e.printStackTrace();
}finally {
//将stuClass 从ClassPool 移除
if(stuClass != null){
stuClass.detach();
}
}
}
修改一个类的父类
package com.example.myjavassist;
public class Person {
}
public static void testSetSuperClass(){
System.out.println("testSetSuperClass");
//创建ClassPool
ClassPool classPool = ClassPool.getDefault();
try {
//添加类路径
classPool.insertClassPath(new ClassClassPath(String.class));
classPool.insertClassPath(new ClassClassPath(Person.class));
classPool.insertClassPath("/Users/feifei/Desktop/1");
// 加载类
//创建类
CtClass stuClass = classPool.get("com.feifei.Student");
CtClass personClass = classPool.get("com.example.myjavassist.Person");
if(stuClass.isFrozen()){
stuClass.freeze();
}
stuClass.setSuperclass(personClass);
//toClass 将CtClass 转换为java.lang.class
Class<?>clazz = stuClass.toClass();
System.out.println("testSetSuperClass ------ 属性列表 -----");
Field[] fields = clazz.getDeclaredFields();
for(Field field:fields){
System.out.println("testCreateClass"+field.getType()+"\t"+field.getName());
}
System.out.println("testSetSuperClass ------ 方法列表 -----");
Method[] methods = clazz.getDeclaredMethods();
for(Method method:methods){
System.out.println("testSetSuperClass "+method.getReturnType()+"\t"+method.getName()+"\t"+ Arrays.toString(method.getParameterTypes()));
}
stuClass.writeFile("/Users/feifei/Desktop/1");
personClass.writeFile("/Users/feifei/Desktop/1");
} catch (NotFoundException | CannotCompileException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
}
}
![](https://img.haomeiwen.com/i2718191/6bf5becb558dfcc8.png)
方法重命名、复制方法、新建方法,添加方法体。
package com.example.myjavassist;
public class Calculator {
public void getSum(long n) {
long sum = 0;
for (int i = 0; i < n; i++) {
sum += i;
}
System.out.println("n="+n+",sum="+sum);
}
}
public static void testInsertMethod(){
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = null;
try {
ctClass = pool.get("com.example.myjavassist.Calculator");
//获取类中现有的方法
String getSumName = "getSum";
CtMethod methodOld = ctClass.getDeclaredMethod(getSumName);
String methodNewName = getSumName+"$impl";
//修改原有方法的方法名
methodOld.setName(methodNewName);
//创建一个新的方法getSumName,并将旧方法 复制成新方法中.
CtMethod newMethod = CtNewMethod.copy(methodOld,getSumName,ctClass,null);
//设置新newMethod的方法体
StringBuffer body = new StringBuffer();
body.append("{\nlong start = System.currentTimeMillis();\n");
// 调用原有代码,类似于method();($$)表示所有的参数
body.append(methodNewName + "($$);\n");
body.append("System.out.println(\"Call to method " + methodNewName
+ " took \" +\n (System.currentTimeMillis()-start) + " + "\" ms.\");\n");
body.append("}");
newMethod.setBody(body.toString());
//为类新添加方法
ctClass.addMethod(newMethod);
Calculator calculator =(Calculator)ctClass.toClass().newInstance();
calculator.getSum(10000);
//将类输出到文件
ctClass.writeFile("/Users/feifei/Desktop/1");
} catch (NotFoundException e) {
e.printStackTrace();
} catch (CannotCompileException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}catch (Exception e){
e.printStackTrace();
}
finally {
if(ctClass!=null){
ctClass.detach();
}
}
}
![](https://img.haomeiwen.com/i2718191/f3db2d001bd87805.png)
Github: 选择 myjavaassit module
五、参考文章
https://www.jianshu.com/p/a6be7cdcfc65
https://www.jianshu.com/p/a9b3aaba8e45
https://blog.csdn.net/top_code/article/details/51708043
http://www.javassist.org/tutorial/tutorial2.html
javassit github:
https://github.com/jboss-javassist/javassist
网友评论