Android Gradle插件技术简述

548 阅读3分钟

前言

通过阅读,帮大家梳理Gradle插件的脉络,知道Gradle插件是什么,能帮我们做什么。

但凡对大家有一些用处,都是好的。

一、什么是Gradle

一种自动化构建工具,构建脚本使用Groovy/Koltin的DSL编写,而不是传统的XML

1.自动化构建

构建,即Build。

一说Build,Android同学就很熟悉了,Android打包流程就是一个Build过程。

下面是一段Android工程Gradle配置。

Sample
-- app
   -- src
   -- build.gradle
-- mylibrary
   -- src
   -- build.gradle
-- build.gradle
-- settings.gradle

Android打包流程示意 截屏2022-05-30 11.18.54.png Gradle Build代码,是一段程序文件,运行在本机的JVM上。就如同Apk包是一段程序文件,运行在Art上。

但凡是程序,大抵是要有输入和输出的。

  • apk程序的输入是用户操作应用程序,输出为响应操作。
  • Gradle build程序的输入为项目文件(源码,资源等等),输出为Apk文件。

自动化

一次构建需要处理成千上万个原始文件,最终仅生成一个apk文件,中间有几十上百个流程。如果没有这个构建程序帮我们把流程串联起来,实现自动化,我无法想象光靠人力如何能解决。

这种自动化的能力,不是天生的,是先驱努力的结果。 大家也有机会成为后人的先驱,青史留名。

扩展了解:Jenkins构建

2.Groovy语言

初印象

一说到Gradle,大家第一印象可能是下面这串,不错,这些就是Groovy语言编写的。

// app/build.gradle
plugins {
    id 'com.android.application'
}
android {
    compileSdk 31
    defaultConfig {
        applicationId "com.a.gradlepluginsample"
        minSdk 24
        targetSdk 31
        versionCode 1
        versionName "1.0"
   }
}
dependencies {
    implementation 'androidx.appcompat:appcompat:1.3.0'
    implementation 'com.google.android.material:material:1.4.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
}

再看Groovy

我们来写一段Groovy代码

//Main.groovy
//跟java一模一样
static void main(String[] args) {
    def person = new Person()
    person.name = "zhang san"
    println("Hello world! ${person.name}")
}
class Person {
    def name
}
//运行结果: Hello world! zhang san

再看一段

//ClosureSample.groovy
static def  testClosure(String a, int b, Closure closure) {
    println("$a! $b")
    closure()
}
static def testClosure2( Closure closure) {
    closure()
}

static void main(String[] args) {
    testClosure("hello world", 1) {
        println "i am in closure 1"
    }
    //这个写法更像 buildGradle中的配置
    testClosure2 {
        println "i am in closure 2"
    }
}
//运行结果: 
hello world! 1
i am in closure 1
i am in closure 2

是java,又不是java的感觉。。。

Groovy 是 用于JVM的一种敏捷的动态语言,既可以用于面向对象编程,又可以用作纯粹的脚本语言。 基本特点:

  • Groovy代码动态地编译成运行于Java虚拟机(JVM)上的Java字节码,并与其他Java代码和库进行互操作。
  • 支持闭包,支持DSL(Domain Specific Languages领域特定语言)(build.gradle)
  • 源文件可以当作脚本直接运行,区别于Java源文件

下面这张图,很好的表达了Groovy的部分特性

image.png

扩展:Main.Groovy编译后产物

//Main.class
public class Main extends Script {
    public Main() {
        CallSite[] var1 = $getCallSiteArray();
        super();
    }

    public Main(Binding context) {
        CallSite[] var2 = $getCallSiteArray();
        super(context);
    }

    public static void main(String... args) {
        CallSite[] var1 = $getCallSiteArray();
        var1[0].callStatic(InvokerHelper.class, Main.class, args);
    }

    public Object run() {
        CallSite[] var1 = $getCallSiteArray();
        Object person = var1[1].callConstructor(Person.class);
        String var3 = "zhang san";
        ScriptBytecodeAdapter.setProperty(var3, (Class)null, person, (String)"name");
        return var1[2].callCurrent(this, new GStringImpl(new Object[]{var1[3].callGetProperty(person)}, new String[]{"Hello world! ", ""}));
    }
}

3.基于Groovy的DSL脚本

领域特定语言(DSL)

专注于某个特性程序领域的计算机语言。例如Gradle脚本文件,Android布局xml文件,web的Html文件,正则表达式,SQL等等,离开特定宿主程序,无法使用。

Gradle主要是利用Groovy的闭包特性来编写DSL脚本,所以build.gradle看起来不像代码,更像是一堆配置。

小结

现在我们对Gradle有了初步的认识

  • 构建工具
  • Groovy开发
  • DSL脚本

那么Gradle构建Android项目的过程是什么样的?

二、Gradle构建Android项目

下面是一段Android工程Gradle配置。(上面复制过来)

Sample
-- app
   -- src
   -- build.gradle
-- mylibrary
   -- src
   -- build.gradle
-- build.gradle
-- settings.gradle

Android打包流程示意 截屏2022-05-30 11.18.54.png   构建特点:

  • 拆分不同的任务
  • 任务有先后依赖关系
  • 前一个任务输出是下一个任务的输入,例如class合成为dex
  • 任务也可以并行,如果app和mylibrary没有依赖关系就可以并行提速。

如果是你,你会怎么设计这个构建框架?

先来看看Android Project中的实际情况

module与task.png

整个工程是一个Project。app,buildSrc,mylibrary都是它的Sub Project。 每个Sub Project都对应一组Task集合。

当我们运行app module,会先运行 tasks [:app:assembleDebug]生成apk文件

截屏2022-05-30 16.42.51.png

一次构建被分解为N个Task的执行过程。

Executing tasks: [:app:assembleDebug] in project ../../GradlePluginSample

//buildSrc中的这组是自定义插件内容,先跳过。
> Task ....
> Task :buildSrc:compileKotlin UP-TO-DATE
> Task :buildSrc:classes UP-TO-DATE
> Task :buildSrc:jar UP-TO-DATE
> Task :buildSrc:build UP-TO-DATE

> Configure project :
> Configure project :app

//列举一些好玩的Task
> Task ....
//生成BuildConfig文件
> Task :app:generateDebugBuildConfig

// merge所有的manifest文件
> Task :app:processDebugManifest

//AAPT 生成R文件
> Task :app:processDebugResources
> Task ....
//javac 编译java文件
> Task :app:compileDebugJavaWithJavac

//转换class文件为dex文件
> Task :app:dexBuilderDebug

//打包成apk并签名
> Task :app:packageDebug

那么问题来了,我能写自己的task,嵌入到这些自带的task中么?

答案当然是可以。我们在build.gradle中追加如下代码,然后Gradle Sync后,出现了右侧的task,双击右侧 "Task a"

截屏2022-05-30 17.39.45.png build 日志如下

Executing tasks: [a] in project /../GradlePluginSample/app

hi settings.gradle
> Task :buildSrc:compileKotlin UP-TO-DATE

> Configure project :
hi root build.gradle

> Configure project :app
hi app build.gradle
 task a in Configure //相关输出

> Task :app:...
> Task :app:compileDebugJavaWithJavac UP-TO-DATE
> Task :app:dexBuilderDebug UP-TO-DATE
> Task :app:packageDebug UP-TO-DATE
> Task :app:assembleDebug UP-TO-DATE

> Task :app:a //相关task执行
last  //相关task执行输出

到这里,一个简单的task就写出来了,并且还给它设置了依赖执行的task。

那接下来又会去想,如果我这个task比较复杂呢,而且xx.gradle更偏向于DSL脚本,在这里写逻辑是不合适的,那怎么办?

我们可以把核心逻辑写到单独的工程中,然后在当前工程的xx.gradle中引入核心逻辑。

  • 这个单独的工程的产物,即为Gradle插件
  • 当前工程引入Gradle插件,同时在xx.gradle中为Gradle插件配置参数

再看app/build.gradle,

plugins {
    id 'com.android.application'
}

android {
    compileSdk 31

    defaultConfig {
        applicationId "com.a.gradlepluginsample"
        minSdk 24
        targetSdk 31
        versionCode 1
        versionName "1.0"
    }
}

你甚至可以build.gradle中直接写插件,但插件还是推荐写在独立工程,方便上传仓库,供多个应用使用。

// app/build.gradle
class GreetingPlugin implements Plugin<Project> {
    void apply(Project project) {
        project.task('ahello') {
            doLast {
                println 'Hello from the GreetingPlugin'
            }
        }
    }
}

// Apply the plugin
apply plugin: GreetingPlugin

Gradle插件共有三种方式创建

  • 直接脚本,如上
  • buildSrc Module
  • 独立工程

buildSrc和独立工程较为接近,区别在于,独立工程可以发布到外部仓库。buildSrc只能当前项目使用。

可以通过 ./gradlew init 创建插件工程,但手动也并不麻烦,核心就两个文件

手动在root目录下新建buildSrc文件夹,项目结构如下

Sample 
-- buildSrc 
    -- src/main/kotlin/com/x/x/xxPlugin.kt
    -- build.gradle 
-- build.gradle 
-- settings.gradle

这里我们就可以用Java/kotlin/Groovy编写Plugin逻辑了,其实kotlin也可以编写xx.gradle.

// buildSrc/build.gradle
plugins {
    id 'java-gradle-plugin'
    id 'org.jetbrains.kotlin.jvm' version '1.5.31'
}
repositories {
    jcenter()
    google()
    mavenCentral()
}
dependencies {
    implementation gradleApi() //gradle sdk
    implementation 'com.android.tools.build:gradle:7.2.1'

}

gradlePlugin {
    plugins {
        //定义插件,原先是通过resource文件配置
        firstPlugin {
            id = 'com.a.first'
            implementationClass = 'com.a.plugin.FirstPlugin'
        }
    }
}
// buildSrc/src/main/kotlin/com/a/plugin/FirstPlugin.kt
package com.a.plugin

import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.api.variant.ApplicationVariant
import org.gradle.api.Plugin
import org.gradle.api.Project

class FirstPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        println("hello first plugin")
        println("project name ${project.name}")

        //可以获取到在app/build.gradle中配置的一些属性
        var appExtension = project.extensions.getByType(AndroidComponentsExtension::class.java)
        appExtension.onVariants {  variant ->
            if (variant is ApplicationVariant) {
                println("---buildType:${variant.buildType}---")
                println("minSdkVersion:${variant.minSdkVersion.apiLevel}")
                println("applicationId:${variant.applicationId.get()}")
            }
        }
    }
}

Gradle Sync后,在app中引入插件

// app/build.gradle
plugins {
    id 'com.android.application'
    //使用插件
    id 'com.a.first'
}

双击运行 task assembleDebug,build日志输出

10:10:39: Executing task 'assembleDebug'...
Executing tasks: [assembleDebug] in project /Users/x/y/z/GradlePluginSample/app

hi settings.gradle
> Task :buildSrc:compileKotlin UP-TO-DATE
> Task :...
> Task :buildSrc:jar UP-TO-DATE
> Task :buildSrc:validatePlugins UP-TO-DATE
> Task :buildSrc:build UP-TO-DATE

> Configure project :
hi root build.gradle

> Configure project :app
hello first plugin
project name app
hi app build.gradle
//可以获取到在app/build.gradle中配置的一些属性
---buildType:debug---
minSdkVersion:24
applicationId:com.a.gradlepluginsample
---buildType:release---
minSdkVersion:24
applicationId:com.a.gradlepluginsample

> Task :app:...
> Task :app:compileDebugJavaWithJavac UP-TO-DATE
> Task :app:dexBuilderDebug UP-TO-DATE
> Task :app:packageDebug UP-TO-DATE
> Task :app:assembleDebug UP-TO-DATE

上面的日志输出,你可能会注意到两点:

  • 在执行Task之前,总是会先执行Configure,而且Configure先Root Project,然后Sub Project
  • 我们可以获取到在app/build.gradle中配置的一些属性,例如applicationId,buildType等等,并且是可编程修改的。

小结:

Gradle是什么?引用开头的话:

Gradle Build代码,是一段程序文件,面向构建过程编程。

Gradle插件是什么?

Gradle提供编程框架理念,而具体场景逻辑,以插件的形式进行扩展

我们可以编写插件,嵌入到构建的各个阶段。

Gradle插件能做什么事情

先找找工程中用到的第三方插件:

//多渠道打包
apply plugin: 'com.tencent.vasdolly'
//组件化方案
apply plugin: 'com.alibaba.arouter'
//自动化数据埋点
apply plugin: 'com.x.plugin.statistics'
//上传到maven仓库
apply plugin: 'maven-publish'

还有一些其他用到的插件

//隐私访问api检测
apply plugin: 'privacycheck-plugin'
//符号表mapping.txt上传
apply plugin: 'com.x.plugin.symupload'

再回看Android打包流程示意 截屏2022-05-30 11.18.54.png

看看这些插件都发生在哪些阶段?

  • com.alibaba.arouter,源文件阶段,为我们生成了一些java文件,形如ARouter$$Group$$activity.java
  • com.x.plugin.statistics,class阶段,修改class文件,在某些method中注入代码
  • privacycheck-plugin,同上,修改class文件
  • com.tencent.vasdolly,apk阶段,多渠道打包,apk签名
  • maven-publish,library工程构建后阶段,从class到jar,打包发布到外部仓库
  • com.x.plugin.symupload,app工程构建后阶段,上传构建产物mapping.txt

你甚至可以通过插件直接往dex中写入class或者生成dex(某些热修复方案)

回到这段开头的问题

Gradle插件能做什么事情? 答案是无所不能

  • 当你觉得模版代码文件太多,想少写一些
  • 当你觉得一个个文件检查累死,想批量检查
  • 当你觉得一个个文件改代码累死,想批量修改
  • 当你觉得三方lib不能满足你需要,想魔改
  • 当你觉得流程太多,操作繁琐,头皮发麻,想更加智能一些
  • 当你觉得。。。。

想想你的痛点,从编写第一个插件开始

参考

Gradle详解 邓凡平

Gradle-维基百科

Developing Custom Gradle Plugins

Android Gradle插件开发指南

滴滴DoKit-Android核心原理揭秘之AOP字节码实现

Java字节码增强探秘-美团

Android构建流程