Gradle详解

Gradle 是一款强大的构建工具,但 Gradle 不仅仅是一款强大的构建工具,它更像是一个编程框架。Gradle 的组成可以细分为如下三个方面:

groovy 核心语法:包括 groovy 基本语法、闭包、数据结构、面向对象等等。
Android DSL(build scrpit block):Android 插件在 Gradle 所特有的东西,我们可以在不同的 build scrpit block 中去做不同的事情。
Gradle API:包含 Project、Task、Setting 等等。

Gradle 的语法是以 groovy 为基础的,而且它还有自己独有的 API,所以我们可以把 Gradle 认作是一款编程框架,利用 Gradle 我们可以在编程中去实现项目构建过程中的所有需求。想要随心所欲地使用 Gradle,我们必须提前掌握好 groovy。需要注意的是,Groovy 是一门语言,而 DSL是一种特定领域的配置文件,Gradle 是基于 Groovy 的一种框架工具,而gradlew 则是 gradle 的一个兼容包装工具。

一、Gradle的优势:

Gradle 有以下优势:

  1. 灵活性:相对于 Maven、Ant 等构建工具,Gradle 提供了一系列的 API 让我们有能力去修改或定制项目的构建过程。例如我们可以利用 Gradle 去动态修改生成的 APK 包名。
  2. 粒度性:使用 Maven、Ant 等构建工具时,我们的源代码和构建脚本是独立的,而且我们也不知道其内部的处理是怎样的。但是 Gradle 则不同,它从源代码的编译、资源的编译、到生成 APK 的过程中都是一个接一个来执行的。此外,Gradle 构建的粒度细化到了每一个task 之中。并且它所有的 Task 源码都是开源的,在我们掌握了这一整套打包流程后,我们就可以通过修改它的 Task 去动态改变其执行流程。例如 Tinker 框架的实现过程中,它通过动态地修改 Gradle 的打包过程生成 APK 的同时,也生成了各种补丁文件。
  3. 扩展性:Gradle 支持插件机制,所以我们可以复用这些插件,就如同复用库一样简单方便。
  4. 兼容性:Gradle 不仅自身功能强大,而且它还能兼容所有的 Maven、Ant 功能,也就是说,Gradle 吸取了所有构建工具的长处。

可以看到,Gradle 相比于其它构建工具,其好处不言而喻,而其最核心的原因就是因为
Gradle 是一套编程框架。

二、Gradle 的生命周期

所谓 Gradle 的生命周期,即 gradle 的执行流程,也就是 Gradle 先执行什么后执行什么。

gradle的执行流程分了Initialization初始化阶段、Configuration配置阶段和Execution执行阶段。Initialization初始化阶段解析整个工程中所有Project,构建所有的Project对应的project对象。Configuration配置阶段解析所有projects对象中的task,构建好所有task的拓扑图。Execution执行阶段执行具体的task及其依赖task。

Gradle详解_第1张图片

2.1、初始化阶段

初始化阶段会读取根工程中的 setting.gradle 中的 include 信息,确定有多少工程加入构建, 然后会为每一个项目(build.gradle 脚本文件)创建一个个与之对应的 Project 实例,最终形成一个项目的层次结构。

与初始化阶段相关的脚本文件是 settings.gradle, 而一个 settings.gradle 脚本对应一个Settings 对象,我们最常用来声明项目的层次结构的 include 就是 Settings 对象下的一个方法,在 Gradle 初始化的时候会构造一个 Settings 实例对象,以执行各个 Project 的初始化配置。
在 settings.gradle 文件中,我们可以指定其它 project 的位置,这样就可以将其它外部工程中的 moudle 导入到当前的工程之中了。

include "speech"
project(":speech").projectDir = new File("../OtherApp/speech")

2.2、配置阶段

配置阶段的任务是执行各项目下的 build.gradle 脚本,完成 Project 的配置,与此同时,会构造 Task 任务依赖关系图以便在执行阶段按照依赖关系执行 Task。而在配置阶段执行的代码通常来说都会包括以下三个部分的内容,如下所示:

  1. build.gralde 中的各种语句。
  2. 闭包。
  3. Task 中的配置段语句。

需要注意的是,执行任何 Gradle 命令,在初始化阶段和配置阶段的代码都会被执行。

2.3、执行阶段

在配置阶段结束后,Gradle 会根据各个任务 Task 的依赖关系来创建一个有向无环图,我们可以通过 Gradle 对象的 getTaskGraph 方法来得到该有向无环图。并且当有向无环图构建完成之后, 所有 Task 执行之前, 我们可以通过 whenReady(groovy.lang.Closure) 或者addTaskExecutionGraphListener(TaskExecutionGraphListener) 来接收相应的通知,其代码如下所示:

gradle.getTaskGraph().addTaskExecutionGraphListener( 
  new TaskExecutionGraphListener() {
	@Override
	void graphPopulated(TaskExecutionGraph graph) {
	}
})

然后,Gradle 构建系统会通过调用 gradle <任务名> 来执行相应的各个任务。可以看到,整个 Gradle 生命周期的流程包含如下 四个部分:

首先,解析 settings.gradle 来获取模块信息,这是初始化阶段。然后,配置每个模块,配置的时候并不会执行 task。
接着,配置完了以后,有一个重要的回调 project.afterEvaluate,它表示所有的模块都已经配置完了,可以准备执行 task 了。

最后,执行指定的 task 及其依赖的 task。

在 Gradle 构建命令中,最为复杂的命令可以说是 gradle build 这个命令了,因为项目的构建过程中需要依赖很多其它的 task。

2.4、生命周期监听

生命周期监听的设置有两种方法:

  1. 实现一个特定的监听接口;
  2. 提供一个用于在收到通知时执行的闭包。

Project 提供的一些生命周期回调方法:

- afterEvaluate(closure); 
- afterEvaluate(action); 
- beforeEvaluate(closure); 
- beforeEvaluate(action);

Gradle 提供的一些生命周期回调方法:

- afterProject(closure)afterProject(action) 
- beforeProject(closure)beforeProject(action) 
- buildFinished(closure)buildFinished(action) 
- projectsEvaluated(closure)projectsEvaluated(action) 
- projectsLoaded(closure)projectsLoaded(action) 
- settingsEvaluated(closure)settingsEvaluated(action) 
- addBuildListener(buildListener) 
- addListener(listener) 
- addProjectEvaluationListener(listener)

三、project 核心 api

每一个待编译的工程都是一个 Project,Gradle为每个build.gradle都会创建一个相应的Project领域对象,在编写Gradle脚本时,实际上是在操作 诸如Project这样的Gradle领域对象,所有.gradle的文件都可以编写Groovy代码。

在 Project 中有很多的 API,但是根据它们的属性和用途我们可以将其分解为六大部分:

Project相关 API:让当前Project拥有了操作它的父Project以及管理它的子Project的能力。

Task 相关 API:为当前 Project 提供了新增 Task 以及管理已有 Task 的能力。

属性相关的 Api:Gradle 会预先为我们提供一些 Project 属性,而属性相关的 api 让我们拥有了为 Project 添加额外属性的能力。

File 相关 Api:Project File 相关的 API 主要用来操作我们当前 Project 下的一些文件处理。

Gradle 生命周期 API:即我们在上一篇讲解过的 Gradle 核心之生命周期。

其它 API:添加依赖、添加配置、引入外部文件等等零散 API 的聚合。

3.1、Project 相关 API

通过 gradle 管理的工程都会有一个根工程 project,根工程用来管理子工程。下面我们来看看 Project 相关的 API。

3.1.1 getAllprojects()

getAllprojects 表示获取所有 project 的实例,示例代码如下所示:

def getProjects(){ 
  this.getAllprojects().eachWithIndex{
    Project project, int index	-> if(index==0){
      println "Root Project :${project.name}"
    }else{
      println "+--- Project :${project.name}"
    }
  }
}

我们调用了 getAllprojects 方法返回一个包含根 project 与其子 project 的 Set 集合,并链式调用了 eachWithIndex 遍历 Set 集合。接着,我们会判断当前的下标 index 是否是 0, 如果是则表明当前遍历的是 rootProject,则输出 rootProject 的名字,否则输出 child project 的名字。

然后我们在命令行执行 gradle clean,其运行结果可以看到,会先配置我们的 rootProject, 并输出了对应的工程信息。接着便会执行子工程 app 的配置。rootProject 与其旗下的各个子工程组成了一个树形结构。

3.1.2 getSubprojects()

getSubprojects 表示获取当前工程下所有子工程的实例,同 getAllprojects 的用法一样,
getSubprojects 方法返回了一个包含子 project 的 Set 集合。

3.1.3 getParent()

getParent().name 是获取父 project 的方法。 如果在根目录的 build.gradle 中调用
getParent() ,由于根 project 没有父节点了,所有返回的是 null。

3.1.4 getRootProject()

getRootProject().name 获取的是根节点 project。形成的 project 树中肯定是有根节点的,所以在任意子节点 project 中调用 getRootProject 都返回的是根节点 project,所以肯定不会返回空。

3.1.5 project()

project 表示的是指定工程的实例,然后在闭包中对其进行操作。可以看到,在 project 方法中两个参数,一个是指定工程的路径,另一个是用来配置该工程的闭包。下面我们看看如 何灵活地使用 project:

project('app'){ 
  Project project ->
  apply plugin: "com.android.application" group 'com.test'
  version '1.0.0' dependencies{} android{}
}
3.1.6 allprojects()

allprojects 表示用于配置当前 project 及其每一个子 project,在 allprojects 中我们一般用来配置一些通用的配置,比如最常见的全局仓库配置。

3.1.7 subprojects()

subprojects 用于统一配置当前 project 下的所有子 project, 给所有的子工程引将aar 文件上传置Maven 服务器的配置脚本:

subprojects {
  if (project.plugins.hasPlugin("com.android.library")) { 
    apply from:'../publishToMaven.gradle'
  }
}

先判断当前 project 下的子 project 是不是库,如果是库才有必要引入 publishToMaven 脚本。需要注意与 allprojects() 的区别是,subprojects() 不包含当前 project。

3.2 属性相关 API

/**
* The default project build file name.
*/
String DEFAULT_BUILD_FILE = "build.gradle";
/**
* The hierarchy separator for project and task path names.
*/
String PATH_SEPARATOR = ":";
/**
* The default build directory name.
*/
String DEFAULT_BUILD_DIR_NAME = "build";
String GRADLE_PROPERTIES = "gradle.properties"; 
String SYSTEM_PROP_PREFIX = "systemProp"; 
String DEFAULT_VERSION = "unspecified";
String DEFAULT_STATUS = "release";

第一个属性 DEFAULT_BUILD_FILE = “build.gradle” 表明默认读取的配置文件是 build.gradle, 这也证明了上面说有 build.gradle 的文件夹就是一个 project 的结论。

第二个属性 PATH_SEPARATOR 表示的是分隔符。

第三个属性 DEFAULT_BUILD_DIR_NAME 表示默认的输出文件夹,每个工程都会有一个 build文件夹存放 project 输出。

3.2.1 ext 扩展属性
ext{
  //测试环境/正式环境
  isRelease = true;
  //定义一个包名,用于存放APT生成的所有类文件
  packageNameForAPT = "com.new_modular_customarouter"
  
  app_android = [
    compileSdkVersion : 29,
    buildToolsVersion : "29.0.3",
    applictionId      : "com.new_modular_customarouter",
    minSdkVersion.    : 15,
    targetSdkVersion  : 29,
    versionCode       : 1,
    versionName       : "1.0"
  ]
	// order,personal,自己的包名
	app_appid = [
    "order": "com.new_modular_customarouter.order", 
    "personal": "com.new_modular_customarouter.personal"
  ]
	// 测试环境的服务器地址,和,正式环境服务器地址
	url = [
    "debug"	: "https://11.22.33.44/debug", 
    "release" : "https://55.66.77.88/release"
  ]

// 定义依赖项,体现真实开发项目的过程
  app_dependencies = [
    "appcompat": 'androidx.appcompat:appcompat:1.1.0',
    "constraint": 'androidx.constraintlayout:constraintlayout:1.1.3',
    "okhttp3"	: "com.squareup.okhttp3:okhttp:3.10.0", 
    "retrofit"	: "com.squareup.retrofit2:retrofit:2.5.0", 
    "fastjson"	: "com.alibaba:fastjson:1.2.58"
  ]
}





apply plugin: 'com.android.application'

// 定义变量
def app_android = this.getRootProject().ext.app_android;
def app_dependencies = this.rootProject.ext.app_dependencies; 
android {
  compileSdkVersion app_android.compileSdkVersion 
  buildToolsVersion app_android.buildToolsVersion 
  defaultConfig {
    applicationId app_android.applicationId 
    minSdkVersion app_android.minSdkVersion 
    targetSdkVersion app_android.targetSdkVersion 
    versionCode app_android.versionCode 
    versionName app_android.versionName

		// 这个方法用于在BuildConfig.java中添加属性,Java代码中可以直接使用			 BuildConfig.isRelease获取其值,接收三个非空的参数,第一个:确定值的类型,第二个:指定 key 的名字, 第三个:传值(必须是 String)切记:不能在 android 根节点,只能在 defaultConfig 或 buildTypes 节点下
  	buildConfigField("boolean", "isRelease", String.valueOf(isRelease))

		// 在 gradle 文件中配置选项参数值(用于 APT 传参接收)
		// 必须写在 defaultConfig 节点下javaCompileOptions {
		annotationProcessorOptions {
		arguments = [
      moduleName: project.getName(), 
      packageNameForAPT: packageNameForAPT
    ]
    }
  }
}

dependencies {
  implementation fileTree(dir: 'libs', include: ['*.jar'])
  // 循环引入第三方库
  app_dependencies.each {
    k, v ->implementation v
  }
  if (isRelease) {
    implementation project(":order") implementation project(":personal")
  }
}

3.3文件相关 API

文件定位:常用的文件定位 API 有下面两个方法:

//定位单个文件
File file(Object path);
//定位多个文件
ConfigurableFileCollection files(Object... paths);

def file =file(path)

文件拷贝:常用的文件拷贝 API 为copy,不仅可以对文件进行拷贝,也可以对文件夹进行拷贝。

copy{
  //将 app module 下生成的 apk 目录拷贝到根目录下的 build 目录中
  from file('build/outputs/apk/')
  into getRootProject().getBuildDir().path + '/apk' exclude{
    //排除不需要拷贝的文件
  }
  rename{
    //对拷贝过来的文件进行重命名
  }
}

四、Task

Gradle以Task(任务)的形式组织每一步操作,每个Task执行一个原子操作(例如把Java编译成class文件、把class打成jar/dex文件、APK签名等)。

每个Project包含若干Task,Task之间存在依赖关系,执行一个Task前,会先执行它所依赖的Task。

每个Task有自己的名字(例如'assemble'),结合其所属Project的名字(例如':app'),可以组成完整名(例如':app:assemble')。

Gradle内建了一个名为tasks的Task,可以列举Project中的所有Task。

一个Project由一个或者多个Task组成,它是构建过程中的原子任务,可以使编译class、上传jar包等

4.1 Task 定义与配置

task helloTask(group:'leren',description: 'task'){
  
}
this.tasks.create(name:'helloTask1'){ 
  setGroup('leren') 
  setDescription('task')
}

这些属性都是可以进行配置的。

参数 含义 默认值
name task的名字 不能为空,必须指定
type task的父类 DefaultTask
action 当task执行的时候,需要执行的闭包closure或行为Action Null
overwrite 是否替换已经存在的task false
dependsOn task依赖的task的集合 []
group task属于哪个组 null
description task的描述 null
constructorArgs 传递到Task Class构造器中的参数 Null

4.2 Task 执 行

//计算build执行时长
def startBuildTime,endBuildTime
this.afterEvaluate{
  def preBuildTask = project.tasks.getByName('preBuild')
  preBuildTask.doFirst{
    startBuildTime = System.currentTimeMillis()
    println 'the start time is '+startBuildTime
  }
  def buildTask = project.tasks.getByName('build')
  buildTask.doLast{
    endBuildTime = System.currentTimeMillis()
    println "this.build time is :"${endBuildTime-startBuildTime}
  }
}

4.3 Task 执行顺序

指定 Task 的执行顺序有三种方式:dependsOn 强依赖方式、通过 Task 输入输出指定、通过 API 指定执行顺序。

4.3.1 dependsOn 强依赖方式

dependsOn 强依赖的方式可以细分为静态依赖和动态依赖,被依赖的 task 先执行,这和我们 java 的继承关系是很相似的。

4.3.2 通过 Task 输入输出指定

通过 Task 来指定输入输出,Task 的输入输出对应 TaskInput 和 TaskOutput。首先,我们定义了一个 WirteTask,然后,在注释 1 处,指定了输出文件为destFile, 并写入版本信息到 XML 文件。接着,定义了一个 readTask,并在注释 2 处,指定输入文件为上一个 task(即 writeTask) 的输出文件。最后, 在注释 3 处,使用 dependsOn 将这两个 task 关联起来,此时输入与输出的顺序是会先执行写入,再执行读取。这样,一个输入输出的实际案例就实现了。

task writeTask{
  inputs.property('versionCode',this.versionCode)
  inputs.property('versionName',this.versionName)
  inputs.property('versionInfo',this.versionInfo)
  //1、指定输出文件为destFile
  outputs.file this.destFile
  doLast{
    def data = inputs.getProperties()
    File file = outputs.getFiles().getSingleFile()
    //写入版本信息到xml文件
  }
}
task readTask{
  //2、指定输入文件为上一个task的输出文件
  inputs.file this.destFile
  doLast{
    //读取输入文件的内容并显示
    def file = inputs.files.singleFile
    println file.text
  }
}
task outputwithinputTask{
  dependsOn writeTask,readTask
  doLast{
    
  }
}
4.3.3 通过API指定执行顺序

除了 dependsOn 的方式,我们还可以在 task 闭包中通过 mustRunAfter 方法指定 task 的依赖顺序,mustRunAfter 可以指定一个或多个 task,其示例代码如下所示:

task taskX{
  doLast{
    println 'taskX'
  }
}
task taskY{
  mustRunAfter taskX
  doLast{
    println 'taskY'
  }
}

4.4 挂接自定义 task 到构建生命周期

我们可以使用 gradle 提供的一系列生命周期 API 去挂接我们自己的 task 到构建生命周期之中,比如使用 afterEvaluate 方法将我们第三小节定义的 writeTask 挂接到 gradle 配置完所有的 task 之后的时刻,示例代码如下所示:

this.project.afterEvaluate{ project ->
  def buildTask = project.tasks.getByName('build')
  if(buildTask==null){
    throw GradleException('the build task is not found')
  }
  buildTask.doLast{
    writeTask.execute()
  }
}

4.5 Task 类型

除了定义一个新的 task 之外,我们也可以使用 task 的 type 属性来直接使用一个已有的 task 类型,比如 Gradle 自带的 Copy、Delete、Sync task 等等。示例代码如下所示:

//删除根目录下的build文件
task clean(type:Delete){
  delete rootProject.buildDir
}
//将doc复制到build/target目录下
task copyDocs(type: Copy){
  from 'src/main/doc'
  into 'build/target/doc'
}
//执行时会复制源文件到目标目录,然后从目标目录删除所有非复制文件
task syncFile(type: Sync){
  from 'src/main/doc'
  into 'build/target/doc'
}

4.6 常用Task操作

4.6.1 执行Task,查看Project中的所有Task

执行Task时,直接把Task名称传给gradle即可。

如果下载了Gradle并配置了环境变量,则可在工程根目录执行:

gradle tasks

更推荐的做法,是在工程根目录下调用GradleWrapper执行:

./gradlew tasks
4.6.2 执行多个Task

如果需要先后执行多个Task,例如tasks和clean,将它们依次传给gradle即可:

./gradlew tasks clean
4.6.3 排除特定Task

使用gradle的-x--exclude-task参数,可指定执行Task时排除特定Task。例如:

./gradlew build -x check
4.6.4 执行SubProject中的Task

如果想执行子工程':app'中的Task,可使用Task的完整名执行

./gradlew :app:tasks

也可以切换到子工程目录执行,但切换当前目录会影响Gradle脚本中的相对路径,不推荐。

cd app
gradle tasks
4.6.5 Task参数、查看Task详细信息

Task可以定义参数,可在执行时从命令行传入。例如Gradle内建了一个叫“help”的Task,带有一个--task参数,可以用于查看一个Task的详细信息。

查看“tasks”这个Task的详细信息,就可以执行命令如下。其中会显示一个Task的名称、类型、参数、详细介绍、分组等。

$ ./gradlew help --task tasks
:help
Detailed task information for tasks

Path
     :tasks

Type
     TaskReportTask (org.gradle.api.tasks.diagnostics.TaskReportTask)

Options
     --all     Show additional tasks and detail.

Description
     Displays the tasks runnable from root project 'GradleStudy' (some of the displayed tasks may belong to subprojects).

Group
     help

BUILD SUCCESSFUL

Total time: 1.051 secs
4.6.6 一些常用GradleTask
  • clean: 清除build目录编译生成的文件 (Deletes the build directory.)

  • assemble:编译工程 (Assembles the outputs of this project. [jar])

  • build:编译并测试工程 (Assembles and tests this project. [assemble, check])

  • test:单元测试等 (Runs the unit tests. [classes, testClasses])

  • check:测试工程,包含test (Runs all checks. [test])

  • wrapper:生成GradleWrapper文件 (Generates Gradle wrapper files. [incubating])

  • help: 帮助信息 (Displays a help message.)

  • tasks:查看Project的所有Task (Displays the tasks runnable from root project ‘Xxx’.)

  • dependencies:查看Project的依赖 (Displays all dependencies declared in root project ‘Xxx’.)

  • projects: 查看SubProject (Displays the sub-projects of root project ‘Xxx’.)

4.6.7 查看Task依赖树

每个Task都会依赖若干Task,这些Task又会依赖别的Task,所有Task就会形成一个依赖树。

为了更加直观的学习,可以在app/build.gradle中添加如下的简单脚本,让Gradle输出Task的依赖树。

方法printDependencies通过递归的方式,输出每个Task依赖的Task。afterEvaluate语句块中,先找到assembleDebug这个Task,然后调用printDependencies输出其依赖树。由于Android中有大量Task依赖,打印出所有Task需要很久,所以这里限制了最大递归深度为3。

def printDependencies(Task task, String prefix, int depth, int maxDepth) {
    println prefix + task.project.name + ':' + task.name
    def tasks = task.getTaskDependencies().getDependencies(task)
    if (depth < maxDepth - 1) {
        tasks.each { t ->
            printDependencies(t, prefix + '\t', depth + 1, maxDepth)
        }
    } else {
        if (tasks.size() > 0) {
            println prefix + '\t' + "(${tasks.size()} child tasks...)"
        }
    }
}

afterEvaluate {
    println '==============================='
    def buildTask = tasks.findByName('assembleDebug')
    printDependencies(buildTask, '', 0, 3)
    println '==============================='
}

五、Gradle Plugin (插件)

一个 Project 到底包含多少个 Task,在很大程度上依赖于编译脚本指定的插件。插件定义了一系列基本的 Task,并指定了这些 Task 的执行顺序。

整体来看,Gradle 作为一个框架,负责定义通用的流程和规则;根据不同的需求,实际的编译工作则通过具体的插件来完成。

自定义 Gradle 插件的本质就是把逻辑独立的代码进行抽取和封装,以便于我们更高效地通过插件依赖这一方式进行功能复用。而在 Android 下的 gradle 插件共分为两大类:

  • 脚本插件:同普通的 gradle 脚本编写形式一样,通过 apply from: ‘test.gradle’ 引用。
  • 对象插件:通过插件全路径类名或 id 引用。

5.1 脚本插件

脚本插件就是一个普通的 gradle 构建脚本,我们既可以写在 build.gradle 里面,也可以自己新建一个 gradle 脚本文件进行编写。如果是新建一个 gradle 脚本,则需要通过 apply from 引用。

//test.gradle
project.task("showConfig") {
    doLast {
        println("$project.name:showConfig")
    }
}
 
//build.gradle
apply from: '../test.gradle'

5.2 对象插件

对象插件是指实现了 org.gradle.api.Plugin 接口的类。Plugin 接口需要实现 void apply(T target) 这个方法。该方法中的泛型指的是此 Plugin 可以应用到的对象,而我们通常是将其应用到 Project 对象上。编写对象插件主要有三种方式:

  • 直接在gradle脚本文件中
  • 在buildSrc目录下
  • 在独立的项目下
5.2.1 在gradle脚本文件中

直接在 gradle 脚本中编写这个方式是最为简单的。打开 app.gradle 文件,在其中编写一个类实现 Plugin 接口:

//app.gradle
class CustomPluginInBuildGradle implements Plugin<Project> {
    @Override
    void apply(Project target) {
       target.task('showCustomPluginInBuildGradle'){
            doLast {
                println("task in CustomPluginInBuildGradle")
            }
        }
    }
}

然后通过插件类名引用它:

//app.gradle
apply plugin: CustomPluginInBuildGradle
5.2.2 在buildSrc目录下

除了直接写在某个模块的构建脚本中,我们还可以将插件写在工程根目录下的 buildSrc 目录下,这样可以在多个模块之间复用该插件。虽然 buildSrc 是 Gradle 在项目中配置自定义插件的默认目录,但它并不是标准的 Android 工程目录,所以使用这种方式需要我们事先手动新建一个 java library module,该 module 必须命名为 buildSrc。将 src/main/java 改成 src/main/groovy。

在 buildSrc 目录下新建一个 build.gradle,并在 build.gradle 中引用 groovy 插件:

apply plugin: 'groovy'
 
dependencies {
    compile gradleApi()
    compile localGroovy()
}

然后新建一个 xxxPlugin.groovy 并实现 Plugin 接口,例如:

package com.lerendan.buildsrc
 
import org.gradle.api.Plugin
import org.gradle.api.Project
 
class TestPlugin implements Plugin<Project> {
  @Override
  void apply(Project project) {
    project.task('pluginTest') {
      doLast {
        println 'Hello World'
      }
    }
  }
}

可以看到,上述 plugin 仅是在 apply() 方法内部创建了一个名为 pluginTest 的 task。由于 buildSrc 目录是 gradle 默认的目录之一,该目录下的代码会在构建是自动编译打包,并被添加到 buildScript 中的 classpath 下,所以不需要任何额外的配置,就可以直接被其他模块的构建脚本所引用。

通过类名引用插件的需要使用全限定名,也就是需要带上包名,或者可以先导入这个插件类,如下:

//app.gradle
apply plugin: com.lerendan.buildsrc.TestPlugin

注意这里引用的方式可以是通过类名引用,也可以通过给插件映射一个 id,然后通过 id 引用。通过简单的 id 的方式,我们可以隐藏类名等细节,使的引用更加容易。映射方式很简单,在 buildSrc目录下创建 resources/META-INF/gradle-plugins/xxx.properties,这里的 xxx 也就是所映射的 id,这里我们假设取名myplugin。myplugin.properties 文件中配置该 id 所对应的 plugin 实现类:

implementation-class=com.lerendan.buildsrc.TestPlugin

此时就可以通过 id 来引用对象的插件了:

//app.gradle
apply plugin: 'myplugin'
5.2.3 在独立工程下

在 buildSrc 下创建的 plugin 只能在该工程下的多个模块之间复用代码。如果想要在多个项目之间复用这个插件,我们就需要在一个单独的工程中编写插件,将编译后的 jar 包上传到 maven 仓库。这里为了不增加复杂度,我们还是在该工程下创建一个 standaloneplugin 模块(java library module)。只需要明白我们完全可以在一个独立的工程下来编写插件。我们看下创建好后的目录结构:

Gradle详解_第2张图片

从目录结构来看,和 buildSrc 目录是一致的。区别在于 buildSrc 下的代码在构建时会自动编译并被引用。而我们在独立项目中编写的插件如果要能正确的被引用到,需要上传到 maven 仓库中,然后显式地在需要引用的项目中的 buildSrcipt 中添加对该构件的依赖。

接下来我们来完成插件代码:

package com.lerendan.aloneplugin
 
import org.gradle.api.Plugin
import org.gradle.api.Project
 
class AlonePlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
        project.task('showAlonePlugin') {
            doLast {
                println('task in AlonePlugin')
            }
        }
    }
}

在 alonePlugin 目录下新建一个 build.gradle,并在 build.gradle 中引用插件项目构建脚本:

//alonePlugin build.gradle
apply plugin: 'groovy'
apply plugin: 'maven'
dependencies {
    compile gradleApi()
    compile localGroovy()
}
group = 'com.lerendan.aloneplugin'
version = '1.0.0'
uploadArchives {
    repositories {
        mavenDeployer {
            repository(url: uri('../repo'))
        }
    }
}

这里与 buildSrc 不同的是,我们引用了 apply plugin ‘maven’,通过 maven 插件,我们可以轻松的配置 group,version 以及 uploadArchives 的相关属性,然后执行 gradlew uploadArchives 这个任务,就可以将构件打包后上传到 maven 仓库了。同样为了示例简单,我们上传到一个本地仓库 repository(url: uri(’…/repo’)) 中。

上传之后就可以在项目根目录下找到 repo 这个目录了。最后我们通过给根目录下的 build.gradle 配置 buildScript 的 classpath,就可以引用这个插件了。注意,classpath 的格式为 group:artifact:version。

//根目录下 build.gradle
buildscript {
    repositories {
        maven {
            url uri('repo')
        }
        jcenter()
        google()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.3.2'
        classpath 'com.lerendan.aloneplugin:alonePlugin:1.0.0'
    }
}

引用插件:

//app.gradle
apply plugin: 'testAlonePlugin'

5.3 对象插件中自定义 Extension 与 Task

5.3.1 自定义 Extension

下面我们来看下自定义 Extension 的步骤,首先创建一个实体类:

//ReleaseInfoExtension.groovy
package com.lerendan.buildsrc
 
class ReleaseInfoExtension{
    String versionName
    String versionCode
    String versionInfo
 
    @Override
    String toString() {
        return "versionName = $versionName , versionCode = $versionCode , versionInfo = $versionInfo"
    }
}

接着我们在 buildSrc 中创建一个 Gradle 插件:

//TestPlugin.groovy
package com.lerendan.buildsrc
 
import org.gradle.api.Plugin
import org.gradle.api.Project
 
class TestPlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
        project.extensions.create("releaseInfo", 
                                  ReleaseInfoExtension.class)
 
        project.task('pluginTest') {
            doLast {
                ReleaseInfoExtension releaseInfo = project.releaseInfo
                println releaseInfo
            }
        }
    }
}

可以看到,我们通过 project.extensions.create 创建了这样一个自定义的 Extension。接着我们在 app module 的 build.gradle 中引入这个 plugin,就可以在 app moudle 的 build.gradle 脚本中使用 releaseInfo 去配置扩展属性,代码如下所示:

//app module 的 build.gradle
apply plugin: com.lerendan.buildsrc.TestPlugin
 
releaseInfo {
    versionCode = "1"
    versionName = "1.0.0"
    versionInfo = "第一个版本~"
}
 
......

执行这个 task :

gradle pluginTest
5.3.2 自定义 Task

使用自定义扩展属性 Extension 仅仅是为了让使用插件者有配置插件的能力。而插件还得借助自定义 Task 来实现相应的功能,必须继承 DefaultTask。这里我们创建一个 Task,其具体实现代码如下所示:

//ReleaseInfoTask.groovy
package com.lerendan.buildsrc
 
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction
 
class ReleaseInfoTask extends DefaultTask {
 
    ReleaseInfoTask() {
        // 1、在构造器中配置了该 Task 对应的 Task group,即 Task 组,并为其添加上了对应的描述信息。        
        group = 'version_manager'
        description = 'release info update'
    }
 
    // 2、在 gradle 执行阶段执行    
    @TaskAction
    void doAction() {
        updateVersionInfo()
    }
 
    private void updateVersionInfo() {
        // 3、从 realeaseInfo Extension 属性中获取相应的版本信息
        ReleaseInfoExtension releaseInfoExtension = 
          project.extensions.releaseInfo
        println releaseInfoExtension
 
    }
}

首先我们在构造器中配置了该 Task 对应的 Task group,即 Task 组,并为其添加上了对应的描述信息。接着,我们使用了 @TaskAction 注解标注了 doAction 方法,这样这个方法就会在 gradle 执行阶段执行。最后可以使用 project.extensions.releaseInfo 从 realeaseInfo Extension 属性中了获取相应的配置信息。

可以看到,自定义的插件 task 都会遵循这个步骤,当然,最后别忘了在我们的 TestPlugin 的 apply 方法中加入下面代码去创建 ReleaseInfoTask 实例,代码如下所示:

//TestPlugin.groovy
package com.lerendan.buildsrc
 
import org.gradle.api.Plugin
import org.gradle.api.Project
 
class TestPlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
        // 创建extension
        project.extensions.create("releaseInfo", ReleaseInfoExtension.class)
        // 创建task
        project.tasks.create("releaseInfoTask", ReleaseInfoTask.class)
    }
}

执行 releaseInfoTask 这个 task:

gradle releaseInfoTask

六、Variants(变体)

6.1 Variants 是什么

要理解 Variants 的作用,就必须先了解 buildType、flavor、dimension 与 variant 之间的关系。在 android gradle plugin V3.x 之后,每个 flavor 必须对应一个 dimension,可以理解为 flavor 的分组,然后不同 dimension 里的 flavor 会组合成一个 variant。示例代码如下所示:

    android {
        ...
        defaultConfig {...}
        //gradle 默认就有 debug 和 release 两个 buildType
        buildTypes {
            debug{...}
            release{...}
        }
 
        flavorDimensions "version"
        productFlavors {
            demo {
                dimension "version"
            }
            full {
                dimension "version"
            }
        }
    }

根据上述配置 Gradle 会创建以下构建变体:

  • demoDebug
  • demoRelease
  • fullDebug
  • fullRelease

在 Android 对 Gradle 插件的扩展支持之中,其中最常用的便是利用变体(Variants)来对构建过程中的各个默认的 task 进行 hook。关于 Variants 共有 三种类型,如下所示:

  • applicationVariants:只适用于 app plugin。
  • libraryVariants:只适用于 library plugin。
  • testVariants:在 app plugin 与 libarary plugin 中都适用。

6.2 Variants 的使用

我们来看看 applicationVariants 的使用,首先我们在 app.gradle 中配置 buildTypes、flavorDimensions、productFlavors 同上。然后,我们可以 使用 applicationVariants.all 在配置阶段之后去获取所有 variant 的 name 与 baseName。代码如下所示:

flavorDimensions "version"
productFlavors{
  demo{
    dimension "version"
  }
  full{
    dimension "version"
  }
}
this.afterEvaluate{
  this.android.applicationVariants.all{ variant ->
    println "name: $variant.name,baseName: $variant.baseName"
  }
}

最后我们来执行下 gradle clean 任务:

可以看到,name 与 baseName 的区别:demoDebug 与 demo-debug 。

接下来我们来看看使用 applicationVariants.all 在配置阶段之后去修改输出的 APK 名称:

this.afterEvaluate{
  applicationVariants.all { variant ->
    variant.outputs.all{
      outputFileName = "app"+variant.versionName+variant.buildType.name
      +"_${variant.productFlavors[0].name}"+"_${releaseTime()}.apk"
    }
  }
}

可以看到,我们上面用到了一个 releaseTime() 方法获取当前时间:

def static releaseTime(){
  return new Date().format("MMddHHmm",TimeZone.getTimeZone("GMT+8"))
}

最后我们来执行以下 gradle clean:

>Configure project:app
app1.0debug_demo_05090927.apk
app1.0release_demo_05090927.apk
...

可以看到正常修改了 apk 的名称。

最后我们来看一下如何对 applicationVariants 中的 Task 进行 Hook,我们可以在 android.applicationVariants.all 的闭包中通过 variant.task 来获取相应的 Task。代码如下所示:

this.afterEvaluate{
    applicationVariants.all{variant ->
        def task = variant.checkManifestProvider
        println task.name
    }
}

然后,执行 gradle clean,其输出信息如下所示:既然可以获取到变体中的 Task,我们就可以根据不同的 Task 类型来做特殊处理。例如,我们可以利用 variants 去解决插件化开发中的痛点:编写一个对插件化项目中的各个插件自动更新的脚本,其核心代码如下所示:

this.afterEvaluate{
    this.android.applicationVariants.all{ variant ->
        def checkTask = variant.checkManifest
        checkTask.doFirst{
            def bt = variant.buildType.name
            if(bt=='qa' || bt=='preview' || bt== 'release'){
                update_plugin(bt)
            }
        }
        
    }
}

至于 update_plugin 的实现,主要就是一些插件安全校验与下载的逻辑,这部分其实跟 Gradle 没有什么联系。

variant 中能获取到哪些 task 我们可以去 ApplicationVariant 的父类 BaseVariant 中去查看

七、Transform

Google 官方在 Android Gradle V1.5.0 版本以后提供了 Transfrom API,允许第三方 Plugin 在打包成 .dex 文件之前的编译过程中操作 .class 文件,我们需要做的就是实现 Transform 来对 .class 文件遍历以拿到所有方法,修改完成后再对原文件进行替换即可。总的来说,Gradle Transform 的功能就是把输入的 .class 文件转换为目标字节码文件。我们可以通过 Gradle Plugin 来注册我们编写的 Transform。注册后的 Transform 会被 Gradle 包装成一个 Gradle Task,这个 TransForm Task 会在 java compile Task 执行完毕后运行。

7.1 Transform 的使用

下面我们来看看如何使用 Transform,首先如果是在 buildSrc 中,由于 buildSrc 的执行时机要早于任何一个 project,因此需要添加仓库:

apply plugin: 'groovy'
repositories{
    jcenter()
    google()
}
dependencies{
    compile gradleApi()
    compile localGroovy()
    compile 'com.android.tools.build:gradle:3.4.0'
}

然后,创建一个 Transform 的子类继承自 com.android.build.api.transform.Transform:

class TransformTest extends Transform{
    @Override
    String getName(){
        return "MyCustomTransform"
    }
    @Override
    Set<QualifiedContent.ContentType> getInputTypes(){
        return TransformManager.CONTENT_CLASS
    }
    @Override
    Set<? super QualifiedContent.Scope> getScopes(){
        return TransformManager.SCOPE_FULL_PROJECT
    }
    @Override
    boolean isIncremental(){
        return false
    }
    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException ,InterruptedException,IOException{
        super.transform(transformInvocation)
    }
}

可以看到其创建步骤可以细分为五步,如下所示:

7.1.1、getName()

指定自定义 Transform 的名称。返回对应的 Task 名称。

7.1.2、getInputTypes()

可以看到这个方法返回的是一个 Set 集合,指明你自定义的这个 Transform 处理的输入类型集合。QualifiedContent.ContentType 是一个接口,它的实现类有 DefaultContentType 和 ExtendedContentType。为了方便 TransformManager 为我们封装了以下几种输入类型集合:

public static final Set<ContentType> CONTENT_CLASS = ImmutableSet.of(CLASSES);
public static final Set<ContentType> CONTENT_JARS = ImmutableSet.of(CLASSES,RESOURCES);
public static final Set<ContentType> CONTENT_RESOURCES = ImmutableSet.of(RESOURCES);
public static final Set<ContentType> CONTENT_NATIVE_LIBS = ImmutableSet.of(NATIVE_LIBS);
public static final Set<ContentType> CONTENT_DEX = 
    ImmutableSet.of(ExtendedContentType.DEX);
public static final Set<ContentType> CONTENT_DEX_WITH_RESOURCES = 
    ImmutableSet.of(ExtendedContentType.DEX,RESOURCE);

分别代表的是:

  • CONTENT_CLASS:表示需要处理 java 的 class 文件。
  • CONTENT_JARS:表示需要处理 java 的 class 与 资源文件。
  • CONTENT_RESOURCES:表示需要处理 java 的资源文件。
  • CONTENT_NATIVE_LIBS:表示需要处理 native 库的代码。
  • CONTENT_DEX:表示需要处理 DEX 文件。
  • CONTENT_DEX_WITH_RESOURCES:表示需要处理 DEX 与 java 的资源文件。
7.1.3、getScopes()

可以看到这个方法返回的是一个 Set 集合,用来指明自定义的 Transform 的输入文件所属的范围,这是因为 gradle 是支持多工程编译的。Scope 是一个枚举类:

enum Scope implements ScopeType{
	PROJECT(0x01),
    SUB_PROJECTS(0x04),
    EXTERNAL_LIBRARIES(0x10),
   	TESTED_CODE(0x20),
    PROVIDED_ONLY(0x40),
    @Deprecated
    PROJECT_LOCAL_DEPS(0x02),
    @Deprecated
    SUB_PROJECTS_LOCAL_DEPS(0x08);
}

可以看到目前有 5 种基本类型,分别代表的是:

  • PROJECT:只有项目内容。
  • SUB_PROJECTS:只有子项目。
  • EXTERNAL_LIBRARIES:只有外部库,
  • TESTED_CODE:由当前变体(包括依赖项)所测试的代码。
  • PROVIDED_ONLY:只提供本地或远程依赖项。

同样,为了方便,TransformManager 为我们封装了 getScope 的返回:

public static final Set<ScopeType> PROJECT_ONLY = ImmutableSet.of(Scope.PROJECT);
public static final Set<Scope> SCOPE_FULL_PROJECT = 
    Sets.immutableEnumSet(Scope.PROJECT,Scope.SUB_PROJECTS,Scope.EXTERNAL_LIBRARIES);
public static final Set<ScopeType> SCOPE_FULL_WITH_IR_FOR_DEXING = 
    new ImmutableSet.Builder<ScopeType>().addAll(SCOPE_FULL_PROJECT)
		.add(InternalScope.MAIN_SPLIT).build();
public static final Set<ScopeType> SCOPE_FULL_WITH_FEATURES = 
    new ImmutableSet.Builder<ScopeType>().addAll(SCOPE_FULL_PROJECT)
		.add(InternalScope.FEATURES).build();
public static final Set<ScopeType> SCOPE_FULL_WITH_IR_AND_FEATURES = 
    new ImmutableSet.Builder<ScopeType>().addAll(SCOPE_FULL_PROJECT)
		.add(InternalScope.MAIN_SPLIT).add(InternalScope.FEATURES).build();
public static final Set<ScopeType> SCOPE_FEATURES = 
    ImmutableSet.of(InternalScope.FEATURES);
public static final Set<ScopeType> SCOPE_FULL_LIBRARY_WITH_LOCAL_JARS =
    ImmutableSet.of(Scope.PROJECT,InternalScope.LOCAL_JARS);
public static final Set<ScopeType> SCOPE_IR_FOR_SLICING = 
    ImmutableSet.of(Scope.PROJECT,Scope.SUB_PROJECTS);

如果一个 Transform 不想处理任何输入,只是想查看输入的内容,那么只需在 getScopes() 返回一个空集合,然后在getReferencedScopes() 返回想要接收的范围。

public Set<? super Scope> getReferencedScopes() { 
    return ImmutableSet.of(); 
}
7.1.4、isIncremental()

isIncremental 方法用于确定是否支持增量更新,如果返回 true,TransformInput 会包含一份修改的文件列表,如果返回 false,则会进行全量编译,并且会删除上一次的输出内容。

7.1.5、transform(TransformInvocation transformInvocation)

它是 Transform 的关键方法,在 transform() 方法中,就是用来给我们进行具体的输入输出转换过程的。它是一个空实现,input 的内容将会打包成一个 TransformInvocation 对象,因为我们要想使用 input,我们需要详细了解一下 TransformInvocation 参数。

public interface TransformInvocation {
 
    // 输入作为 TransformInput 返回
    Collection<TransformInput> getInputs(); 
 
    //TransformOutputProvider 可以用来创建输出内容
    TransformOutputProvider getOutputProvider(); 
 
    boolean isIncremental();
...
}

TransformInput 可认为是所有输入文件的一个抽象,它主要包括两个部分,如下所示:

public interface TransformInput {
    Collection<JarInput> getJarInputs();
    Collection<DirectoryInput> getDirectoryInputs();
}
 
public interface JarInput extends QualifiedContent {
 
    File getFile(); //jar文件
 
    Set<ContentType> getContentTypes(); // 是class还是resource
 
    Set<? super Scope> getScopes();  //属于Scope:
}

其中:

  • DirectoryInput 集合:表示以源码方式参与项目编译的所有目录结构与其目录下的源码文件。
  • JarInput 集合:表示以 jar 包方式参与项目编译的所有本地 jar 包和远程 jar 包。需要注意的是,这个 jar 所指也包括 aar。
  • TransformOutputProvider 表示 Transform 的输出,利用它我们可以获取输出路径等信息。
public interface TransformOutputProvider {
    //根据 name、ContentType、QualifiedContent.Scope返回对应的文件( jar / directory)
    File getContentLocation(String name, Set<QualifiedContent.ContentType> types, 
        Set<? super QualifiedContent.Scope> scopes, Format format);
}

即我们可以通过 TransformInvocation 来获取输入,同时也获得了输出的功能。举个例子:

    @Override
void transform(TransformInvocation transformInvocation) throws TransformException, 
    InterruptedException, IOException {
    super.transform(transformInvocation)
    println '--------------- MyCustomTransform visit start --------------- '
    def startTime = System.currentTimeMillis()
    def inputs = transformInvocation.inputs
    def outputProvider = transformInvocation.outputProvider
    // 1、删除之前的输出    
    if (outputProvider != null) {
        outputProvider.deleteAll()
    }
 
    // Transform 的 inputs 有两种类型,一种是目录,一种是 jar包,要分开遍历    
    inputs.each { TransformInput input ->
        // 2、遍历 directoryInputs(本地 project 编译成的多个 class⽂件存放的目录)        
        input.directoryInputs.each { DirectoryInput directoryInput ->
            handleDirectory(directoryInput, outputProvider)
        }
        // 3、遍历 jarInputs(各个依赖所编译成的 jar 文件)        
        input.jarInputs.each { JarInput jarInput ->
            handleJar(jarInput, outputProvider)
        }
    }
    def cost = (System.currentTimeMillis() - startTime) / 1000
    println '--------------- MyCustomTransform visit end --------------- ' p
    rintln "MyCustomTransform cost : $cost s"
}

这里我们主要是做了三步处理,如下所示:

  • 删除之前的输出。
  • 遍历 directoryInputs(本地 project 编译成的多个 class ⽂件存放的目录)。
  • 遍历 jarInputs(各个依赖所编译成的 jar 文件)。

在 handleDirectory 与 handleJar 方法中则是进行了相应的 文件处理 && ASM 字节码修改。

编写完 Transform 的代码之后,我们就可以在 Plugin 的 apply 方法中加入下面代码去注册 TransformTest 的实例,代码如下所示:

class PluginTest implements Plugin<Project>{
	@Override
    void apply(Project project){
        def appExtension = project.extensions.findByType(AppExtension.class)
        appExtension.registerTransform(new TransformTest())
    }
}

八、常用名词解释

8.1 Gradle Wrapper

用IDEA/Android Studio创建基于Gradle的工程时,默认会在工程根目录创建GradleWrapper,包括gradlew可执行脚本和gradle/wrapper文件夹,其中指定了和工程配套的gradle版本。

在工程根目录下直接执行./gradlew,会自动将参数传给wrapper指定版本的gradle,执行对应的命令;如果本机还没有该版本的gradle,则会先自动下载。

工程配置和Gradle版本通常需要对应,不正确的Gradle版本可能无法正常编译工程,因此推荐使用GradleWrapper执行Gradle命令。

gradle/wrapper/gradle-wrapper.properties文件,指定了gradle版本、下载地址、下载的文件存放位置(Mac系统中默认在~/.gradle/wrapper/dists目录)。此文件内容示例:

distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip

在Android Studio/IDEA中,可通过Preferences – Build, Execution, Deployment – Gradle,设置Project-level settings中Use default gradle wrapper,指定Android Studio使用工程配置的Gradle Wrapper。

8.2 Project、RootProject、SubProject (Module)

Project是Gradle中的基本概念之一,即一个工程。一个工程可以包含多个SubProject,也称为Module,最顶层的工程也称为RootProject。

一个标准的Android工程,文件结构如下。

  • 每个build.gradle对应一个Project,最外层的是RootProject,里面的app是SubProject。
  • settings.gradle不是必须的,一般在包含子工程时就需要用这个文件指定,即我们通常所见的include ':app'脚本。
  • 这里的':app'就是子工程的名字,通常和文件夹名称对应。
settings.gradle
build.gradle
app
    build.gradle

8.3 StartParameter

Gradle执行时有一些称为StartParameter的参数,StartParameter可在命令行设置,可通过gradle --help查看。例如:

  1. --quiet,执行过程中,只显示Error级别的Log。
  2. --stacktrace,执行过程中,输出所有Exception的stacktrace。
  3. --full-stacktrace,执行过程中,输出所有Exception的完整stacktrace。
  4. --no-daemon,不使用Deamon。Deamon是用于加速Gradle执行的后台进程,有些情况下使用Deamon会有问题(可参考 https://docs.gradle.org/current/userguide/gradle_daemon.html)
  5. --offline,离线模式,不使用网络资源。

在命令行可通过-P参数传入projectProperties,并在Gradle脚本中获取

# 命令行传入projectProperties
./gradlew clean -Pkey=value
// gradle脚本中获取projectProperties
print gradle.startParameter.projectProperties.get('key')

还可以通过-D参数传入systemPropertiesArgs,并在Gradle脚本中获取

# 命令行传入systemPropertiesArgs
./gradlew clean -Dkey=value
// gradle脚本中获取systemPropertiesArgs
print gradle.startParameter.systemPropertiesArgs.get('key')

8.4 gradle.properties

Properties文件格式可由java.util.Properties解析,包含若干键值对,类似HashMap

Gradle运行时会自动读取gradle.properties文件并引用其中的属性。有多个位置可以放gradle.properties文件,按优先级从低到高如下:

  • Project所在目录(即build.gradle所在目录),包括RootProject和SubProject
  • GradleHome目录(Mac中默认为~/gradle
  • 通过gradle命令行-D参数指定的Property

在gradle.properties文件中,一些保留Key可用于配置Gradle运行环境,例如org.gradle.daemon用于设置GradleDeamon,org.gradle.logging.level用于设置Gradle的Log级别等。

除了保留Key以外,其他Key都可以作为变量用于配置工程。例如在Project目录的gradle.properties中统一定义Support包的版本号,然后在build.gradle中使用定义的变量如下。

SUPPORT_LIBRARY_VERSION=23.4.0
dependencies {
    implementation "com.android.support:support-v4:${SUPPORT_LIBRARY_VERSION}"
    implementation "com.android.support:appcompat-v7:${SUPPORT_LIBRARY_VERSION}"
    implementation "com.android.support:design:${SUPPORT_LIBRARY_VERSION}"
    implementation "com.android.support:recyclerview-v7:${SUPPORT_LIBRARY_VERSION}"
}

8.5 buildscript与allprojects

在RootProject的build.gradle中,经常会看到buildscript和allprojects两个语句块,并且里面都定义了一些相同的东西。

buildscript,顾名思义,是编译脚本,也就是说编译一个工程时需要的配置,例如常会看到下面这样的脚本,表示编译时要用到Android Gradle Plugin。

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.3'
    }
}

allprojects,则用于配置所有project,包括SubProject,这里面的配置的东西,则是工程代码需要的东西,例如依赖的各种开源库等。

allprojects {
    repositories {
        jcenter()
    }
}

8.6 Repositories

Gradle的依赖管理完全兼容Maven和Ivy,常使用Maven仓库来实现依赖管理,当Library打包完成后上传到Maven仓库,Gradle则会从Maven仓库下载需要的依赖。

Repository就是用来指定所需要的Maven仓库。除了常见的jcenter(),mavenCentral(),还可以指定本地搭建的Maven仓库、指定URL的Maven仓库等,例如国内一些Maven仓库镜像,以及很多公司内部私有的Maven仓库等。

repositories {
    jcenter()
    mavenCentral()
    maven { url 'http://maven.oschina.net/content/groups/public/' }
    ivy { url "http://repo.mycompany.com/repo" }
    localRepository { dirs 'lib' }
    maven {
        url "sftp://repo.mycompany.com:22/maven2"
        credentials {
            username 'user'
            password 'password'
        }
    }
}

8.7 Dependencies

Gradle依赖管理官方文档
https://docs.gradle.org/current/userguide/dependency_management.html

DependencyNotation

DependencyNotation用于描述要依赖的模块。

8.7.1 外部依赖

其中group通常用包名,name表示实际的名字,version表示版本,classifier在Android中是ProductFlavor和BuildType的组合(后面会介绍),ext则表示扩展名。

implementation 'androidx.appcompat:appcompat:1.1.0'

implementation group: 'androidx.appcompat', name: 'appcompat', version: '1.1.0', 
8.7.2 Project依赖
implementation project(':someProject')
8.7.3 文件依赖
dependencies {
  implementation files('hibernate.jar', 'libs/spring.jar')
  implementation fileTree(dir: "libs", include: ["*.jar"])
}
8.7.4 依赖传递(transitive)

Gradle依赖项可配置transitive属性,表示是否递归解析此模块的依赖项,默认为true。

implementation('org.hibernate:hibernate:3.0.5') {
    transitive = true
}
8.7.5 依赖树查看

每个模块都会依赖若干模块,这些模块又分别依赖其他模块,形成一个依赖树。Gradle提供了名为dependencies的Task,可查看Project的依赖树。

$ ./gradlew :app:dependencies
8.7.6 依赖冲突分解

依赖项很多时,依赖项之间经常会发生冲突。例如多个SDK分别依赖了不同版本的AppCompat,就可能导致冲突。Gradle提供了一些API可以用来处理依赖冲突。

常见的依赖冲突解决思路可参考:

Gradle依赖项学习总结,dependencies、transitive、force、exclude的使用与依赖冲突解决
https://www.paincker.com/gradle-dependencies

8.8 ProductFlavor、BuildType与Build Variant

Android中定义了ProductFlavor和BuildType的DSL。

8.8.1 ProductFlavor

ProductFlavor可以实现一套代码编译成不同的版本,版本之间差异比较小,例如开发版本、测试版本、线上版本;或是发布到某些应用市场的定制版本(例如需要修改一些资源文件)等。

ProductFlavor中包含了一些应用相关的配置,例如minSdkVersion,versionCode等。下面的代码,就是在对默认的ProductFlavor做配置。

android {
    defaultConfig {
        applicationId "com.paincker.gradle.demoapplication"
        minSdkVersion 15
        targetSdkVersion 24
        versionCode 1
        versionName "1.0"
    }
}

可以在ProductFlavors中定义新的Flavor并进行配置,覆盖DefaultProductFlavor中的相应配置。

android {
    productFlavors {
        myflavor {
            minSdkVersion 20
        }
    }
}
8.8.2 BuildType

BuildType本身是软件开发中的通用概念,表示编译版本。

Android中定义了自己的BuildType接口,其中包含了一些编译相关的配置,例如debuggable(是否可调试)、minifyEnable(是否开启Proguard)等。

可以在buildTypes中配置支持的BuildType如下。即使不做任何配置,默认也会有Debug和Release两个BuildType,且分别包含了一套默认值,例如Debug的debuggable参数默认为true,而Release的debuggable参数默认为false。

android {
    buildTypes {
    	debug {
        }
    	develop {
        	debuggable false
            minifyEnabled false
        }
        release {
        	debuggable false
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

8.9 BuildConfig

Android开发常用到BuildConfig类,这个类可在编译时生成,用于在代码中获取一些编译相关的参数,包括是否可以Debug、当前BuildType和Flavor名字、VersionCode和VersionName等,例如常会用BuildConfig.DEBUG判断是否输出Log信息。

BuildType配置中提供了一个buildConfigField方法,可以往BuildConfig中添加自定义字段。

int xxLibraryVersion = 192
android {
	buildTypes {
    	debug {
        	buildConfigField "int", "XX_LIBRARY_VERSION", xxLibraryVersion
        }
        release {
        	buildConfigField "int", "XX_LIBRARY_VERSION", xxLibraryVersion
        }
    }
}
dependencies {
	implementation "com.xxx:xx:1.0.${xxLibraryVersion}"
}
public final class BuildConfig {
  public static final boolean DEBUG = Boolean.parseBoolean("true");
  public static final String BUILD_TYPE = "debug";
  // Fields from build type: debug
  public static final int XX_LIBRARY_VERSION = 192;
}

private void showLibraryInfo() {
	String msg = "xx library version is " + BuildConfig.XX_LIBRARY_VERSION;
    Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
}

8.10 Configurations

在Gradle中,一个Project可以有多个Configuration,每个Configuration有不同的依赖配置。

例如在dependencies中,经常用到implementation xxx,这里的implementation就是一个Configuration。

8.11 SourceSet

Gradle中使用SourceSet管理Java源码;Android定义了自己的SourceSet,其用法和Gradle类似。

默认的SourceSet即main,根目录位于src/main,其中又包括多个子目录,例如:

  • src/main/java,Java源码
  • src/main/resources,Java资源文件
  • src/main/res,Android资源文件
  • src/main/assets,Android assets文件

ProductFlavor、BuildType、BuildVariant、test/androidTest,也会按照一定的形式组合产生SourceSet。

可以配置某个SourceSet的根目录,或者指定具体的子目录(支持多个目录),如下:

android {
	sourceSets {
    	myflavor {
            res.srcDirs = ['src/main/res', 'src/main/res2']
        }
        debug.setRoot('src/main')
    }
}

使用Gradle的sourceSet可以查看项目中所有的SourceSet。

$ ./gradlew :app:sourceSets

8.12 AAR多版本发布

Android开发经常会用到AAR,有时候希望AAR能支持发布多个版本,并在不同的情况下依赖不同版本(包括不同的BuildType和ProductFlavor)。例如主工程依赖xxLibrary,希望Debug版本APK依赖Debug版本的AAR,而Release版本APK依赖Release版本的AAR。

在Android中依赖SubProject或AAR时,如果没有特殊配置,AAR的发布和模块依赖默认均为Release版本。

实际尝试主工程依赖子工程,子工程中读取BuildConfig.DEBUG的值始终是false,修改Android Studio中子模块的BuildVariant配置也没有效果。

dependencies {
	implementation project(':xxx_library')
	implementation 'com.xxx:xxx:1.0.5@aar' {
    	transitive = true
    }
}

可用如下方式配置子模块或者独立AAR工程发布所有版本:

apply plugin: 'com.android.library'

android {
	// 发布非默认版本
    publishNonDefault true
    // 指定默认版本,发布AAR到Maven时会从默认版本生成POM依赖配置。
    // 不指定会导致POM无法正确生成,从而依赖不能传递。
    defaultPublishConfig "falvorARelease"

    productFlavors {
        flavorA { }
        flavorB { }
    }
}

用下面的方式依赖子模块或已经发布的AAR:

dependencies {

	// Debug、Release版本APK,分别依赖Debug、Release版本子模块
    debugCompile project(path: ':xxx', configuration: 'flavorADebug')
    releaseCompile project(path: ':xxx', configuration: 'flavorARelease')

	// 指定依赖aar版本,并设置transitive为true
    debugCompile('com.xxx:library:1.0.5:flavorADebug@aar') {
    	transitive = true
    }
    releaseCompile('com.xxx:library:1.0.5:flavorARelease@aar') {
    	transitive = true
    }
}

Flavor、BuildType、BuildVariant、test/androidTest,也会按照一定的形式组合产生SourceSet。

可以配置某个SourceSet的根目录,或者指定具体的子目录(支持多个目录),如下:

android {
	sourceSets {
    	myflavor {
            res.srcDirs = ['src/main/res', 'src/main/res2']
        }
        debug.setRoot('src/main')
    }
}

使用Gradle的sourceSet可以查看项目中所有的SourceSet。

$ ./gradlew :app:sourceSets

8.12 AAR多版本发布

Android开发经常会用到AAR,有时候希望AAR能支持发布多个版本,并在不同的情况下依赖不同版本(包括不同的BuildType和ProductFlavor)。例如主工程依赖xxLibrary,希望Debug版本APK依赖Debug版本的AAR,而Release版本APK依赖Release版本的AAR。

在Android中依赖SubProject或AAR时,如果没有特殊配置,AAR的发布和模块依赖默认均为Release版本。

实际尝试主工程依赖子工程,子工程中读取BuildConfig.DEBUG的值始终是false,修改Android Studio中子模块的BuildVariant配置也没有效果。

dependencies {
	implementation project(':xxx_library')
	implementation 'com.xxx:xxx:1.0.5@aar' {
    	transitive = true
    }
}

可用如下方式配置子模块或者独立AAR工程发布所有版本:

apply plugin: 'com.android.library'

android {
	// 发布非默认版本
    publishNonDefault true
    // 指定默认版本,发布AAR到Maven时会从默认版本生成POM依赖配置。
    // 不指定会导致POM无法正确生成,从而依赖不能传递。
    defaultPublishConfig "falvorARelease"

    productFlavors {
        flavorA { }
        flavorB { }
    }
}

用下面的方式依赖子模块或已经发布的AAR:

dependencies {

	// Debug、Release版本APK,分别依赖Debug、Release版本子模块
    debugCompile project(path: ':xxx', configuration: 'flavorADebug')
    releaseCompile project(path: ':xxx', configuration: 'flavorARelease')

	// 指定依赖aar版本,并设置transitive为true
    debugCompile('com.xxx:library:1.0.5:flavorADebug@aar') {
    	transitive = true
    }
    releaseCompile('com.xxx:library:1.0.5:flavorARelease@aar') {
    	transitive = true
    }
}

你可能感兴趣的:(android,gradle)