Gradle Plugin 是什么?
Gradle Plugin 是用于扩展 Gradle 构建工具功能的插件。它可以添加新的任务、配置或改变现有任务的行为。通常,Gradle Plugin 是用 Java、Groovy 或 Kotlin 编写的。不过,没有必要为了写gradle而专门学习groovy,使用kotlin和java编写已经完全足够了。
一个典型的Gradle Plugin示例
apply plugin: 'com.android.application' // 这就是一个gradle plugin
apply plugin: 'kotlin-android'// 这就是一个gradle plugin
apply plugin: 'kotlin-android-extensions'// 这就是一个gradle plugin
android { // 这个是'com.android.application'的拓展
compileSdkVersion 30
buildToolsVersion "30.0.2"
defaultConfig {
applicationId "com.hencoder.gradleplugin"
minSdkVersion 21
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
如何查看原生gradle文件及其内容?
我们使用的默认gradle都在 dependencies
里边的classpath 'com.android.tools.build:gradle:7.1.2'
,可以理解为repositories是一个仓库,而classpath就是仓库里边存放的具体的一种东西。
buildscript {
ext.kotlin_version = '1.4.10'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.1.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
那么我们如何去查看classpath 'com.android.tools.build:gradle:7.1.2'
的具体代码是怎么写的呢?
为了不让编译报错,可以在模块的build.gradle里边添加如下的代码:
dependencies {
// 省略其他依赖
compileOnly('com.android.tools.build:gradle:7.1.2') // 添加这一行
}
然后同步工程之后我们在External Libraries
下边就能看到android写的gradle源码了。
如何写一个自定义的Plugin?
直接在build.gradle内部添加
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
class MyPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
println "Hello, ${project.name}"
}
}
apply plugin: MyPlugin
我们直接在内部定义了一个MyPlugin类,然后通过apply plugin: MyPlugin
应用了我们的插件,MyPlugin后边不需要添加.class, 在groovy中这个可以省略。
另外,我们上边提到了插件的拓展,我们也可以自定义添加拓展,新定义一个MyExtension
,是针对MyPlugin
的拓展,调用myExtension
即可执行插件的拓展代码。
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
class MyPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
def extension = target.extensions.create('myExtension', MyExtension)
println "Hello, ${extension.name}"
}
}
class MyExtension {
def name = "myExtension"
}
apply plugin: MyPlugin
myExtension { // 这里这样执行插件拓展代码,会导致name的修改不会生效,因为MyExtension在创建的时候,就会吧apply内部的逻辑也执行了,
name 'zhangjiangjun'
}
上边的例子是有问题的,myExtension内部对变量的修改无法执行,因为MyExtension在创建的时候,就会把apply内部的逻辑也执行了。要想正确的执行,需要做以下修改:
class MyPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
def extension = target.extensions.create('myExtension', MyExtension)
target.afterEvaluate {
println "Hello, ${extension.name}"
}
}
}
class MyExtension {
def name = "myExtension"
}
apply plugin: MyPlugin
myExtension {
name 'zhangjiangjun' // 这个其实是一个方法调用,不省略的写法是这样的:name('zhangjiangjun'),最终会调用到setName('zhangjiangjun')
}
afterEvaluate 会在整个工程配置完成后进行执行,此时再打印就能得到正确的值了。
为什么我们需要使用gradle插件?
写到这里,各位可能有一个疑惑,为什么我们需要使用插件?上边的写法,我们用下几行代码也能实现:
def extension = new MyExtension()
extension.name = 'zhangjiangjun'
println "Hello, ${extension.name}"
我们在一个gradle文件内部直接定义插件这种方式一般是不推荐的, gradle插件的优势在于复用,上边例子中的写法完全和这个脱离了。
插件的代码需要写在buildSrc目录下
这个目录中的文件,安卓在构建的时候会去自动读取,且需要注意,不能在settings中引用这个模块,否则会报错,这个里边的文件会在settings.gradle执行完成后,在各个模块的build.gradle执行前执行。具体来说是在 初始化阶段 和 配置阶段 之间。
Gradle 构建生命周期概述:
-
初始化阶段:
- Gradle 会识别并加载项目中的
settings.gradle
或settings.gradle.kts
文件,并开始初始化所有项目(包括多项目构建中的子项目)。
- Gradle 会识别并加载项目中的
-
配置阶段:
- 在此阶段,Gradle 会配置所有项目的构建脚本,包括
build.gradle
或build.gradle.kts
文件。它会评估和配置所有的插件、任务、依赖关系等。 - 此时,
buildSrc
目录会被首先扫描并编译,buildSrc
目录中的build.gradle
或build.gradle.kts
会被加载。Gradle 会根据其中的插件、扩展等内容来配置构建。
- 在此阶段,Gradle 会配置所有项目的构建脚本,包括
-
执行阶段:
- 在此阶段,Gradle 会根据配置好的任务和依赖关系来执行构建。构建过程中的任务会按依赖关系执行。
详细执行顺序:
-
初始化阶段:Gradle 识别并加载
settings.gradle
文件,初始化所有项目。 -
构建脚本的评估(配置阶段) :Gradle 会评估每个项目的
build.gradle
文件,此时会加载buildSrc
目录中的所有内容:buildSrc
目录中的代码会被编译并加入到构建的类路径中。- 如果你在
buildSrc
中定义了插件或其他自定义构建逻辑,它们会被加载并应用到当前的构建中。
-
执行阶段:在配置阶段后,Gradle 会执行任务。 为了让我们写的插件更具有通用性,我们需要将插件的逻辑都写入到buildSrc目录下,让所有的模块都能使用。我们直接在项目中创建新的module, 并且命名为
buildSrc
.构建后的文件目录结构如下, 具体的详情说明会在后面进行详细的说明:
示例解释:
apply plugin
的id对用的是gradle-plugin
下边的文件名:
example
也就是插件的别名,对应的是自定义的Plugin里边对插件的自定义名称。
example {
name 'zhangjiangjun'
}
- Transform 内部主要是对class等文件的自定义操作,我们这个示例中,只是将文件做了搬运,请注意,如果我们自定义了Transform,而没有复写
transform
方法,将会导致所有的class文件都不会被加载进去,导致apk无法使用,如果需要对字节码进行插桩等操作,也可以在这里边使用,使用的工具可以是javassit或者是asm等。
class MyTransform extends Transform {
/**
* 返回此 Transform 的名称。
* 这是 Gradle 用于区分不同 Transform 的标识符。
*/
@Override
String getName() {
return 'MyTransform'
}
/**
* 指定此 Transform 处理的内容类型(例如:class 文件、资源文件等)。
* 这里返回 TransformManager.CONTENT_CLASS,表示此 Transform 处理的是编译后的 class 文件。
*/
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
/**
* 指定此 Transform 应该操作的范围。
* SCOPE_FULL_PROJECT 表示此 Transform 将作用于项目中的所有模块(包括应用模块、库模块、依赖模块等)。
*/
@Override
Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
/**
* 指示此 Transform 是否支持增量构建。
* 如果为 true,Gradle 只会传递更改或更新过的文件给 Transform。
* 这里设置为 false,意味着每次都会处理整个内容。
*/
@Override
boolean isIncremental() {
return false
}
/**
* 主要的转换入口点,Transform 逻辑在此实现。
* 它接收一个 TransformInvocation 对象,其中包含:
* - 输入内容(包括 jar 文件和目录)
* - 输出提供者(outputProvider),它告诉我们将转换后的文件存放在哪里。
*
* 在此方法中,对于每个输入(jar 文件和目录),示例代码只是简单地将它们从输入位置复制到输出位置。
* 在实际的 Transform 中,我们可以在这里修改字节码或执行其他操作,然后再将文件写入输出。
*/
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
// 获取所有输入内容
def inputs = transformInvocation.inputs
// 获取输出提供者,用于计算输出文件的存放位置
def outputProvider = transformInvocation.outputProvider
// 遍历每个输入
inputs.each { transformInput ->
// 遍历每个 jar 输入
transformInput.jarInputs.each { jarInput ->
// 计算此 jar 文件在构建中间目录中的目标位置
File dest = outputProvider.getContentLocation(
jarInput.name,
jarInput.contentTypes,
jarInput.scopes,
Format.JAR
)
println "Jar: ${jarInput.file}, Dest: ${dest}"
// 将原始的 jar 文件复制到目标位置
FileUtils.copyFile(jarInput.file, dest)
}
// 遍历每个目录输入(通常是编译后的 class 文件)
transformInput.directoryInputs.each { directoryInput ->
// 计算此目录文件在构建中间目录中的目标位置
File dest = outputProvider.getContentLocation(
directoryInput.name,
directoryInput.contentTypes,
directoryInput.scopes,
Format.DIRECTORY
)
println "Dir: ${directoryInput.file}, Dest: ${dest}"
// 将原始的目录文件复制到目标位置
FileUtils.copyDirectory(directoryInput.file, dest)
}
}
}
}
transform操作的就是箭头所指向的任务:
- 注册自己的transform任务,这样我们的transform任务就能触发了。