Gradle完全解析

作者: junesolar | 来源:发表于2018-05-07 11:23 被阅读703次

    导语: 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插件
    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)
    }
    

    相关文章

      网友评论

        本文标题:Gradle完全解析

        本文链接:https://www.haomeiwen.com/subject/tevdrftx.html