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)中去寻找,按照下列顺序:

优先级123456
属性project本身的属性extra属性extensionsconvention属性task对应的属性继承与父project的属性
方法project本身的方法build.gradle中定义的方法extensionsconvention中的方法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插件
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)
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值