Android Gradle Plugin

192 阅读2分钟

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源码了。

image.png

如何写一个自定义的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 构建生命周期概述:

  1. 初始化阶段

    • Gradle 会识别并加载项目中的 settings.gradlesettings.gradle.kts 文件,并开始初始化所有项目(包括多项目构建中的子项目)。
  2. 配置阶段

    • 在此阶段,Gradle 会配置所有项目的构建脚本,包括 build.gradlebuild.gradle.kts 文件。它会评估和配置所有的插件、任务、依赖关系等。
    • 此时,buildSrc 目录会被首先扫描并编译,buildSrc 目录中的 build.gradlebuild.gradle.kts 会被加载。Gradle 会根据其中的插件、扩展等内容来配置构建。
  3. 执行阶段

    • 在此阶段,Gradle 会根据配置好的任务和依赖关系来执行构建。构建过程中的任务会按依赖关系执行。

详细执行顺序:

  1. 初始化阶段:Gradle 识别并加载 settings.gradle 文件,初始化所有项目。

  2. 构建脚本的评估(配置阶段) :Gradle 会评估每个项目的 build.gradle 文件,此时会加载 buildSrc 目录中的所有内容:

    • buildSrc 目录中的代码会被编译并加入到构建的类路径中。
    • 如果你在 buildSrc 中定义了插件或其他自定义构建逻辑,它们会被加载并应用到当前的构建中。
  3. 执行阶段:在配置阶段后,Gradle 会执行任务。 为了让我们写的插件更具有通用性,我们需要将插件的逻辑都写入到buildSrc目录下,让所有的模块都能使用。我们直接在项目中创建新的module, 并且命名为buildSrc.构建后的文件目录结构如下, 具体的详情说明会在后面进行详细的说明:

image.png

示例解释:

  1. apply plugin的id对用的是gradle-plugin下边的文件名:

image.png

  1. example也就是插件的别名,对应的是自定义的Plugin里边对插件的自定义名称。
example {
  name 'zhangjiangjun'
}

image.png

  1. 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操作的就是箭头所指向的任务:

image.png

  1. 注册自己的transform任务,这样我们的transform任务就能触发了。

image.png

最新的依赖管理方式

参考文献:juejin.cn/post/729243…