Android Gradle入门

对Gradle的疑问

从刚学习Android开始,就对gradle有着好多疑问,随着了解的越来越多,疑问也越来越多。疑问基本包括:gradle的作用是什么?gradle是怎么构建Android项目的?gradle每个文件的作用?gradle文件中配置具体是配置啥,都是什么含义?怎么使用gradle?多模块项目是怎么构建的?怎么在多模块中找到初始Activity?多模块之间怎么通信?多模块中每个模块要怎么单独运行?
抱着上面的疑问,开启对Gradle的研究。。。

Gradle在Android中的作用

  1. 作用:Gradle是一种构建工具,它可以帮你管理项目中的差异,依赖,编译,打包,部署......,你可以定义满足自己需要的构建逻辑,写入到build.gradle中供日后复用.
  2. Gradle脚本是基于Groovy语言来编译执行的(Groovy 是一种 JVM 上的脚本语言,基于 java 扩展的动态语言)
  3. 总结:一个可编程的工具,用来执行一系列有序的任务来表达自动化需求(编译源代码 -> 拷贝文件 -> 组装构建,当然这些任务不是固定的,可能还有其他任务)
  4. 再简单一点:Gradle就是自动化的编译、打包程序
  5. Gradle官网文档
  6. gradle学习系列文章
  7. Android Gradle 完整指南

Groovy的简单介绍:

本地只简单介绍一下 Groovy 在 Android 中的使用,要想深入了解需看gradle学习系列文章

  1. Task:在Gradle中一个原子性的操作叫做 task,简单理解为task是Gradle脚本中的最小可执行单元。但是要是执行gradle文件中某个task,会发现所有task都被执行

  2. Task Actions:一个 Task 是由一序列 Action (动作)组成的,当运行一个 Task 的时候,这个 Task 里的 Action 序列会按顺序依次执行。Gralde 里通过 doFirst、doLast 来为 Task 增加 Action 。doFirst:task执行时最先执行的操作doLast:task执行时最后执行的操作

  3. Project:每一个 build.gradle 脚本文件被 Gradle 加载解析后,都会对应生成一个 Project 对象,在脚本中的配置方法其实都对应着 Project 中的API

  4. Extension:Android中 gradle脚本中添加的类似android这种命名空间的配置就是extension,翻译成中文意思就叫扩展。它的作用就是通过实现自定义的 Extension,可以在 Gradle 脚本中增加类似 android 这样命名空间的配置,Gradle 可以识别这种配置,并读取里面的配置内容

    • 嵌套Extension:Extension可以嵌套,类似于内部类的写法,在Android项目的gradle中随处可见
    • Android中的Extension:


      Android中的Extension.png
  5. NamedDomainObjectContainer:顾名思义就是命名领域对象容器,它的主要功能有:

    • 它能够通过DSL(在Gradle脚本中)创建指定 type 的对象实例;
    • 指定的 type 必须有一个 public 构造函数,且必须带有一个 String name 的参数,type 类型的领域对象必须有名为“name”的属性;
    • 它是一个实现了 SortedSet 接口的容器,所以所有领域对象的 name 属性值都必须是唯一的,在容器内部会用 name 属性来排序;

Gradle在Android中的运用

Gradle的几个核心类型(主要从Android项目的角度)

Project:表示需要构建的一个项目,类似Android项目的一个module,Project和build.gradle是一一对应的。Project提供了一套api用来查看工程信息。
Task:对一个项目的构建,就是对该项目中Task集合的运行,每个Task执行一些基本的工作,比如:编译、运行单元测试、压缩成apk等
build.gradle:构建脚本(相当于maven的pom.xml)
gradle.properties:属性文件,每个项目可以创建属性文件也可以不创建。
setting.gradle:声明所需的配置来实例化项目的层次结构(意思就类似编译整个大工程,需要包含哪些模块一起编译)

Android Studio通过Gradle构建项目的文件介绍

gradle文件目录.png

项目结构.png

以上文件结构表明:Android项目是Gradle通过多模块构建的

各文件作用介绍:

  1. settings.gradleGradle:使用该文件来配置多项目构建。它应该由一行代码组成:include ':app'
    这告诉Gradle app子目录也是一个Gradle项目。如果在稍后的时间,您要通过可用的向导将Android库添加到此项目中,则会创建另一个项目子目录并将其添加到此文件中。

    1. 我们在 Android 应用开发中,一个 Project 可以包含若干个 module ,这种就叫做多项目构建。在 Android Studio 项目中,根目录都有一个名叫 settings.gradle 的文件,然后每个 module 的根目录中又有一个 build.gradle 文件,Gradle 就是通过 settings.gradle 来进行多项目构建的。
    2. Gradle 在运行时会读取并解析 settings.gradle 文件,生成一个 Settings对象,然后从中读取并解析子项目的 build.gradle 文件,然后为每个 build.gradle 文件生成一个 Project 对象,进而组装一个多项目构建出来。
    3. Settings 里最核心的API就是 include 方法,通过该方法引入需要构建的子项目。
  2. gradle-wrapper.properties:它配置了所谓的Gradle Wrapper。这使您可以构建Android项目,而无需首先安装Gradle,其一般结构如下:

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

前四行表示当包装首次运行时,它将下载Gradle发行版并将其存储在主目录.gradle/wrapper/dists中的目录中。最后一行distributionUrl是Gradle的下载地址

  1. gradle.propertie 文件:
    1. 在与 build.gradle 文件同级目录下,定义一个名为 gradle.properties 文件,里面定义的键值对,可以在 Project 中直接访问:
      gradle.properties里定义属性值:company="hangzhouheima"
      在 build.gradle 文件里可以这样直接访问:println company = ${company}
    2. 扩展属性:可以通过 ext 命名空间来定义属性,我们称之为扩展属性:
      定义属性:ext {username = "hjy" age = 30}
      使用属性:println username
  1. build.gradle:史上最全Android build.gradle配置详解

// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript { //这里是gradle脚本执行所需依赖,分别是对应的maven库和插件
    
    repositories {
        google() //从Android Studio3.0后新增了google()配置,可以引用google上的开源项目
        jcenter() //是一个类似于github的代码托管仓库,声明了jcenter()配置,可以轻松引用 jcenter上的开源项目
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.0.0' //此处是android的插件gradle,gradle是一个强大的项目构建工具

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }}

allprojects { //这里是项目本身需要的依赖,比如项目所需的maven库
    repositories {
        google()
        jcenter()
    }}

// 运行gradle clean时,执行此处定义的task任务。
// 该任务继承自Delete,删除根目录中的build目录。
// 相当于执行Delete.delete(rootProject.buildDir)。
// gradle使用groovy语言,调用method时可以不用加()。
task clean(type: Delete) {
    delete rootProject.buildDir
}

方法解析:
1. apply:apply(options: Map<String, ?>):

我们通过该方法使用插件或者是其他脚本,options里主要选项有:

  • from: 使用其他脚本,值可以为 Project.uri(Object) 支持的路径
  • plugin:使用其他插件,值可以为插件id或者是插件的具体实现类例如:

//使用插件,com.android.application 就是插件id
apply plugin: 'com.android.application'
//使用插件,MyPluginImpl 就是一个Plugin接口的实现类
apply plugin: MyPluginImpl
//引用其他gradle脚本,push.gradle就是另外一个gradle脚本文件
apply from: './push.gradle'

文件中第一行使用apply plugin表示应用了一个插件,其中提供了Android 编译、测试、打包等等的所有task,该插件一般有两种值可选:

  • com.android.application:表示该模块为应用程序模块,可以直接运行,打包得到的是.apk文件
  • com.android.library:表示该模块为库模块,只能作为代码库依附于别的应用程序模块来运行,打包得到的是.aar文件

2. 对Project进行配置:

//通过path定位并获取该 Project 对象
project(path: String): Project
//通过path定位一个Project,并进行配置
project(path: String, config: Closure): Project
//针对所有项目进行配置,但是为了保证每个项目的独立性,我们一般不会在这里面操作太多共有的东西。
allprojects(config: Closure)
//针对所有子项目进行配置
subprojects(config: Closure)

3. 其中几个 {...} 闭包 解释下:

buildscript {...} --> 定义了 Android 编译工具(gradle)的类路径。声明仓库、添加插件专用闭包。就是对gradle的配置
repositories {...} --> 就是代码仓库,我们平时的添加的一些 dependency 就是从这里下载的。 jCenter是一个著名的 Maven 仓库。
dependencies {...} --> 添加插件、远程依赖
apply --> 使用插件,插件虽然导入进来了,但是子项目用不用就是个事了,要是不用的话,是不需要打包进来的,所以要主动声明下
每个项目单独的 build.gradle --> 针对每个moudle 的配置,如果这里的定义的选项和顶层build.gradle定义的相同,顶层build.gradle会被覆盖

4. 依赖的添加格式: group:name:version(组名:库名称:版本号)

举例:implementation 'androidx.appcompat:appcompat:1.2.0'
本地依赖:通过files()方法可以添加文件依赖,如果有很多jar文件,我们也可以通过fileTree()方法添加一个文件夹,除此之外,我们还可以通过通配符的方式添加:implementation fileTree(dir: 'libs', include: ['*.jar'])

每个项目单独的 build.gradle

apply plugin: 'com.android.application'

android {    
    compileSdkVersion 29 
    buildToolsVersion "30.0.2"
    
    defaultConfig {
        applicationId "com.wang.text"
        minSdkVersion 23
        targetSdkVersion 29
        versionCode 1
        versionName "1.0" 
        
        testInstrumentationRunner  "androidx.test.runner.AndroidJUnitRunner"
    }
    
    buildTypes {
        release { 
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation project(path: ':module_two'
}
  • apply plugin:第一行代码应用了Android 程序的gradle插件,作为Android 的应用程序,这一步是必须的,因为plugin中提供了Android 编译、测试、打包等等的所有task。
  • android:这是编译文件中最大的代码块,关于android 的所有特殊配置都在这里,这就是又我们前面的声明的 plugin 提供的。
  1. defaultConfig就是程序的默认配置,注意,如果在AndroidMainfest.xml里面定义了与这里相同的属性,会以这里的为主。
  2. 这里最有必要要说明的是applicationId的选项:在我们曾经定义的AndroidManifest.xml中,那里定义的包名有两个用途:一个是作为程序的唯一识别ID,防止在同一手机装两个一样的程序;另一个就是作为我们R资源类的包名。在以前我们修改这个ID会导致所有用引用R资源类的地方都要修改。但是现在我们如果修改applicationId只会修改当前程序的ID,而不会去修改源码中资源文件的引用。
  • buildTypes:定义了编译类型,针对每个类型我们可以有不同的编译配置,不同的编译配置对应的有不同的编译命令。默认的有debug、release 的类型。
  • dependencies:是属于gradle 的依赖配置。它定义了当前项目需要依赖的其他库。
Module完整的build.gradle配置如下
// 声明是Android程序,//com.android.application 表示这是一个应用程序模块//com.android.library 标识这是一个库模块//而这区别:前者可以直接运行,后着是依附别的应用程序运行
apply plugin: 'com.android.application'

android {
    signingConfigs {// 自动化打包配置,签名
        release {// 线上环境
            keyAlias 'test'
            keyPassword '123456'
            storeFile file('test.jks')
            storePassword '123456'
        }
        debug {// 开发环境
            keyAlias 'test'
            keyPassword '123456'
            storeFile file('test.jks')
            storePassword '123456'
        }
    }
    compileSdkVersion 27//设置编译时用的Android版本
    defaultConfig {
        applicationId "com.billy.myapplication"//项目的包名
        minSdkVersion 16//项目最低兼容的版本
        targetSdkVersion 27//项目的目标版本
        versionCode 1//版本号
        versionName "1.0"//版本名称
        flavorDimensions "versionCode"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"//表明要使用AndroidJUnitRunner进行单元测试
    }
    buildTypes {// 生产/测试环境配置
        release {// 生产环境
            buildConfigField("boolean", "LOG_DEBUG", "false")//配置Log日志
            buildConfigField("String", "URL_PERFIX", "\"https://release.cn/\"")// 配置URL前缀
            minifyEnabled false//是否对代码进行混淆
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'//指定混淆的规则文件
            signingConfig signingConfigs.release//设置签名信息
            pseudoLocalesEnabled false//是否在APK中生成伪语言环境,帮助国际化的东西,一般使用的不多
            zipAlignEnabled true//是否对APK包执行ZIP对齐优化,减小zip体积,增加运行效率
            applicationIdSuffix 'test'//在applicationId 中添加了一个后缀,一般使用的不多
            versionNameSuffix 'test'//在applicationId 中添加了一个后缀,一般使用的不多
        }
        debug {// 测试环境
            buildConfigField("boolean", "LOG_DEBUG", "true")//配置Log日志
            buildConfigField("String", "URL_PERFIX", "\"https://test.com/\"")// 配置URL前缀
            minifyEnabled false//是否对代码进行混淆
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'//指定混淆的规则文件
            signingConfig signingConfigs.debug//设置签名信息
            debuggable false//是否支持断点调试
            jniDebuggable false//是否可以调试NDK代码
            renderscriptDebuggable false//是否开启渲染脚本就是一些c写的渲染方法
            zipAlignEnabled true//是否对APK包执行ZIP对齐优化,减小zip体积,增加运行效率
            pseudoLocalesEnabled false//是否在APK中生成伪语言环境,帮助国际化的东西,一般使用的不多
            applicationIdSuffix 'test'//在applicationId 中添加了一个后缀,一般使用的不多
            versionNameSuffix 'test'//在applicationId 中添加了一个后缀,一般使用的不多
        }
    }

    sourceSets {//目录指向配置
        main {
            jniLibs.srcDirs = ['libs']//指定lib库目录
        }
    }

    packagingOptions{//打包时的相关配置
        //pickFirsts做用是 当有重复文件时 打包会报错 这样配置会使用第一个匹配的文件打包进入apk
        // 表示当apk中有重复的META-INF目录下有重复的LICENSE文件时  只用第一个 这样打包就不会报错
        pickFirsts = ['META-INF/LICENSE']

        //merges何必 当出现重复文件时 合并重复的文件 然后打包入apk
        //这个是有默认值得 merges = [] 这样会把默默认值去掉  所以我们用下面这种方式 在默认值后添加
        merge 'META-INF/LICENSE'

        //这个是在同时使用butterknife、dagger2做的一个处理。同理,遇到类似的问题,只要根据gradle的提示,做类似处理即可。
        exclude 'META-INF/services/javax.annotation.processing.Processor'
    }

    productFlavors {  
        wandoujia {}
        xiaomi {}
        _360 {}
    }

    productFlavors.all {
            //批量修改,类似一个循序遍历
        flavor -> flavor.manifestPlaceholders = [IFLYTEK_CHANNEL: name]
    }

    //程序在编译的时候会检查lint,有任何错误提示会停止build,我们可以关闭这个开关
    lintOptions {
        abortOnError false
        //即使报错也不会停止打包
        checkReleaseBuilds false
        //打包release版本的时候进行检测
    }

}

dependencies {
    //项目的依赖关系
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    //本地jar包依赖
    implementation 'com.android.support:appcompat-v7:27.1.1'
    //远程依赖
    implementation 'com.android.support.constraint:constraint-layout:1.1.2'
    testImplementation 'junit:junit:4.12'
    //声明测试用例库
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'}

Gradle 构建生命周期

在解析 Gradle 的编译过程之前我们需要理解在 Gradle 中非常重要的两个对象。Project和Task。

每个项目的编译至少有一个 Project,一个 build.gradle就代表一个project,每个project里面包含了多个task,task 里面又包含很多action,action是一个代码块,里面包含了需要被执行的代码。

在编译过程中, Gradle 会根据 build 相关文件,聚合所有的project和task,执行task 中的 action。因为 build.gradle文件中的task非常多,先执行哪个后执行那个需要一种逻辑来保证。这种逻辑就是依赖逻辑,几乎所有的Task 都需要依赖其他 task 来执行,没有被依赖的task 会首先被执行。所以到最后所有的 Task 会构成一个 有向无环图(DAG Directed Acyclic Graph)的数据结构。

无论什么时候执行Gradle构建,都会运行3个不同的生命周期阶段:初始化、配置、执行。

  • 初始化(Initialization):Gradle为每个项目创建一个Project实例,在多项目构建中,Gradle会找出哪些项目依赖需要参与到构建中。
  • 配置(Configuration):执行所有项目的构建脚本,也就是执行每个项目的build.gradle文件。这里需要注意的是,task里的配置代码也会在这个阶段执行。
  • 执行(Execution):Gradle按照依赖顺序依次执行task。

自己总结出来的gradle多模块构建流程:(若有错误还望指出)

项目启动编译后,会先下载gradle-wrapper.properties文件中配置的gradle版本,若已经下载就不需要下载了;然后去加载Setting文件,将其中指定的所有module全列入编译;最后从根gradle文件开始配置,编译。

引用博客:

gradle学习系列文章
史上最全Android build.gradle配置详解
Gradle在Android工程中的运用
重要-作为Android开发者必须了解的Gradle知识
Android Gradle 完整指南

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

推荐阅读更多精彩内容