Gradle必备

Gradle基础

gradle是一个脚本框架,用来构建android程序的一个脚本,在认识前,我们先以最基础的方式来认识它。

gradle配置

首先在官网下载对应的版本,这里我选择6.6.1-all的版本,下载完后,放到指定的目录,然后配置gradle的环境变量:

  1. open -e .bash_profile 打开配置文件
  2. 添加 GRADLE_HOME、PATH
1
2
3
4
5
6
7
export GRADLE_HOME=/Users/xiangcheng/gradle/gradle-6.6.1
export PATH=${PATH}:/Users/xiangcheng/gradle/gradle-6.6.1/bin

----------------官方写法如下--------------------------------

export GRADLE_HOME=/Users/xiangcheng/gradle/gradle-6.6.1
export PATH=$PATH:$GRADLE_HOME/bin
  1. source .bash_profile 重置配置文件,以便新 path 生效
  2. open -e ~/.zshrc 打开另一个配置
  3. 在最后一行添加 source ~/.bash_profile
  4. source ~/.zshrc 重置配置文件

配置 zshrc 是因为有的机器 bash_profile 配置不管用,添加这个就行了

  1. 运行 gradle –version,出现版本号则 Gradle 配置成功 gradle安装 注意:gradle版本和jdk版本需要有对应关系: gradle和jdk对应关系 这里我安装的是gradle-6.6.1的版本,因此我需要设置JDK8,如果本地安装的jdk版本多个的话,mac会使用最高的版本,先查看本地安装了哪些jdk版本
1
/usr/libexec/java_home -V

输出如下:

1
2
3
4
Matching Java Virtual Machines (3):
    18.0.2 (arm64) "Amazon.com Inc." - "Amazon Corretto 18" /Users/xiangcheng/Library/Java/JavaVirtualMachines/corretto-18.0.2/Contents/Home
    11.0.23 (arm64) "Amazon.com Inc." - "Amazon Corretto 11" /Users/xiangcheng/Library/Java/JavaVirtualMachines/corretto-11.0.23/Contents/Home
    1.8.0_412 (arm64) "Amazon" - "Amazon Corretto 8" /Users/xiangcheng/Library/Java/JavaVirtualMachines/corretto-1.8.0_412/Contents/Home

所以默认用的是jdk18,我需要改下环境变量,将jdk改成8:

1
2
export JAVA_HOME=/Users/xiangcheng/Library/Java/JavaVirtualMachines/corretto-1.8.0_412/Contents/Home
export PATH=$JAVA_HOME/bin:$PATH

再运行:

1
source ~/.bash_profile

上面的jdk配置好后,然后找个文件夹,创建build.gradle,然后输入:

1
println("hello world!")

然后用gradle build.gradle运行该文件,输出如下: 运行结果 说明已经成功了。

gradle初始化工程

平时都是直接通过as创建工程,下面通过gradle来创建一个工程:

  1. 输入gradle init来初始化
  2. 接着会输入相关配置信息: alt text
  3. 命令行提示选择项目模板
  4. 命令行提示选择开发语言
  5. 命令行提示选择脚本语言
  6. 输入工程名
  7. 输入包名 最终工程创建如下: 通过gradle初始化创建的工程 和android studio创建的工程结构是一致的。

Gradle Wrapper

as创建一个工程的时候,默认会有Gradle Wrapper文件夹,它里面会有个wrapper.properties文件夹,配置了gradle相关信息。
Gradle Wrapper 文件的作用就是可以让你的电脑在不安装配置 Gradle 环境的前提下运行 Gradle 项目,你的机器要是没有配 Gradle 环境,那么你 clone gradle 项目下来,执行 init 命令,会根据 gradle-wrapper.properties 文件中声明的 gradle URL 远程路径去下载 gradle 构建工具,cd 进该项目,执行了./gradlew -v,然后下载了对应版本的gradle,如果此时执行的是gradle -v,就用的是本地配置的环境变量中的gradle版本,此时不会去下载。 alt text 然后就可以在项目目录下运行 gradle 命令了,不过还是推荐大家在机器配置统一的 Gradle 环境

  • gradlew –> linux 平台脚本
  • gradlew.bat –> window 平台脚本
  • gradle-wrapper.jar –> Gradle 下载、管理相关代码
  • gradle-wrapper.properties –> Gradle 下载、管理配置参数

gradle-wrapper.properties 文件中参数详解:

  • distributionUrl –> Gradle 压缩包下载地址

  • zipStoreBase –> 本机存放 Gradle 压缩包主地址

  • zipStorePath –> 本机存放 Gradle 压缩包主路径

    • Gradle 压缩包完整的路径是 zipStoreBase + zipStorePath
  • distributionBase –> 本机 Gradle 压缩包解压后主地址

  • distributionPath –> 本机 Gradle 压缩包解压后路径

    • Gradle 解压完整的路径是 distributionBase + distributionPath
    • distributionBase 的路径是环境 path 中 GRADLE_USER_HOME 的地址
    • Windows:C:/用户/你电脑登录的用户名/.gradle/
    • MAC:~/.gradle/
    • 你 MAC 要是配了 Gradle 环境变量,distributionBase 就是你自己解压缩的 gradle 路径

这几个地址还是要搞清楚的~

关于as如果配置统一的gradle版本,可以看如下截图: alt text

  • gradle-wrapper.properties – 使用 wrapper 也就是 AS 来管理 Gradle
  • Specifiled location – 使用本地文件,也就是我们自己管理 Gradle,这样每个工程的gradle-wrapper.properties就不起作用了,使用本机配置的gradle版本
gradlw和gradle区别

gradlew实际是一个脚本文件,它是在gradle工程的根目录下面,如果是windows的话,会执行gradlew.bat文件,这个在前面通过命令初始化工程的时候可以看到,其中gradlew脚本会通过gradle/wrapper/gradle-wrapper.jar来使用项目中配置的gradle版本,而gradle命令是使用本机配置的gradle版本。

Gradle中task

task可以认为是gradle中最小执行单元,正是因为有了task,gradle框架才能有序的工作。实际上根据 Gradle 构建项目的流程,是先把所有的 .gradle 脚本执行一遍,编译生成对应的 Gradle、Setting、Project 对象,然后根据我们在构建脚本中设置的构建配置,生成一张 Task 组成的:有向无环图,先来看看这张图: alt text 左边表示的是task之间的依赖关系,右边表示的是项目构建过程中要经过的任务,正是因为有了这些任务,项目才能构建出来。

实际上在gradle构建过程中,会把每一个工程的build.gradle文件会构建成一个project对象,而该project对象由一个个的task组成,比如在构建android项目的时候会有编译java代码的task,编译资源的task等。这些task其实是定义在android插件中,关于插件后面再说。

比如android工程中有很多我们熟悉的task: alt text 可以看到在build这个分组下面有很多熟悉的task,比如有assemble的task,它是用来打包的task。而它们又依赖其它的task,最终一个个的task被执行完,包也打出来了。 比如我在终端上运行./gradlew build的时候,会执行一大堆的task: alt text

Task 是完成一类任务,实际上对应的也是一个对象。而 Task 是由无数个 Action 组成的,Action 代表的是一个个函数、方法,每个 Task 都是一堆 Action 按序组成的执行图,就好像我们在 Class 的 main 函数中按照逻辑调用一系列方法一样。

创建task

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
task hello{
    println "hello world"
}

task(hello2){
    println "hello world2"
}

task ('hello3'){
    println "hello world3"
}

运行task

通过./gradlew task名来运行task,运行结果如下: alt text

如果是子模块的话,则通过./gradlew 模块名:task名运行task,运行结果如下: alt text

这是在工程的根目录下build.gradle定义的三个task,然后通过./gradlew hello命令来执行hello这个task,而其它两个task也输出日志,是因为这几条日志都是在配置阶段输出,关于gradle的生命周期后面会说。那为什么gradlew执行脚本就在根目录下,还需要./来执行该脚本呢? alt text 这是因为在终端里面,需要通过./来获取当前目录,如果不加的话,会导致终端在目录下找不到。

其实task是project的方法,project表示的是模块在初始化阶段会生成,上面使用task hello{},创建的task实际是传入了action的闭包,它是在构建的时候被调用的: alt text

在创建task的时候可以指定其他属性,可以通过task其他的重载方法来创建:

1
2
3
task demo1(group: "demo组",name:"demo2",description:"我是demo2的task",action:{
    println("我是demo2 task的action")
})

比如在构建demo1的时候,申明了其它属性,此处的action跟上面写在括号外面是一回事,只不过此处是通过map来创建的,还有一点,map中指定的name不会生效,会被demo1的名字给覆盖了,目前发现没什么作用,执行结果如下: alt text

task.dofirst和dolast

doFirst是task执行前要调用的action,doLast是task执行后要调用的action:

1
2
3
4
5
6
7
8
9
task demo2{
    println("demo2")
    doFirst{
        println("demo2 的 doFirst")
    }
    doLast{
        println("demo2 的 doLast")
    }
}

关于task其它的使用可以在看三方插件的时候再看。

Gradle插件

gradle插件(plugin)是构建项目时候用到的,它其实是由一系列的task组成的,其实android的工程,google已经帮我们实现了android gradle plugin(agp)。google也是遵循了gradle的task依赖法则,构成一个有序无向图。最终去执行一个个的tak。

插件主要通过定义仓库,然后定义插件的名字,比如安卓中依赖agp是通过如下的方式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
buildscript {
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.5.3'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}

buildscript是rootProject的方法,它声明插件的仓库、添加插件专用闭包 allprojects主要是指所有的模块需要的三方依赖库时候的仓库 像上面定了agp插件3.5.3,然后在app模块中通过apply plugin: 'com.android.application'来使用这个插件,而app模块选的android{}就是来自于该插件,我们叫他是DSL配置块。 其实像android{}的实现在插件中是通过delegate+闭包实现 DSL 配置块

  1. 首先把闭包定义好:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def android = {
	compileSdkVersion 25
	buildToolsVersion "25.0.2"
    
    // 这个对应相应的方法
	defaultConfig {
		minSdkVersion 15
		targetSdkVersion 25
		versionCode 1
		versionName "1.0"
	}
}
  1. 定义数据模型
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Android {
    int mCompileSdkVersion
    String mBuildToolsVersion
    BefaultConfig mBefaultConfig

    Android() {
        this.mBefaultConfig = new BefaultConfig()
    }

    void defaultConfig(Closure closure) {
        closure.setDelegate(mProductFlavor)
        closure.setResolveStrategy(Closure.DELEGATE_FIRST)
        closure.call()
    }
}

class BefaultConfig {
    int mVersionCode
    String mVersionName
    int mMinSdkVersion
    int mTargetSdkVersion
}
  1. 绑定数据
1
2
3
Android bean = new Android()
android.delegate = bean//将上面定义好的bean给设置到闭包里面
android.call()//闭包的调用

Gradle构建过程

其实构建过程就是指编译对应的.gradle文件,然后生成对象,最后执行task。 主要分为三个步骤:

  • Initialization:初始化阶段,按顺序执行init.gradle,setting.gradle,生成Gradle、Setting、Project对象
  • Configuration:配置阶段,按顺序执行 root build.gradle -> 子项目 build.gradle 脚本,生成 Task 执行流程图
  • Execution:执行阶段,按照 Task 执行图顺序运行每一个 Task,完成一个个步奏,生成最终 APK 文件 整个流程如下: alt text 通过流程图观察,我们可以在合适的时机监听gradle的构建过程,也就是钩子方法。 下面通过这些钩子方法来获取项目构建各个阶段、任务的耗时情况,在setting.gradle中添加如下代码:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
long beginOfSetting = System.currentTimeMillis()
def beginOfConfig
def configHasBegin = false
def beginOfProjectConfig = new HashMap()
def beginOfProjectExcute
gradle.projectsLoaded {
    println '初始化阶段,耗时:' + (System.currentTimeMillis() -
            beginOfSetting) + 'ms'
}
gradle.beforeProject { project ->
    if (!configHasBegin) {
        configHasBegin = true
        beginOfConfig = System.currentTimeMillis()
    }
    beginOfProjectConfig.put(project, System.currentTimeMillis())
}
gradle.afterProject { project ->
    def begin = beginOfProjectConfig.get(project)
    println '配置阶段,' + project + '耗时:' +
            (System.currentTimeMillis() - begin) + 'ms'
}
gradle.taskGraph.whenReady {
    println '配置阶段,总共耗时:' + (System.currentTimeMillis() -
            beginOfConfig) + 'ms'
    beginOfProjectExcute = System.currentTimeMillis()
}
gradle.taskGraph.beforeTask { task ->
    task.doFirst {
        task.ext.beginOfTask = System.currentTimeMillis()
    }
    task.doLast {
        println '执行阶段,' + task + '耗时:' +
                (System.currentTimeMillis() - task.beginOfTask) + 'ms'
    }
}
gradle.buildFinished {
    println '执行阶段,耗时:' + (System.currentTimeMillis() -
            beginOfProjectExcute) + 'ms'
}

由于在setting.gradle执行前,Gradle对象其实已经生成,所以通过gradle对象的projectsLoaded回调获取初始化时间。第一次进入到beforeProject的时候记录配置的起始时间,并记录每一个project的配置起始时间,然后在afterProject中算出每一个project配置的时间。然后通过gradle.taskGraph来获取有序无向图,然后通过whenReady方法回调算出总的配置时间。最后通过有序无向图的beforeTask来添加task的开始和结束的执行时间来算出task的执行耗时。最后通过gradle的buildFinish来获取总共的构建时间。

gradle打包速度优化

首先了解下安卓中的variant是什么意思,它表示一个包的变体,其中变体是由productFlavor(产品风味)和buildType(构建类型)组合而成的。其中productFlavor是由维度和渠道构成的,buildType指的是debug还是release,比如维度可以配置价格和渠道的两个维度,而渠道可以配置google、xiaomi等渠道:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
android {
    // 定义 buildTypes(构建类型)
    buildTypes {
        debug {
            minifyEnabled false
            applicationIdSuffix ".debug"
        }
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    // 定义 productFlavors(产品风味)
    flavorDimensions 'channel', 'pricing'
    productFlavors {
        //配置渠道维度下两个变体
        google {
            dimension 'channel'
        }
        xiaomi {
            dimension 'channel'
        }
        //配置价格下两个变体
        free {
             dimension 'pricing'
        }
        vip {
             dimension 'pricing'
        }
    }
    // 在这里的 `productFlavors` 和 `buildTypes` 组合形成不同的 variants
}

所以上面通过维度和渠道组合后的productFlavor有googleFree、googleVip、xiaomiFree、xiaomiVip四个,再和buildType组合后,就是8个变体,分别是:

  • googleFreeDebug
  • googleFreeRelease
  • googleVipDebug
  • googleVipRelease
  • xiaomiFreeDebug
  • xiaomiFreeRelease
  • xiaomiVipDebug
  • xiaomiVipRelease

知道了variant后,下面就是了解下打包了,一般通过build Analyze查看build过程中的一些警告,使用该工具需要先进行build,第二个工具是gradle profile工具,一般通过如下命令:

1
2
./gradlew installGoogleFreeDebug --build-cache -x lint -x test --warning-mode=none -Pkotlin.compiler.execution.strategy=in-process -PcompilerArgs=-Xlint:deprecation --no-configuration-cache --profile
adb shell am start -n 包名/应用主入口

如果通过adb命令启动主入口的话,需要在清单文件中给主入口加上android:exported="true"属性,装包完了后,本地会生成一个html文件,该文件会有gradle每个阶段的耗时情况,并且会有每个task的耗时。

再个就是gradle scan工具,一般通过如下命令:

1
2
./gradlew installGoogleFreeDebug --build-cache -x lint -x test --warning-mode=none -Pkotlin.compiler.execution.strategy=in-process -PcompilerArgs=-Xlint:deprecation --no-configuration-cache --scan
adb shell am start -n 包名/应用主入口

scan会生成一个在线的网页,第一次使用需要邮箱校验,它的信息比profile更全面,更加智能,其中performance跟profile中效果差不多,它多了timeline选项,表示什么时候执行了什么task,switchs选项表示一些建议,比如缓存没开,会提示off。
其实gradle优化最直接是通过组件化来开发,如果全app编译的话,组件越多,需要的时间越长,所以通过组件化来降低打包时间是最直接的,把一些其他依赖的组件通过aar进行依赖或远程依赖。
参考:
Gradle 爬坑指南 – 概念初解、Grovvy 语法、常见 API
Gradle 爬坑指南 – 理解 Plugin、Task、构建流程
深度探索 Gradle 自动化构建技术(三、Gradle 核心解密)

Licensed under CC BY-NC-SA 4.0
Built with Hugo
Theme Stack designed by Jimmy