一、Gradle 自定义插件步骤
参考://www.greatytc.com/p/03eb55536298
在Gradle中自定义插件,有三种方式:
- 在 build.gradle 脚本中直接创建使用
- 在 buildSrc 模块中使用
- 在独立 Module 中使用
对比这三种方式,各自优缺点如下:
- 方式一比较快捷,但是可能不能为其它项目使用;
- 方式二创建了 buildSrc 模块后,Android Studio 会直接识别其为插件模块,在主工程 .gradle 文件中可以直接 apply 插件,而不用引入 maven 或 jcenter 等仓库才能使用插件;
- 方式三就需要使用引入 maven 或 jcenter 等仓库才能使用插件。
另外,在 IDEA 中也能开发 Gradle 插件,但是在 Android Studio 中更利于进行插件依赖和调试。所以建议直接在 Android Studio 中创建插件,若提供给其他项目使用,则创建 maven 、jcenter 仓库上传脚本上传到远程仓库后进行远程依赖就行。
以下所有的插件实现都是通过在 Android Studio 中创建 buildSrc 模块实现的。
通过 buildSrc 方式自定义插件过程中遇见的问题:
Q:定义了多个插件如何声明和使用?
A:gradle-plugins 为声明插件的目录,项目中创建了多个 Plugin.groovy 文件,可以在这里创建多个 youPluginName.properties 文件,内容为:
implementation-class=包名.插件类名
使用时,直接在 app.gradle 中进行依赖:
apply plugin: 'youPluginName'
如果是远程maven等仓库依赖,则需要添加仓库地址,并且需要在项目根目录添加插件版本 classPath 。
Q:buildSrc 插件模块,在定义插件时,如何使用第三方依赖?
A:同一般依赖引用,比如下面要使用 Transform + Javassist 进行操作字节码,则需要同时加入 gradle 和 javassist 远程依赖,在 buildSrc 模块下的 .gradle 文件配置如下:
apply plugin: 'groovy'
sourceCompatibility = 1.8
targetCompatibility = 1.8
dependencies {
repositories {
google()
mavenLocal()
jcenter()
}
implementation gradleApi() // gradle sdk
implementation localGroovy() // groovy sdk
// transform 时需要用到gradle tool的api,需要单独引入
implementation 'com.android.tools.build:gradle:3.1.3'
implementation 'com.android.tools.build:gradle-api:3.1.3'
implementation 'org.javassist:javassist:3.20.0-GA'
}
二、Javassist + Task 自动生成 .java 文件
按照上面的配置,先来试一下如何使用 javassit 在编译期自动生成 java 代码。
具体场景:在系统自动生成 BuildConfig.java 文件后(也就是系统内置任务 generateDebugBuildConfig 之后新建任务执行),自动生成我们自定义的 java 代码文件。
具体 .groovy 代码如下:
package com.coral.plugin
import com.android.build.gradle.AppPlugin
import org.gradle.api.Plugin
import org.gradle.api.Project
import com.android.build.gradle.AppExtension
/**
* desc: 利用 Javassist,在系统自动生成BuildConfig.java文件后,自动生成我们的java文件
*/
public class CreateJavaPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
System.out.println("----------------Begin----------------")
System.out.println("This is out custom plugin.")
def android = project.extensions.getByType(AppExtension)
// 注册一个Transform
def classTransform = new MyTransform(project)
android.registerTransform(classTransform)
// 创建一个 Extension
project.extensions.create("testCreateJavaConfig", CreateJavaExtension)
// 生产一个类
if (project.plugins.hasPlugin(AppPlugin)) {
// 获取到 Extension,也即是 .gradle 文件中的闭包
android.applicationVariants.all { variant ->
// 获取到 scope 作用域
def variantData = variant.variantData
def scope = variantData.scope
// 拿到 .gradle 中配置的 Extension 值
def config = project.extensions.getByName("testCreateJavaConfig")
// 创建一个 Task(名称为:coralDebugCreateJavaPlugin 或 coralReleaseCreateJavaPlugin)
def createTaskName = scope.getTaskName("coral", "CreateJavaPlugin")
def createTask = project.task(createTaskName)
// 设置 task 要执行的任务
createTask.doLast {
// 生成 java 类
createJavaTest(variant, config)
}
// 设置 task 依赖于生成 BuildConfig 的 task,然后在生成 BuildConfig 后生成我们的类
String generateBuildConfigTaskName = variant.getVariantData()
.getScope().getGenerateBuildConfigTask().name
// 任务名称:generateDebugBuildConfig
println("generateBuildConfigTaskName = " + generateBuildConfigTaskName)
def generateBuildConfigTask = project.tasks.getByName(generateBuildConfigTaskName)
if (generateBuildConfigTask) {
createTask.dependsOn generateBuildConfigTask
generateBuildConfigTask.finalizedBy createTask
}
}
}
System.out.println("----------------Has it finished?----------------")
}
static void createJavaTest(variant, config) {
println("---begin create: " + variant + ", " + config.str)
// 要生成的内容
def content = """package com.coral.demo;
/**
* Created by xss on 2018/11/20.
*/
public class TestClass {
public static final String str = "${config.str}";
}
"""
// 获取到 BuildConfig 类的路径
File outputDir = variant.getVariantData().getScope().getBuildConfigSourceOutputDir()
// app/build/generated/source/buildConfig/debug
println("outputDir = " + outputDir.absolutePath)
def javaFile = new File(outputDir, "TestClass.java")
javaFile.write(content, 'UTF-8')
println("---create finished---")
}
}
public class CreateJavaExtension {
def str = "动态生成Java类的字符串"
}
在 app.gradle 文件中配置如下:
// 自动生成 Java 类插件
apply plugin: 'myPluginCreateJava'
testCreateJavaConfig {
str = '动态生成Java类'
}
同步gradle 后,在Studio右侧 app -> Tasks -> other 可以看到自定义的任务:
双击执行任务编译成功后,在 app/build/generated/source/buildConfig/debug 目录下可以看到自动生成的 java 文件,在项目中可以进行直接引用该类。
参考:http://www.10tiao.com/html/227/201709/2650241354/1.html
三、Transform + Javassist 编译期注入代码到 .class文件
使用 Transform + Javassit 操作字节码,需要在 .gradle 中添加 Transform 和 Javassist 的 API ,配置按上面的 .gradle 配置就行。
具体场景:在项目的 MainActivity 的 onCreate() 方法内部插入一行代码。
MyTransform.groovy 文件代码如下:
package com.coral.plugin
import com.android.build.api.transform.Context
import com.android.build.api.transform.DirectoryInput
import com.android.build.api.transform.Format
import com.android.build.api.transform.JarInput
import com.android.build.api.transform.Transform
import com.android.build.api.transform.QualifiedContent
import com.android.build.api.transform.TransformException
import com.android.build.api.transform.TransformInput
import com.android.build.api.transform.TransformOutputProvider
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
public class MyTransform extends Transform {
Project project
/**
* 构造方法,保留原project备用
*/
MyTransform(Project project) {
this.project = project
}
/**
* 设置自定义 Transform 对应的 Task 名称
* 类似:TransformClassesWithPreDexForXXX,对应的 task 名称为:transformClassesWithMyTransformForDebug
* 会生成目录 build/intermediates/transforms/MyTransform/
*/
@Override
String getName() {
return "MyTransform"
}
/**
* 指定输入的类型,可指定我们要处理的文件类型(保证其他类型文件不会传入)
* CLASSES - 表示处理java的class文件
* RESOURCES - 表示处理java的资源
*/
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
/**
* 指定 Transform 的作用范围
*/
@Override
Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
/**
* 是否支持增量编译
*/
@Override
boolean isIncremental() {
return false
}
/**
* 核心方法,具体如何处理输入和输出
* @param inputs 为传过来的输入流,两种格式,一种jar包格式,一种目录格式
* @param outputProvider 获取到输出目录,最后将修改的文件复制到输出目录,这一步必须执行,不让编译会报错
*/
@Override
void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs,
TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
System.out.println("----------开始Transform-----------")
// Transform 的 inputs 分为两种类型,一直是目录,一种是 jar 包。需要分开遍历
inputs.each { TransformInput input ->
// 1) 对类型为"目录"的 input 进行遍历
input.directoryInputs.each { DirectoryInput dirInput ->
// demo1. 在MainActivity的onCreate()方法之前注入代码
MyInject.injectOnCreate(dirInput.file.absolutePath, project)
// 获取 output 目录
def dest = outputProvider.getContentLocation(dirInput.name, dirInput.contentTypes,
dirInput.scopes, Format.DIRECTORY)
// 将 input 的目录复制到 output 指定目录
FileUtils.copyDirectory(dirInput.file, dest)
}
// 2) 对类型为 jar 文件的 input 进行遍历
input.jarInputs.each { JarInput jarInput ->
// jar 文件一般是第三方依赖库jar包
// 重命名输出文件(同目录 copyFile 会冲突)
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)
}
}
System.out.println("----------结束Transform-----------")
}
}
MyInject.groovy 文件操作字节码代码如下:
package com.coral.plugin
import javassist.ClassPool
import javassist.CtClass
import javassist.CtConstructor
import javassist.CtMethod
import org.gradle.api.Project
public class MyInject {
private static ClassPool classPool = ClassPool.getDefault()
public static void injectOnCreate(String path, Project project) {
classPool.appendClassPath(path)
classPool.appendClassPath(project.android.bootClasspath[0].toString())
classPool.importPackage("android.os.Bundle")
File dir = new File(path)
if (dir.isDirectory()) {
dir.eachFileRecurse { File file ->
String filePath = file.absolutePath
if (file.getName().equals("MainActivity.class")) {
// 获取 MainActivity
CtClass ctClass = classPool.getCtClass("com.coral.demo.MainActivity")
println("ctClass = " + ctClass)
// 解冻
if (ctClass.isFrozen()) {
ctClass.defrost()
}
// 获取到 onCreate() 方法
CtMethod ctMethod = ctClass.getDeclaredMethod("onCreate")
println("ctMethod = " + ctMethod)
// 插入日志打印代码
String insertBeforeStr = """android.util.Log.e("--->", "Hello");"""
ctMethod.insertBefore(insertBeforeStr)
ctClass.writeFile(path)
ctClass.detach()
}
}
}
}
}
如何使自定义的 Transform 有作用?需要定义插件进行注册,MyTransformPlugin.groovy 代码如下:
import com.android.build.gradle.AppPlugin
import org.gradle.api.Plugin
import org.gradle.api.Project
import com.android.build.gradle.AppExtension
public class MyTransformPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
def android = project.extensions.getByType(AppExtension)
// 注册Transform
def classTransform = new MyTransform(project)
android.registerTransform(classTransform)
}
}
在 app.gradle 使用时也需要 apply plugin ,依赖脚本同上。
说明:
自定义的 Transform 在编译的时候并不会被触发执行,在安装 apk 时会触发执行;
-
自定义的 Transform 会自动生成几种不同 gradle task,任务名称规则为:transformClassWith$${getName}For${variant}
双击上述自定义的 transform 任务会去执行 Transform 中的 transform() 方法,进行字节码操作代码。这一步可以看到我们再 groovy 中的打印日志,很方便调试。
在自定义的 MyTransform 中,使用 transform() 方法处理字节码,除了调用 MyInject 类的方法处理不同,其他的处理步骤都是统一的。
transform() 处理步骤大致可以分为:1)对类型为目录的 input 遍历;2)调用 javassist api 处理字节码;3)生成输出路径,将操作后的 input 目录复制到 output 指定目录;4)对类型为 jar 的 input 遍历;5)重命名输出文件(防止复制文件冲突);5)生成输出路径 & 将输入内容复制到输出。