Android Studio——Gradle与build.gradle

Gradle

Gradle是一种项目构建工具,说白了就是对项目进行配置,告诉编译器怎么编译项目的管理工具。面向Java应用为主。当前其支持的语言限于Java、Groovy、Kotlin和Scala,计划未来将支持更多的语言。

Gradle is an open-source build automation tool focused on flexibility and performance. Gradle build scripts are written using a Groovy or Kotlin DSL.

Gradle现在主要的呈现形式是Groovy语言,鉴于Google对Kotlin的大力推崇,和Kotlin更加灵活的特性(没有最灵活,只有更灵活),Gradle也支持用Kotlin编写,虽然,目前某些API的兼容还不是那么友好。

对于学生,比如我,Maven就够用了,除非Android强制的Gradle构建。但是在大型项目构建中,Gradle更加顺手,虽然我没做过啥大型项目😄。写这篇的目的是讲解Android中的build.gradle,方便能看懂。

背景

build.gradle的背景知识可以去官方文档,这里详细介绍了如何安装配置Gradle,以及如何通过gradle构建一个项目等等。

API

官方文档,这里详细介绍了Gradle的API,想要细致了解的也可以参考。

Gradle脚本

Gradle一般会产生两个脚本文件,settings.gradle和build.gradle。settings.gradle包含项目的模块信息,build.gradle包含项目(模块)的构建配置细节。每个脚本文件都对应一个实例对象,settings.gradle对应Setting,build.gradle对应Project。一个项目的构建过程遵循以下的生命周期

  • 创建Settings对象,默认配置;
  • 要是有settings.gradle脚本,覆盖配置Settings对象;
  • 根据Settings对象,建立Project实例们的层次性(比如多模块);
  • 最后,基于宽度优先的原则,通过执行build.gradle配置构建每个Project,当然,可以通过Project.evaluationDependsOnChildren()或者Project.evaluationDependsOn(java.lang.String)重载改变这种默认的构建顺序。

Gradle脚本由以下元素组成PluginsTasksDeplendenciesPropertiesMethods

实例分析

apply plugin: 'com.android.application'
apply plugin: 'jacoco'

jacoco {
    toolVersion = "0.8.5"
}

def localPropertiesFile = project.rootProject.file('signing.properties')
def useLocalSigning = localPropertiesFile.exists()
Properties localProperties = null

if (useLocalSigning) {
    localProperties = new Properties()
    localProperties.load(localPropertiesFile.newDataInputStream())
}

android {
    compileSdkVersion 28
    buildToolsVersion '28.0.3'

    signingConfigs {
        gbDistribution {
            if (useLocalSigning) {
                storeFile file(localProperties.getProperty('signing.storeFile'))
                keyAlias localProperties.getProperty('signing.keyAlias')
                storePassword localProperties.getProperty('signing.storePassword')
                keyPassword localProperties.getProperty('signing.keyPassword')
            }
        }
    }

    defaultConfig {
        applicationId "com.genonbeta.TrebleShot"
        minSdkVersion 14
        targetSdkVersion 28
        versionCode 98
        versionName "1.4.2"
        testInstrumentationRunnerArguments clearPackageData: 'true'
        vectorDrawables {
            useSupportLibrary = true
        }
    }

    lintOptions {
        checkReleaseBuilds false
        abortOnError false
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }

        debug {
            testCoverageEnabled = true
            applicationIdSuffix '.debug'
            versionNameSuffix '-DEBUG'
        }
    }
    flavorDimensions 'mode'
    productFlavors {
        fossReliant {
            dimension 'mode'
            signingConfig signingConfigs.gbDistribution
        }
        googlePlay {
            dimension 'mode'
            signingConfig signingConfigs.gbDistribution
        }
    }
}
task jacocoTestReport(type: JacocoReport) {
    group = "Reporting"
    description = "Generate Jacoco coverage reports after running tests."
    reports {
        xml.enabled = true
        html.enabled = true
        csv.enabled = true
    }
    classDirectories.from = fileTree(
            dir: './build/intermediates/javac/fossReliantDebug/compileFossReliantDebugJavaWithJavac',
            excludes: ['**/*$InjectAdapter.class',
                       '**/*$ModuleAdapter.class',
                       '**/*$ViewInjector*.class',
                       '**/R.class', '**/R$*.class'
            ])
    sourceDirectories.from = files('../app/src/main/java')
    executionData.from = files("$buildDir/outputs/code-coverage/connected/coverage.ec")

    doFirst {
        new File("$buildDir/intermediates/javac/fossReliantDebug/compileFossReliantDebugJavaWithJavac").eachFileRecurse { file ->
            if (file.name.contains('$$')) {
                file.renameTo(file.path.replace('$$', '$'))
            }
        }
    }
}

task copyHebrewResources(type: Copy) {
    File mergedResourcesDir = new File("${buildDir}/mergedResources")

    if (!mergedResourcesDir.isDirectory())
        mergedResourcesDir.mkdirs()

    for (String resEach : android.sourceSets.main.res.srcDirs) {
        File currentFolder = new File(resEach)

        for (File folderContents : currentFolder.listFiles()) {
            if (folderContents.name.endsWith("-he")) {
                String iwNamedFolder = "${folderContents.name.substring(0, folderContents.name.lastIndexOf("-he"))}-iw"

                from folderContents.path
                into "${mergedResourcesDir.path}/${iwNamedFolder}"
                println("Copying resource ${folderContents.path}")
            }
        }
    }

    println("Adding merged resource directories to the resources array ${mergedResourcesDir.path}")
    android.sourceSets.main.res.srcDirs += mergedResourcesDir.path
}

preBuild.dependsOn copyHebrewResources

dependencies {
    testImplementation 'junit:junit:4.12'
    implementation fileTree(include: ['*.jar'], dir: 'libs')

    implementation 'androidx.appcompat:appcompat:1.0.2'
    annotationProcessor 'com.github.bumptech.glide:compiler:4.8.0'
    googlePlayImplementation 'com.anjlab.android.iab.v3:library:1.0.44'
}

这是来自一个Android应用的部分build.gradle代码。

Plugins

Plugin在脚本中的意义类似常规编程中的import导包。除了Groovy和Gradle默认导入的基本API,其他的,比如android{...},都需要通过Plugin导入。可以通过PluginAware.apply(java.util.Map)或者PluginDependenciesSpec的方式导入插件包。这里的Map中的键不是随意设置的,支持from、plugin、to,每次apply类似传进去一句import指令,将插件中的API委托给当前的脚本对象。支持导入的插件类型应该是和Gradle版本绑定的,比如Android的Gradle插件,与Gradle版本之间就存在着兼容关系。

//PluginAware.apply(java.util.Map)
apply plugin: 'com.android.application'
apply plugin: 'jacoco'
//等价于
//PluginDependenciesSpec
plugins{
    id 'com.android.application'
    id  'jacoco'
}
Properties && Methods

在脚本中,最复杂的就是各种properties和methods。Gradle中的属性根据来源和功能被划分到5个集合中,按照Project对象的检索顺序依次为:

  • Project Properties,脚本默认自带属性一系列的项目属性,比如name等。可以直接通过属性名访问,也可以通过project.属性名的方式访问,本质上没区别。这些属性的读写依赖于对应的set/get方法,类似Spring等框架中常见的自动注入;
  • Extra Properties,这是项目于级维护的属性,便于各模块间交互,可以在project.ext这个namespace中进行自定义属性的键值对的声明、读取和更新;
  • Extensions Properties,插件导入的只读属性,这些属性与extension同名;
  • Convention Properties,这些属性是插件通过Project对象下的Convention对象传递给脚本的,其读写操作依赖于Convention对象;
  • Tasks Properties,我们可以通过task名.属性名的方式访问自定义或预定义的task的属性,当然,这种访问是只读的。

note:在本级project中检索不到时,会递归向上,一直到root project,检索extra properties或convention properties,只读。

Gradle中的Methods集也分为5个,而且,范围和检索顺序基本和Properties类似:

  • Project对象自带Methods;
  • build脚本中自定义的Methods;
  • Convention Methods;
  • Tasks,Gradle会为每个Task建立一个同名Method,其参数一般是ClosureAction

note:

  • 在本级project中检索不到时,会递归向上,一直到root project,检索需要的method;
  • 以闭包为值得属性,在Method检索时,会被作为Method看待。
jacoco {
    toolVersion = "0.8.5"
//<==>setToolVersion("0.8.5") 最终调用
//<==>setToolVersion "0.8.5" 快捷写法
//<==>toolVersion "0.8.5" 不规则但可编译
}
//<==>jacoco.toolVersion="0.8.5"
//<==>jacoco.setToolVersion("0.8.5")

jacoco是通过jacoco插件添加的属性,上面的例子中,我们对其中的toolVersion属性进行了配置。由于Delegate,我们可以直接访问属性对象内部的成员属性或方法,进行封装后,我们既可以通过闭包进行批量的配置,也可以通过对象名.属性名,进行单独配置。直接通过属性名访问的方式,使脚本更加易读和美观。虽然,无论怎么写,都可以达到目的。

def localPropertiesFile = project.rootProject.file('signing.properties')
//<==>def localPropertiesFile = rootProject.file('signing.properties')

project是内置的属性,在不冲突混淆的情况下,可以省略。

compileSdkVersion 28
//<==>compileSdkVersion(28)

通常,对属性或方法的检索顺序是,属性集->同名Method->同名get/set方法->上一级。BaseExtension中有setCompileSdkVersion和getCompileSdkVersion方法,但是没有compileSdkVersion这个属性,所以会检索到compileSdkVersion(String version)这个方法调用。虽然通过属性名=属性值或者属性名 属性值的格式经过语法兼容后,都能进行有效的配置,但还是分类配置比较好,有同名属性或者set方法的,通过=赋值,有同名函数的通过快捷函数调用赋值,这样经过IDE的代码格式化后看起来会比较清晰。

Tasks
task jacocoTestReport(type: JacocoReport){...}
//<==>task([type:JacocoReport],"jacocoTestReport",{...})
//<==> tasks.create("jacocoTestReport",JacocoReport.class,{...})

前面的配置还是比较容易理解的,这行代码的原型就比较难懂了。这是Gradle提供的快捷写法之一,在理解这行代码原始形态的过程中,我也经历了比较多的猜测和推翻:

  • Task构造函数,通过命名参数的特性以及快捷写法最终导致这样,但是这是通过task函数创建Task对象,并不是Task的构造函数,所以这种猜测被否定;
  • Task名二次调用,先通过Task task(String name)创建Task对象,然后通过命令链、参数封装等特性进行二次配置,后来发现不存在二次调用的语法,这种猜测也被否定;
  • Task task(Map, String, Closure),Groovy有将键值参数封装成Map作为第一个函数参数的特性,Gradle对Task的特殊封装,jacocoTestReport既是Task的变量名,也是Task名,允许不加引号定义,Groovy的快捷写法中,允许参数表末的闭包位于括号外,所以,最终觉得还是这种猜测比较靠谱。

配置闭包中的操作,对属性和方法的访问也遵循和Project级别类似的顺序,由内向外,由下向上。

classDirectories.from = fileTree(...)
//classDirectories = fileTree(

classDirectories是JacocoReport中的属性,具体的赋值因Gradle插件版本而异,老版本是直接赋值,新版本通过from赋值。

fileTree(
  dir:...
  excludes:...
)

这是文件集合的快捷函数,参数通过Map的形式导入,创建ConfigurableFileTree对文件集进行规则配置,常用的配置参数有dir、excludes、includes、builtBy等。

**/R$*.class

在配置excludes等参数时,通常会用到Ant Path路径匹配规则,乍一看和正则表达式还是挺像的。

? matches one character
* matches zero or more characters
** matches zero or more directories in a path

比如例子中展示的就是匹配所有R类中的内嵌类。

 doFirst{...}

doFirst是Task中的流程函数,通过这一类的函数,用户可以进行task的前置后置等操作。

preBuild.dependsOn copyHebrewResources

preBuild是Task名,通过dependsOn等函数,可以改变Task的执行顺序。

Dependencies
dependencies {
  testImplementation 'junit:junit:4.12'
  implementation 'androidx.cardview:cardview:1.0.0'
  googlePlayImplementation 'com.anjlab.android.iab.v3:library:1.0.44'
  annotationProcessor 'androidx.annotation:annotation:1.0.2'
}

依赖就是将应用在编译过程需要的各种第三方包添加到项目库的操作,除了task,也就这个部分最容易出现问题,主要就是要求的依赖下载不下来的问题,可以参考另一篇文章查看对应的解决方案。API上的差异,Gradle 3.x以前的compile被废弃,分离为apiimplementation,将依赖程度细化了。

  • api,等同于compile,当前项目依赖的包,可以被依赖当前项目的项目访问,即,可导出式依赖;
  • implemention,内部依赖,外部项目无法访问,所以优先使用implemention进行依赖。

库包名称格式,与Maven的大同小异,groudId:artifactId:versionMaven仓库不仅提供Maven格式的依赖导入,也提供Gradle格式的导入。

Dependencies部分可繁可简,对于不同类型的库包选用的API也不一样, 比如,注解包就是通过annotationProcessor进行依赖。特殊情况在这些库包的引导页会有依赖导入样例,或者可以参考官方文档学习。

参考文章

Gradle Build Language Referencehttps://docs.gradle.org/current/dsl/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,258评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,335评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,225评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,126评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,140评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,098评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,018评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,857评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,298评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,518评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,400评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,993评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,638评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,661评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352