Gradle完全解析
导语: Gradle是一个基于groovy语言的自动化构建工具,提供了一个自动化构建任务的框架,具体构建过程支持自定义和配置,也是android官方指定的android app构建工具。本篇文章是本人最近学习Gradle的一个总结,若有理解错误的地方望高手指出。
内容概要
gradle知识点一、groovy概要
在介绍gradle之前,先简要的聊一聊groovy语言,它是一个基于jvm的脚本语言,具有灵活而强大的语法,又和java完美兼容,能够帮助用户高效的编写java代码。gradle的构建脚本正是通过groovy语言描述的,熟悉groovy能够帮助我们学习gradle。详细的语法介绍可以参考下面几篇文章
在此只介绍几个本篇文章需要的几个groovy知识点。
1.1 groovy集合
groovy的语法的简洁性在定义集合时表现的淋漓尽致,对于最常见的List和Map,使用起来特别方便,下面代码展示了groovy这两种集合类的使用。
//中括号[]用来定义List
def list = ["Groovy", "Java", "Kotlin"]
//groovy中的List默认是java中的ArrayList
assert list instanceof ArrayList
//运算符重载,向list中添加元素,也可以调用ArrayList的相关方法操作该list
list << "Python"
//通过下标访问
assert list[1] == "Java"
//遍历list可以使用each方法,传入一个闭包,闭包的参数为list的每个元素
list.each {
println it
}
//当某一条件达到时需要终止遍历时,可以使用any方法。闭包返回true时,终止遍历。
//下列语句只会打印Groovy和Java
list.any {
println it
return it == "Java"
}
//[:]用来定义Map,键值默认时String类型
def map = [groovy:"Groovy", java:"Java", kotlin:"Kotlin"]
//groovy中的Map默认是java中的LinkedHashMap
assert map instanceof LinkedHashMap
//可以通过点运算符和中括号来访问map中的元素
assert map.java == "Java"
assert map["groovy"] == "Groovy"
//遍历map,闭包中的参数为Entry
map.each { Map.Entry entry ->
println "${entry.key}:${entry.value}"
}
上面用法只是展示list和map的基本用法,另外java中的各种集合类在groovy中都是可以使用的,并且groovy的标准库中提供了很多方便好用的函数帮助我们操作集合类,如上例中的each和any函数,具体有哪些函数和用法可以参考api文档中DefaultGroovyMethods类的方法。
1.2 groovy字符串
groovy中字符串分为两种,一种就是java中的String类,另一种是groovy定义的GString类型。GString类型允许插入表达式。定义它们的方式有很多种,如下所示
//定义普通String的方法
//其中第二种允许换行
def string = ['groovy', '''groovy''']
string.each {
assert it instanceof String
}
//定义允许插值GString的方法
//后面三种定义方式都允许换行
def cool = "so cool!!"
def gString = ["groovy ${cool}", """groovy ${cool}""", /groovy ${cool}/, $/groovy ${cool}/$]
gString.each {
assert it instanceof GString
assert it.toString() == ('groovy so cool!!')
}
//若没有插值,则仍然为String类型
def normalString = ["groovy", """groovy""", /groovy/, $/groovy/$]
normalString.each {
assert it instanceof String
}
//String和GString表示同一个字符串拥有不同的hashcode,所以不要用GString作为HashMap的键
def groovy = 'groovy'
assert 'groovy'.hashCode() != "${groovy}".hashCode()
另外插值字符串还有一个重要的特性——延迟赋值
def language = "GROOVY"
//插值是一个普通语句时,只在字符串在初始化时计算表达式
def string = "${language.toLowerCase()} is cool"
//插值是一个闭包时,每次引用该字符串都会执行闭包
def delayString = "${-> language.toLowerCase()} is cool"
assert string.toString() == "groovy is cool"
assert delayString.toString() == "groovy is cool"
//改变language值时,string值不会变化,delayString会变化
language = "JAVA"
assert string.toString() == "groovy is cool"
assert delayString.toString() == "java is cool"
groovy在编写正则匹配的程序时也相当的方便,其中/.../和$/../$这两种字符串写法就是为正则表达式量身定制的,其中包含一些特殊字符时可以不需要转义斜杠。
def string = "G32ro3o2v6y"
//定义正则表达式
def pattern = /[^A-Za-z]/
//计算匹配
def matcher = string =~ pattern
//实际上是通过java的Matcher类进行匹配的
assert matcher instanceof java.util.regex.Matcher
assert matcher.replaceAll("") == "Groovy"
1.3 文件IO与xml解析
脚本语言一般都会提供简洁方便的API用来操作文件,groovy也不例外,再配合闭包比java好用很多!下面展示了groovy中的文件操作
File testFile = project.file("test")
//读取文件
//通过eachLine函数读取文件每一行
testFile.eachLine { String line ->
println line
}
//通过BufferReader读取
testFile.withReader {
def string
while(string = it.readLine()){
println string
}
}
//写文件
//下面两种方式在文件末尾添加内容
testFile << "Groovy is so Cool!"
testFile.append("Kotlin is Cool too!")
//向文件中写入字符,该方法会覆盖掉文件之前的内容
testFile.write("Java is Cool too!")
//复制文件
File targetFile = project.file("copy_test")
targetFile.withOutputStream { os ->
testFile.withInputStream { is ->
os << is
}
}
android开发中经常会用到xml文件,比如app的清单文件AndroidManifest.xml,各种资源,布局文件等。groovy提供了简洁好用的api用来操作xml文件。下面以提取AndroidManifest中所有acvity名称为例展示groovy对xml文件的操作。
//通过XmlSlurper解析xml文件
def androidManifest = new XmlSlurper().parse(project.file("src/main/AndroidManifest.xml"))
//声明名字空间
androidManifest.declareNamespace('android':'http://schemas.android.com/apk/res/android')
//直接通过点操作符引用xml的各个节点,若该节点不止一个可以通过each遍历
androidManifest.application.activity.each { def activity ->
//通过中括号访问节点属性
println activity["@android:name"]
}
了解了基础语法和以上几点groovy知识后便可以进入gradle的学习了
二、Gradle基础
2.1 目录结构
android studio默认生成的目录结构中和gradle相关的文件如下
project root directory
|——gradle
├── wrapper
└── gradle-wrapper.properties
├──app
├── src
│ └── build.gradle
|—— build.gradle
|—— setting.gradle
└── gradle.properties
- gradle-wrapper.properties文件是gradle wrapper的配置文件,当第一次使用gradlew命令时,会根据配置下载正确版本的gradle执行。
- app下的build.gradle是app这个module的gradle脚本,gradle支持多module编译,每个module下都需要一个gradle脚本。
- 根目录下的build.gradle是顶层的gradle脚本, 各个子module可以通过api获取顶层的配置
- gradle.properties用于配置gradle的运行环境,包括设置gradle本身的属性,gradle所在jvm的属性。常用来设置gradle运行的http代理,配置jvm的堆栈内存大小等。另外可以在该文件中定义全局的常量,该常量可以在各个build.gradle文件中直接引用。
2.2 构建周期(LifeCycle)
每个build.gradle文件都和一个Project对象一一对应,build.gradle中的脚本都会委托给Project对象执行,所以脚本中可以调用Project的方法,访问其属性。当通过gradle进行构建时,我们实际执行的是Project对象中的某个task,在执行这个task之前gradle会进行一系列的工作,称之为构建周期。
理解了gradle的构建周期才能看懂gradle脚本的执行顺序,整个构建过程分为三个阶段:
- 初始化阶段:setting.gradle中的脚本会执行,gradle会根据setting.gradle中的配置决定创建哪些Project对象。
- 配置阶段:顶层的build.gradle中的脚本会先执行,之后会根据setting.gradle中的配置顺序执行各个子module中的build.gradle脚本
- 执行阶段:执行某个task,若该task依赖了其他task,会先执行其依赖的task。
下面以运行一个简单的task为例,解释gradle的构建周期。各个文件内容如下
setting.gradle
println "setting.gradle evaluate"
include ':app'
顶层build.gradle
println "top build.gradle evaluate, name ${project.name}"
app目录的build.gradle
println "submodule build.gradle evaluate, name ${project.name}"
//调用Project对象的task方法创建task,groovy在调用函数时可以省略括号
task helloJava {
//doLast中的语句会在task执行完之后执行
doLast {
println "hello java"
}
println "configure task ${name}"
}
//创建showLifeCycle task,依赖helloJava task
task showLifeCycle(dependsOn:helloJava) {
doLast {
println "hello groovy"
}
println "configure task ${name}"
}
上面的脚本创建了两个task,showLifeCycle task依赖于helloJava task,当在终端运行showLifeCycle Task时,打印信息如下
D:\1_androidDemo\LeaningGradle>gradlew -q showLifeCycle
//初始化阶段最先进行,所以先执行的是setting.gradle中的脚本
setting.gradle evaluate
//配置阶段,先执行顶层gradle脚本,该脚本对应的Project的name为LeaningGradle,为该工程的名称
top build.gradle evaluate, name LeaningGradle
//执行子module的gradle脚本,该脚本对应的Project的name为app,为该module的名称
submodule build.gradle evaluate, name app
//创建task时会执行闭包中语句,执行showLifeCycle前会先执行其依赖的task
configure task helloJava
configure task showLifeCycle
//执行阶段,doLast中的语句会在task执行的最后执行
hello java
hello groovy
可以看出,由于showLifeCycle依赖于helloJava,因此会先执行helloJava,且调用task方法创建task时,会执行其闭包中的语句,doLast中的语句会在该task执行期间运行。
2.3 Gradle DSL
build.gradle中的脚本是用groovy语言写的,其中的一些语句实际是函数调用,但是由于groovy中函数调用的极简写法,刚开始接触gradle脚本时不会意识到这些语句实际上是函数调用。下面是创建showLifeCycle task的另一种写法。
//等效写法
task(dependsOn:helloJava, showLifeCycle, { Task task->
task.doLast({
println "hello groovy"
})
println "configure task ${task.name}"
})
这下就清晰了,实际上是调用了Project的task方法,传入了三个参数。
- 第一个参数是一个map,dependsOn键对应的是其依赖的task。
- 第二个参数是该task的name。
- 第三个参数是一个闭包,闭包中的参数为该task本身。我们在闭包中又调用了task的doLast方法传入了一个闭包,该闭包会转换成一个action加入到task中action list的最后一个位置。task在执行时按顺序执行action list。(参考AbstractTask的doLast方法)
build.gradle脚本中的函数调用和属性访问都会委托给Project对象,build.gradle中的buildscript,allprojects,task,apply等都是调用Project对象的相应方法,这些API的集合称为Gradle的DSL。下面介绍gradle DSL中最常用的Project和Task对象,其他内容可以参考官方文档
2.3.1 Project
2.3.3.1 引用域
Project对象和build.gradle文件一一对应,build.gradle脚本文件可以访问Project的属性和方法。脚本在访问属性和方法时会在几个区域(scope)中去寻找,按照下列顺序:
优先级 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|
属性 | project本身的属性 | extra属性 | extensions | convention属性 | task对应的属性 | 继承与父project的属性 |
方法 | project本身的方法 | build.gradle中定义的方法 | extensions | convention中的方法 | task对应的方法 | 继承与父project的方法 |
下面以一个例子解释属性引用,方法的引用类似
顶层build.gradle
ext {
GROOVY_ROOT = "groovy in root project"
KOTLIN = "kotlin"
}
app目录下build.gradle
apply plugin: 'com.android.application'
android {
compileSdkVersion 26
defaultConfig {
applicationId "com.example.joeyongzhu.leaninggradle"
minSdkVersion 15
targetSdkVersion 26
versionCode 1
versionName "1.0"
}
}
ext {
GROOVY = "groovy"
JAVA = "java"
}
//‘<<’运算符重载等价于doLast
task JAVA << {
println "this is task java"
}
task KOTLIN << {
println "this is task kotlin"
}
task testScope << {
//app和root中都定义了GROOVY,默认引用的是当前脚本中定义的
assert GROOVY == "groovy"
//ext域中定义的变量实际上会添加到当前project的extensions中
assert GROOVY == project.getExtensions().getByName("ext").GROOVY
//当前脚本中定义的task会添加到对应project的property中,直接通过task的name引用
assert KOTLIN instanceof Task
//JAVA这个标识符引用的是ext中定义的变量,而不是task
assert JAVA == project.getExtensions().getByName("ext").JAVA
//父脚本中定义的ext也可以直接引用
assert GROOVY_ROOT == "groovy in root project"
//父脚本中定义的ext,优先级比当前脚本的低
assert rootProject.KOTLIN == rootProject.getExtensions().getByName("ext").KOTLIN
//android插件添加的extension,也可以直接引用
assert android == project.getExtensions().getByName("android")
assert "com.example.joeyongzhu.leaninggradle" == project.getExtensions().getByName("android").defaultConfig.applicationId
//convention默认和extensions是同一个实例
assert project.getConvention() == project.getExtensions()
}
所以gradle脚本中引用的属性,可能定义在以上几个域中定义的,需要注意的是extensions中的属性,是可以在运行时添加的。如上面的android插件就在apply时向当前插件中添加了一个名为android的extension,其类型是AppExtension,由android的gradle插件定义。由于extensions优先级不高,所以当我们在脚本中定义变量时注意避免同名冲突。
插件利用了Project的该种特性向脚本中注入了自己的DSL,这也是gradle设计的巧妙之处,脚本的DSL是可以通过插件扩展的,gradle本身只提供一个构建框架,具体构建过程的DSL可由插件扩展。
2.3.1.2 Project常用方法和属性
前面也提到了Project中常用的几个方法和属性,现在挑几个说下。具体可以参考Project的文档
build.gradle中最常出现的几个函数:
//引入android插件,调用了project的apply函数
//传入的是一个map,相当于apply([plugin: 'com.android.application'])
apply plugin: 'com.android.application'
//buildscript函数配置解析脚本的gradle和插件版本
buildscript {
//gradle和插件会按顺序尝试从下面库中获取
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
//allprojects中闭包会在每个子project中调用,通常包含在顶层的build.gradle中
//下面为每个子项目配置repositories, 子项目的dependencies会去这些库中寻找
allprojects {
repositories {
google()
jcenter()
}
}
下面是其他一些常用的函数:
//该函数会在当前project配置完成后调用
afterEvaluate {
println "${it.name} evaluate finish "
}
//file函数,在当前project目录下创建文件
def file = file("hello.txt")
assert "D:\\1_androidDemo\\LeaningGradle\\app\\hello.txt" == file.absolutePath
//tasks属性包含当前project中已经添加的所有task
tasks.each {
println "task: ${it.name}"
}
//注册一个监听器,之后project创建task时会调用该闭包
tasks.whenTaskAdded {
println "task: ${it.name} add"
}
//获取之前的JAVA task,并在其最后添加一个action
tasks.getByName("JAVA").doLast {
println "hello java"
}
//gradle属性指向全局唯一的gradle对象
//调用beforeProject方法监听每个project创建
gradle.beforeProject {
println "project: ${it.name}"
}
//监听所有task的执行
gradle.taskGraph.beforeTask {
println "start task: ${it.name}"
}
2.3.2 Task
Task的方法和属性在引用时和Project有类似的引用区域,详细可以参考gradle DSL文档
这里用一段代码展示Task的一些基础用法,高阶用法可以参考
task testGroovy << {
println "groovy"
}
//另一种创建task的方法
tasks.create("testJava"){
doLast {
println "java"
}
}
//添加依赖
testJava.dependsOn testGroovy
//gradle内置了一些工具task,只需要配置便可以使用
//Copy任务执行复制操作
task testCopy(type: Copy) {
from file("build/outputs/apk/debug")
into file("apk")
include "*.apk"
}
//Exec任务执行命令行
task testCmd(type: Exec) {
commandLine "adb", "devices"
}
//定义任务的输入输出,gradle在运行task前会检测任务的输入输出,如果没有变化,说明该任务up-to-date,便会不会执行
//改造上文的testReadXml,添加输入输出定义,多次运行该task只有第一次会执行,若改动输入输出文件,则会重新执行task
task testTaskUpToDate {
def allActivityName = file("activity_name.txt")
def manifestFile = file("src/main/AndroidManifest.xml")
inputs.file manifestFile
outputs.file allActivityName
doLast {
def androidManifest = new XmlSlurper().parse(manifestFile)
androidManifest.declareNamespace('android':'http://schemas.android.com/apk/res/android')
androidManifest.application.activity.each { def activity ->
println activity["@android:name"]
allActivityName << activity["@android:name"]
}
}
}
其中最后一个例子演示了为task定义输入,输出。gradle在运行task前,会去检测其输入,输出有没有变化,没有变化会标记up-to-date,不会重复运行。
三、Gradle插件
3.1 Android插件
Android构建插件和Java插件一样包含四个基本的task
- assemble 组合所有输出任务
- check 执行所有检查任务
- build 执行assemble任务和check任务
- clean 清空项目的所有输出
前三个任务其实不会做任何事情,只是对构建过程的一个抽象,具体的任务会由插件创建并让其被这几个任务依赖。例如Android插件默认会创建assembleDebug和assembleRelease任务,并且让assemble任务依赖于它们。还会创建lint任务,让check任务依赖于lint。同样我们也可以添加自定义的任务,并让其依赖与android插件创建的任务,从而实现对android的编译流程的hook。
3.1.1 Android DSL
之前提到过gradle脚本中的DSL是可由插件扩展的,android插件同样定义了自己的DSL。下面列举一个常见配置的例子,详细的配置参数可以参考官方文档:
- android配置构建:https://developer.android.com/studio/build/index.html?hl=zh-cn
- android DSL文档:http://google.github.io/android-gradle-dsl/current/com.android.build.gradle.internal.dsl.DefaultConfig.html
//引入android插件
apply plugin: 'com.android.application'
//android DSL配置项
android {
//定义编译sdk版本
compileSdkVersion 26
//默认配置,被所有构建变体共享的配置
defaultConfig {
applicationId "com.example.joeyongzhu.leaninggradle"
minSdkVersion 15
targetSdkVersion 26
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
//构建类型,默认会添加debug和release两种
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
//自定义debug类型的buildType
demo {
//复制debug的配置
initWith debug
//给applicationId添加个后缀
applicationIdSuffix ".demo"
}
}
//产品风味
productFlavors {
//两个纬度的产品风味
flavorDimensions "api", "color"
//颜色风味
blue {
dimension "color"
//向manifest中插入一个变量colorName,AndroidManifest文件中可以通过${colorName}引用该变量
manifestPlaceholders = [colorName:"blue"]
//gradle会生成一个BuildConfig.java文件,该语句向其中插入一个String类型的变量BLUE,其值为“blue”
buildConfigField "String", "BLUE", "\"blue\""
}
red {
dimension "color"
manifestPlaceholders = [colorName:"red"]
buildConfigField "String", "RED", "\"red\""
}
//api风味,重新定义了minSdkVersion
minApi20 {
dimension "api"
minSdkVersion 20
}
minApi25 {
dimension "api"
minSdkVersion 25
}
}
//源集目录配置
sourceSets {
demo {
java.srcDirs = ['src/demo/java', 'src/demo/java/']
}
}
}
Android构建的配置是基于构建变体(BuildVariant)的,构建变体由buildTypes和productFlavors的配置生成。如上述配置会生成一共12个构建变体,在上面脚本添加下列代码,可以打印出所有构建变体的名称。
android.applicationVariants.all { variant ->
println "variant: ${variant.name}"
}
输出的结果为
variant: minApi25RedDebug
variant: minApi25RedRelease
variant: minApi25RedDemo
variant: minApi25BlueDebug
variant: minApi25BlueRelease
variant: minApi25BlueDemo
variant: minApi20RedDebug
variant: minApi20RedRelease
variant: minApi20RedDemo
variant: minApi20BlueDebug
variant: minApi20BlueRelease
variant: minApi20BlueDemo
可见构建变体的名称为:[minApi20,minApi25][blue,red][debug,release,demo],分别由productFlavors和buildTypes确定。
每个构建变体都可以覆盖defaultConfig中的默认配置,并且拥有自己的sourceSet,包括java文件和资源文件。因此可以利用构建变体为同一个项目生成多个不同版本的应用,实现版本间的代码和资源复用。
上述例子中还用到了manifestPlaceholders和buildConfigField两个函数,这两个函数可以实现对AndroidManifest.xml文件和BuildConfig文件的变量注入,从而可以针对不同版本做不同的设置。
3.1.2 Android构建Task
Android插件会为每个构建变体都生成一个assemble+构建变体名称的task,还会生成assemble+构建类型和assemble+产品风味的task用来执行相应的一系列的构建变体的assemble任务。对于上述例子,在命令行执行下列命令查看有哪些assemble task。
gradlew -q tasks --all | findstr /R assemble
输出结果为
app:assemble - Assembles all variants of all applications and secondary packages.
app:assembleBlue - Assembles all Blue builds.
app:assembleDebug - Assembles all Debug builds.
app:assembleDemo - Assembles all Demo builds.
app:assembleMinApi20 - Assembles all MinApi20 builds.
app:assembleMinApi20Blue - Assembles all builds for flavor combination: MinApi20Blue
app:assembleMinApi20Red - Assembles all builds for flavor combination: MinApi20Red
app:assembleMinApi25 - Assembles all MinApi25 builds.
app:assembleMinApi25Blue - Assembles all builds for flavor combination: MinApi25Blue
app:assembleMinApi25Red - Assembles all builds for flavor combination: MinApi25Red
app:assembleRed - Assembles all Red builds.
app:assembleRelease - Assembles all Release builds.
app:assembleMinApi20BlueDebug
app:assembleMinApi20BlueDemo
app:assembleMinApi20BlueRelease
app:assembleMinApi20RedDebug
app:assembleMinApi20RedDemo
app:assembleMinApi20RedRelease
app:assembleMinApi25BlueDebug
app:assembleMinApi25BlueDemo
app:assembleMinApi25BlueRelease
app:assembleMinApi25RedDebug
app:assembleMinApi25RedDemo
app:assembleMinApi25RedRelease
其中assemble命令会依赖每个构建变体的assemble任务,assembleBlue依赖所有color为Blue的构建变体的任务,其他类似。
下面以assembleMinApi20BlueDebug为例,介绍android的构建流程涉及的task
首先在build.gradle中添加下列代码监听所有task的执行
radle.taskGraph.beforeTask {
println "start task: ${it.name}"
}
下面看下执行assembleMinApi20BlueDebug任务后的输出
start task: preBuild
start task: preMinApi20BlueDebugBuild
start task: compileMinApi20BlueDebugAidl
start task: compileMinApi20BlueDebugRenderscript
start task: checkMinApi20BlueDebugManifest
start task: generateMinApi20BlueDebugBuildConfig
start task: prepareLintJar
start task: generateMinApi20BlueDebugResValues
start task: generateMinApi20BlueDebugResources
start task: mergeMinApi20BlueDebugResources
start task: createMinApi20BlueDebugCompatibleScreenManifests
start task: processMinApi20BlueDebugManifest
start task: splitsDiscoveryTaskMinApi20BlueDebug
start task: processMinApi20BlueDebugResources
start task: generateMinApi20BlueDebugSources
start task: javaPreCompileMinApi20BlueDebug
start task: compileMinApi20BlueDebugJavaWithJavac
start task: compileMinApi20BlueDebugNdk
start task: compileMinApi20BlueDebugSources
start task: mergeMinApi20BlueDebugShaders
start task: compileMinApi20BlueDebugShaders
start task: generateMinApi20BlueDebugAssets
start task: mergeMinApi20BlueDebugAssets
start task: transformClassesWithDexBuilderForMinApi20BlueDebug
start task: transformDexArchiveWithExternalLibsDexMergerForMinApi20BlueDebug
start task: transformDexArchiveWithDexMergerForMinApi20BlueDebug
start task: mergeMinApi20BlueDebugJniLibFolders
start task: transformNativeLibsWithMergeJniLibsForMinApi20BlueDebug
start task: transformNativeLibsWithStripDebugSymbolForMinApi20BlueDebug
start task: processMinApi20BlueDebugJavaRes
start task: transformResourcesWithMergeJavaResForMinApi20BlueDebug
start task: validateSigningMinApi20BlueDebug
start task: packageMinApi20BlueDebug
start task: assembleMinApi20BlueDebug
标红的是几个比较重要的task,解释如下:
- pre[构建变体]Build:该构建变体assemble任务执行的第一个任务
- generate[构建变体]Sources:生成所有和该构建相关的java文件和资源文件,如AIDL,R.java,BuildConfig等文件。
- compile[构建变体]Sources:包含所有资源和代码的编译
- transformClassesWithDexBuilderFor[构建变体]:将Class转换为Dex文件
- package[构建变体]:将dex和资源文件打包成apk并签名
- assemble[构建变体]:构建该构建变体
3.1.3 hook android编译流程
实际构建app时,可能android提供的原生构建任务不能满足我们的需求时,可能要hook原生的android编译流程,加入我们定制的操作,有以下几种方式:
(1)利用ApplicationVariant
ApplicationVariant类描述了构建变体,构建变体的所有信息可由其获得。通过修改其属性,可以改变构建过程的一些行为,如下面代码可以更改生成apk的名字。
android.applicationVariants.all { variant ->
variant.outputs.all{ output ->
def file = output.outputFile
output.outputFileName = file.name.replace(".apk", "-modify.apk")
}
}
AppicationVariant的基类BaseVariant还提供了两个接口用于向构建流程中插入自定义的任务
task hookJavaGenerate {
doLast {
println "hook java generation"
}
}
task hookResGenerate {
doLast {
println "hook resource generation"
}
}
android.applicationVariants.all { BaseVariant variant ->
//hookJavaGenerate任务会在java代码编译前执行
variant.registerJavaGeneratingTask(hookJavaGenerate)
//hookResGenerate任务会在资源编译前执行
variant.registerResGeneratingTask(hookResGenerate)
}
(2)向android的task中添加action
利用task的doFirst和doLast函数可以向task中添加action,实现在该task运行前和运行后插入一些操作,例如我们可以在compile[构建变体]Sources这个task前修改代码,实现在编译期间更改代码的行为。以hook compileMinApi20BlueDebugSources任务为例
//注意在project配置阶段完成后向task中加入action,此时该task已经配置完成
afterEvaluate{
//找到compileMinApi20BlueDebugSources任务
tasks.getByName("compileMinApi20BlueDebugSources") {
it.doFirst{
println "do some stuff"
}
}
}
(3)自定义task,让原生的android task依赖该task
很多第三方插件正是利用该方式向android的编译流程中注入自己的一系列的task,还是以hook compileMinApi20BlueDebugSources任务为例
task doSomeStuff << {
println 'Do some stuff'
}
afterEvaluate {
compileMinApi20BlueDebugSources.dependsOn doSomeStuff
}
达到的效果和第二种方式是一样的,不同的是通过添加task方式可以利用task的up-to-date特性,为其定义输入输出,这对耗时的构建任务还是必要的,减少不必要的任务执行,优化构建速度。
四、gradle实例
在调试app时经常需要清除应用数据,重启应用进程,手动操作很麻烦,因此我创建了utils.gradle文件包含了这两个task。将该脚本放到app目录下,在app的build.gradle文件中通过apply from:"utils.gradle",便可以使用。
这个脚本会去分析AndroidManifest中的包名和launcherActivity信息,通过adb命令完成清除应用数据和重启应用进程的任务,下面以该段程序总结Gradle脚本的学习。
//清除应用数据的任务
//注意getAppPackage方法必须在配置阶段结束后调用,因为其调用了getManifestFile方法。
task clearData(type:Exec) { Task task ->
def packageName
//在task执行前获取应用package,此时配置阶段已经结束
task.doFirst {
packageName = getAppPackage()
}
//这边用到了字符串的延迟加载,因为在配置阶段还没有获取packageName
commandLine "adb", "shell", "pm clear ${-> packageName}"
}
//重启应用的任务
task restartApp(type:Exec) { Task task ->
def topActivity
def packageName
task.doFirst {
packageName = getAppPackage()
topActivity = getStartActivity()
}
//-S 参数会使am调用forceStopPackage强制终止应用后再重启
commandLine "adb", "shell", "am start -S ${-> packageName}/${-> topActivity}"
}
//分析AndroidManifest文件获取应用包名
String getAppPackage() {
def androidManifest = new XmlSlurper().parse(getManifestFile())
return androidManifest.@package
}
//分析AndroidManifest文件获取启动Activity的全名
String getStartActivity() {
def topActivity
def androidManifest = new XmlSlurper().parse(getManifestFile())
androidManifest.declareNamespace('android':'http://schemas.android.com/apk/res/android')
//遍历所有声明的Activity,找到包含category.LAUNCHER的activity,注意这边通过any遍历
androidManifest.application.activity.any { def activity ->
activity."intent-filter".any {
it.category.any { def category ->
if("android.intent.category.LAUNCHER".equals(category["@android:name"].toString())) {
topActivity = activity["@android:name"]
return true
}
}
}
}
return topActivity
}
//分析android生成的applicationVariants变量,找到debug模式的sourceSet配置,获取manifest文件的位置
//该函数必须在gradle配置阶段之后调用,否则android属性可能还没有生成,该属性是有android插件添加的
File getManifestFile() {
String manifestFile = project.file("src/main/AndroidManifest.xml").toString()
android.applicationVariants.any { variant ->
if("debug".equals(variant.name)) {
manifestFile = variant.sourceSets[0].manifestFile
return true
}
}
return new File(manifestFile)
}