七、Gradle中的Project

  • 说在前面:Gradle中project是非常重要的,所以也会有非常多的API及其可配置的属性,笔者也有许多不了解的,在这只是讲一些开发中比较常用的一些API和属性。但是了解了这些,其它的自己去看,去查资料相信也是可以搞懂的。当遇到了一些需求是以下API或者属性配置不能解决的,可以到Project文档中查查有没有可以帮助到自己的API。其它想了解的在这也可以查阅。另外,Android Plugin DSL Reference中可查阅android闭包中有哪些可配置选项。共勉

一、Project概念

  1. 什么是Project
  • 前面说过,每个module都会对应着一个Project。先将项目切换到project面板

    project面板.png
  • 我们也知道,初始化阶段,会从settings.gradle中解析生成Project

    settings.gradle.png
  • 可以看到settings.gradle中include了app、annottations(小手一抖多了个t,注意一下就好annotations)、compiler、core、ec,那么我们的项目中有几个Project对象呢?是5个吗?下面我们执行projects这个Task看一下我们的项目中有几个Project

    执行projects task.png
  • 很明显,有个根project 'ecommoerce',在它的下面有5个子Project,所以一共有6个Project。Gradle以树的形式管理Project,最外层有一个根Project,在它下面有其它的几个子Proect。Gradle是根据目录中有没有build.gradle文件来判断这个目录是不是一个Project,我们可以看到在根目录中有一个build.gradle文件,在每个子module中也有它们各自的build.gradle文件。而每个Project都是在build.gradle中去配置和管理的,这些build.gradle最终会被Gradle编译为Project字节码
  1. Project的作用
    根Project是用来统筹管理所有的子Project的,而每个子Project都对应了一个输出。比如我们的app module的类型是application的,那么它最终就对应生成了一个APK文件,是android library类型的,最终会生成一个aar文件,java library类型的生成一个jar文件等等

二、Project核心API

  • Gradle生命周期API

    在Gradle生命周期及其监听中其实已经用过了,里面的this其实就是project,直接用project替换this也是可以的。这一部分的API是用来监听Gradle的生命周期的。

  • project相关API

    这一部分主要是在每个project中获取自己父Project和子Project的,为每个Project提供操作父Project和管理子Project的能力

  1. 获取当前project及其子project


    获取project所有的child.png
  • 注意上面的方法是写在根Project的build.gradle中的,若是在子module中的build.gradle编写,则无法得到下面的结果(模仿执行projects Task的结果),可能只输出了一个“Root project:[所在的module ]”,因为project的getAllProject方法是将当前project当成root project,并且获取当前project的所有子project。

    获取child结果.png
  1. 获取根project


    获取项目的根project.png
  • 注意它们之间的区别

    执行结果.png
  1. 获取project的子project
//调用自己定义的方法,方法中调用了gradle提供的API getSubprojects()
this.getSubProjects()

def getSubProjects() {
    //this.rootProject 获取项目的根目录,
    // getSubprojects()方法返回所有子Project的Set集合,遍历
    def subProjectSet = this.rootProject.getSubprojects()
    subProjectSet.eachWithIndex {
        Project project, int index ->
            if (index > 0 && index < projectSet.size() - 1) {
                println "+--- Project ':$project.name'"
            } else {
                println "\\--- Project ':$project.name'"
            }
    }
}

//在终端执行gradlew clean,以下是结果
'''
> Configure project :app
\--- Project ':annottations'
+--- Project ':app'
+--- Project ':compiler'
+--- Project ':core'
\--- Project ':ec'

'''

  • 顺带说一下:因为执行clean Task,会走Gradle的生命周期,而配置阶段是需要解析所有Project的所有Task,生成拓扑图,前面也有提到,build.gradle最终会被Gradle编译为Project字节码,所以我们在build.gradle中编写我们的脚本,实际上就是在Project内部编写。也就是说我们在app module的build.gradle调用编写的方法的脚本,会在解析的时候被调用,所以即使是我们跑Task clean,也会输出我们想要的结果。

4.获取父project

//在 app build.gradle中加入以下

this.getParentProject()
def getParentProject() {
    def name = "parent为null"
    def parent = this.getParent()
    if (parent != null) { //根Project,已经是最顶层,没有parent
        name = parent.name
    }
    println "project ${this.name} 的parent为:$name"
}


//在终端执行gradlew clean,以下是输出结果
'''
> Configure project :app
project app 的parent为:ecommerce

'''

  • Gradle以树的形式管理Project,通过前面的几个API,已经将所有的Project都关联起来了,我们可以做到在任意的build.gradle(project)中获取其它的project。另外在Gradle中根project是用来统筹管理所有子project的,所以Gradle提供了更加方便的API去管理其子project
  1. project方法配置project
// Project project(String path, Closure configureClosure);

//在根project中的build.gradle
project('app') { Project project ->
    //doSomething for app project
    //比如为app 工程强制使用某个版本的依赖来解决依赖冲突中出现的依赖
    project.configurations.all {
        resolutionStrategy {
            force 'com.android.support:support-annotations:26.1.0'
        }
    }

    //指定输出
    apply plugin:'com.android.application'
    //添加group
    group 'com.github'
    //指定版本
    version '1.0.0'
    //凡是project中可以配置的都可以进行配置
    //比如添加依赖
    dependencies{

    }
    //添加android相关配置
    android{

    }
}
    
    //同样的还可以为其它的project配置,这里就不再配置了,和上面是一样的,将‘app’替换成其它module的名字,然后及对该module进行配置

  • 当然每个Project自己所特有的,最好还是在它自己的build.gradle中配置,Gradle为每个module都提供了自己的build.gradle(project),正是为了让它们职责分明,保证单一原则,你在你那配置你自己的东西就好了。而在根project管理子project,一般都是给这些子project配置一些公共的东西,但是project方法,还是需要一个一个的配置,比如我们每个module的group、version等都是相同的,或者说都依赖某个公共的module,每个build.gradle都需要重复配置,肯定是不符合编码规则的,就像我们编码时,重复的代码,我们都会想办法将它们提取出来。Gradle也提供了这样的方法。

  • 统一配置

//在根project中的build.gradle
//allprojects 为当前project及其所有子project配置
allprojects {
    //仓库
    repositories {
        google()
        jcenter()
    }

    //添加group
    group 'com.github'
    //指定版本
    version '1.0.0'
}

//打印从未配置过的compiler module的group看看是否配置成功
println project('compiler').group

//以下是输出结果,因为笔记添加图片太麻烦了,所以直接拷贝终端的输出结果
'''
C:\Users\***\Desktop\project\ecommerce>gradlew clean
Starting a Gradle Daemon, 2 incompatible Daemons could not be reused, use --status for details

> Configure project :
com.github


BUILD SUCCESSFUL in 16s
6 actionable tasks: 4 executed, 2 up-to-date
'''

  • 统一配置,不包括当前project
//比如我们上传我们的library module到Maven仓库,我们的root project一般是不需要上传上去的,需要将子module上传上去,就可以写一个上传的gradle文件,然后为所有的子module引入。

//省略了project参数
subprojects {
    //库工程才需要上传到Maven
    if (project.plugins.hasPlugin("com.android.library")){
        //当一个功能比较独立时,可以写成一个单独的.gradle文件,然后再需要的地方apply from:'gradle文件path',即可使用该功能
        apply from:'../repositories_upload.gradle'
    }
}

  • 以下是上传到Maven的repositories_upload.gradle文件。为了便于理解,下面的是上传一个library,因为里面还涉及到Task、属性,引入外部文件等方面的内容,并且一些Maven的用户名密码、仓库url等都是定义在其它的地方,所以可能看起来有点混乱,但是讲完之后再来看就可以看的明白了。结合注释,绝大部分都是能够理解的。我们学习Gradle本身就是需要使用它满足我们开发中各种需求,所以实际的应用能让我们更加的了解如何使用Gradle构建项目并完成各种需求。
apply plugin: 'maven' //maven仓库上传插件
apply plugin: 'maven-publish' //maven仓库上传插件
configurations {
    deployerJars
}

repositories {
    mavenCentral()
}

// 判断版本是Release or Snapshots
def isReleaseBuild() {
    return !VERSION.contains("SNAPSHOT")
}

// 获取仓库url
def getRepositoryUrl() {
//    return isReleaseBuild() ? RELEASE_URL : SNAPSHOT_URL
    return Repository_Url
}

//配置上传信息,最重要的是这一段
//我们平时添加依赖的时候就是这样compile 'com.android.support:support-annotations:26.1.0'
//com.android.support 对应 GROUP_ID
//support-annotations 对应 ARTIFACT_ID
//26.1.0 对应 VERSION
//而getRepositoryUrl()获取的是你的Maven仓库URL,userName和password分别对应Maven仓库的用户名和密码,上传到Maven仓库之前需要有你自己Maven仓库,这一步大家google一下基本都会了,当然也可以发布到本地仓库
uploadArchives {
    repositories {
        mavenDeployer {
            pom.version = VERSION
            pom.groupId = GROUP_ID
            pom.artifactId = ARTIFACT_ID
            pom.packaging ='aar'
            repository(url: getRepositoryUrl()) {
                authentication(userName: Authentication_UserName, password: Authentication_Password)
            }
        }
    }
}

// type显示指定任务类型或任务, 这里指定要执行Javadoc这个task,这个task在gradle中已经定义
task androidJavadocs(type: Javadoc) {
    // 设置源码所在的位置
    source = android.sourceSets.main.java.sourceFiles
}

// 生成javadoc.jar
task androidJavadocsJar(type: Jar) {
    // 指定文档名称
    classifier = 'javadoc'
    from androidJavadocs.destinationDir
}

// 生成sources.jar
task androidSourcesJar(type: Jar) {
    classifier = 'sources'
    from android.sourceSets.main.java.sourceFiles
}

// 产生相关配置文件的任务
artifacts {
    archives androidSourcesJar
    archives androidJavadocsJar
}
  • 题外话:学习更多优秀的框架中的是如何使用Gradle的能帮助我们更好的学习。因为那些是真正在实际应用中使用过的东西。并且经过重重检验。比如热修复框架Tinker的build.gradle。现在看起来可能还不太能看的懂,但是也能看懂很多的东西了,只是可能里面一些属性的配置不太了解,结合Tinker接入指南,因为是学习Gradle,所以看看里面的一些属性大概是什么意思即可,也不需要深入了解。建议学完之后再去看看,毕竟学习Gradle,如果能够看懂并且学习微信在Tinker中是如何使用Gradle的,那么对于我们Gradle的学习肯定是很有帮助的.

  • 属性相关API
    Gradle本身为Project提供一些属性,属性相关API可以让我们使用这些属性,并且让我们可以为Project添加额外的属性

  1. Project自带属性
    /**
     * The default project build file name.
     * 所以所有的Project都需要一个默认的build.gradle文件,Gradle默认从该文件读取配置信息
     */
    String DEFAULT_BUILD_FILE = "build.gradle";

    /**
     * The hierarchy separator for project and task path names.
     *路径分割符,如windows文件路径使用斜杠分割
     */
    String PATH_SEPARATOR = ":";

    /**
     * The default build directory name.
     * 默认的build输出文件夹,默认build产生的apk等产物在此目录
     */
    String DEFAULT_BUILD_DIR_NAME = "build";

    /**
     *  Project-wide Gradle settings.
     *  IDE (e.g. Android Studio) users:
     *  Gradle settings configured through the IDE *will override*
     *  any settings specified in this file.
     *  在此属性文件中可修改一些Gradle默认的属性,也可扩展属性
     */
    String GRADLE_PROPERTIES = "gradle.properties";

    /**
     * 以下三个属性基本上不会使用到
     */
    String SYSTEM_PROP_PREFIX = "systemProp";

    String DEFAULT_VERSION = "unspecified";

    String DEFAULT_STATUS = "release";
  1. 为Gradle扩展属性
    Gradle自带的默认属性肯定是无法满足开发需求的,我们可以为Gradle扩展各种各样的属性来管理整个项目
//先来看build.gradle中常见的一部分配置,此处只做演示删掉大部分部分配置

apply plugin: 'com.android.application'
android {
    compileSdkVersion 26
}
dependencies {
    implementation 'com.android.support:appcompat-v7:26.1.0'
}

//前面说过build.gradle在配置阶段会被解析成Project字节码,所以在这里的配置实际上是在Project类中编写代码,而不仅仅是配置。
//写代码的话,里面的一些写死的魔法值如编译sdk版本26,还有写死的字符串appcompat-v7依赖com.android.support:appcompat-v7:26.1.0都是不符合编码风格的
//尤其是许多依赖的版本号都是一样的,如各种support包的版本我们可能使用一样的,写死我们修改就需要修改多个地方,或者说多个module中都需要使用的属性,在每个module中写死更是如此
//所以我们可以像平时开发一样,定义一些属性,并在配置的时候使用定义好的属性
//直接定义属性
apply plugin: 'com.android.application'

def mCompileSdkVersion = 26
def libSupportV7 = 'com.android.support:appcompat-v7:26.1.0'
android {
    compileSdkVersion mCompileSdkVersion
}
dependencies {
   implementation libSupportV7
}
//使用ext闭包扩展属性
apply plugin: 'com.android.application'
ext{
   compileSdkVersion = 26
   libSupportV7 = 'com.android.support:appcompat-v7:26.1.0' 
}
android {
    compileSdkVersion this.compileSdkVersion
}
dependencies {
   implementation this.libSupportV7
}

//直接定义属性和使用ext闭包扩展属性没有本质的区别

//但当定义的属性是所有的module都需要使用的时候,使用ext的优势就可以提现出来了,我们可以在根工程中使用ext扩展这些属性,然后在子工程中利用前面提到的rootProject来使用这些属性
//定义在根工程
ext{
   compileSdkVersion = 26
   libSupportV7 = 'com.android.support:appcompat-v7:26.1.0' 
}

//在子工程中使用
apply plugin: 'com.android.application'
android {
    compileSdkVersion this.rootProject.compileSdkVersion
}
dependencies {
   implementation this.rootProject.libSupportV7
}

//注意此处也可以省略掉rootProject,直接像上面使用this调用,因为在Gradle中,根工程定义的属性会被子工程继承
//一个项目可能有很多module,需要有很多的依赖和其它配置,所有的属性都写在根工程中可能导致根工程的build.gradle文件内容异常的多,不容易查找
//可以像上传library到Maven一样,将这些属性都定义到一个单独的文件中,然后在根工程中引入即可。

//根目录中新建commom_properties.gradle文件(定义两个map)
ext{
    android     =  [compileSdkVersion:26,
                    buildToolsVersion:'26.0.2']
               
    dependencies = [libSupportV7:'com.android.support:appcompat-v7:26.1.0' ,
                   libConstraintLayout:'com.android.support.constraint:constraint-layout:1.1.3']
}

//在根工程中引入
apply from:this.file('commom_properties.gradle') //file方法从当前project目录中查找文件

//在子工程中使用(ext中定义了各种map,直接使用key访问值)
apply plugin: 'com.android.application'
android {
    compileSdkVersion rootProject.ext.android.compileSdkVersion
    buildToolsVersion rootProject.ext.android.buildToolsVersion
}
dependencies {
   implementation rootProject.ext.dependencies.libSupportV7
   implementation rootProject.ext.dependencies.libConstraintLayout
}
//在gradle.properties文件中修改属性
//Android Studio 3.0会在debug apk的manifest文件application标签里自动添加
//android:testOnly="true"属性,导致IDE中run跑出的apk在大部分手机上只能用
//adb install -t <apk>来安装,在oppo手机上甚至安装不了
//解决办法:在gradle.properties文件中修改该属性

android.injected.testOnly=false //修改testOnly属性为false


//添加属性,gradle.properties除了可以修改已有属性之外,还可添加属性,全局可用
//在gradle.properties文件中添加
mApplicationId = wen.github.ecommerce
mVersionCode = 1
mCompileSdkVersion = 26

//在工程中使用
apply plugin: 'com.android.application'
android {
   compileSdkVersion mCompileSdkVersion
   defaultConfig{
       applicationId mApplicationId
       versionCode mVersionCode.toInteger()
   }
}

  • 对于gradle属性文件gradle.properties,想要了解更多点击构建环境
  • 另外一个local.properties,用于构建系统配置本地环境属性,例如SDK安装路径。由于该文件的内容由AS自动生成并且专用于本地开发者环境,因此不应手动修改该文件,或将其纳入版本控制系统。
  • File相关API
    用于当前Project下一些文件的处理
  1. 路径获取相关API
//获取根工程的路径
println getRootDir().absolutePath
//获取build目录路径
println getBuildDir().absolutePath
//获取当前工程路径
println getProjectDir().absolutePath
获取文件路径.png
  1. 文件操作相关API
//查找文件

//在根工程中
println getFileContent('gradle.properties') //输出gradle.properties的内容

def getFileContent(String path) {
    try {
        def file = file(path)
        return file.text
    } catch (GradleException e) { //file()方法在当前project目录下找,找不到会报FileNotFound异常
        println "File $path not found"
    }
    return null
}

//files()方法,接收多个路径参数,基于当前project工程查找多个文件,返回一个collection,使用与file()方法一致
gradle.properties内容.png
//文件的拷贝,讲解Groovy语法之文件操作中通过读取文件中的内容,然后写入到目标文件实现
//Gradle提供了更加简便的方法:copy()

//app module的build.gradle文件中
copy {
    from file('build/outputs/') //可以是文件也可以是目录
    into getRootProject().getBuildDir().path + '/rootOutputs/' //目标路径
    include '**/*.apk'   //选择要复制的文件
    include '**/*.json' //选择要复制的文件
    exclude { detail -> detail.file.name.contains('json') } //排除
    rename { 'aaa.apk'} //重命名
}

//将生成的outputs文件目录整个拷贝到根工程的build目录下的rootOutputs文件夹中
  • 源路径文件

    源路径文件.png
  • 目标路径文件

    目标路径文件.png
//文件树的遍历
fileTree("src/main/java") {//基于基准目录创建文件树
    FileTree fileTree ->
        fileTree.visit {//访问文件树的元素
            FileTreeElement element ->
                println element.file.name
        }
}


//输出结果
'''
wen
github
ecommerce
ECApplication.java
MainActivity.java

'''

//Android项目的根工程的build.gradle中的buildscript块相信大家都见过,新建的项目的时候会自动为我们配置一些东西

//根工程的build.gradle中
buildscript {
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.0.1'
        
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

//为什么这样写不知道大家有没有考虑过,里面又能配置些什么
//buildscript块实际上就是Project.buildscript()方法相信大家都可以理解,点进方法的源码可以看到这个闭包接收一个ScriptHandler类型的参数,那么能够配置的东西就是ScriptHandler类里面提供的给我们的,主要有两个配置方法repositories和dependencies。
//这个buildscript块写完整实际上是下面这样,至于为什么可以去掉scriptHandler是因为闭包中的delegate就是去掉的scriptHandler,在闭包中只要this,owner,delegate中有的方法都是可以调用的,区别是调用的顺序根据闭包委托策略决定(可以到Groovy语法中讲解闭包的this,owner,delegate部分了解)。

buildscript{ ScriptHandler scriptHandler ->
    //配置工程的仓库地址
    scriptHandler.repositories {}
    
    //配置工程的“插件”依赖地址
    scriptHandler.dependencies {}
}

//同样的点进repositories和dependencies方法的源码可查看这两个方法接收的参数以及可配置的内容

buildscript { ScriptHandler scriptHandler ->
    //配置工程的仓库地址
    scriptHandler.repositories { RepositoryHandler repositoryHandler ->
        //一个项目可以有好几个库. Gradle 会根据依赖定义的顺序在各个库里寻找它们。在第一个库里找到了就不会再在第二个库里找它了
        //所以下面都是可选的,我们项目添加的那些依赖库存放在哪个仓库就需要在这配置,Gradle才能帮我们找到这些依赖库并下载下来
        repositoryHandler.google()  //google仓库
        repositoryHandler.jcenter()  //jcenter库
        repositoryHandler.mavenCentral() //maven中心库
        repositoryHandler.mavenLocal() //本地maven库
        repositoryHandler.maven { url "https://maven.google.com" } //配置maven私有仓库
        repositoryHandler.maven { url "https://jitpack.io" } //可以写多个(可理解为多次调用该方法)
        repositoryHandler.maven {
            name '公司名称'
            url "公司私有的maven仓库地址"
            credentials { //配置仓库用户名和密码
                username = 'myName'
                password = 'myPassword'
            }
        }
        repositoryHandler.ivy {} //ivy仓库
        flatDir { dirs '../${项目名称}/libs' } // aar等引入 ,这里只是声明了存放aar的路径,还需要在使用到aar的module的build.gradle的dependencies中引入对应的aar( implementation(name: 'aar名字', ext: 'aar')  )
        //注意如果是library类型的module中引入了aar,除了在该library module中引入,依赖了这个library的module中也需要再次引入
    }

    //配置工程的“插件”依赖地址,需要注意在这是scriptHandler的dependencies方法,而Project也有一个dependencies方法(即app module中的dependencies块)。
    //两者区别:我们的应用程序的开发都会引入各种第三方的依赖库,这些依赖库在Project的dependencies方法中配置,
    //而Gradle实际上也是一个编程框架,那么在开发Gradle工程时也需要一些第三方的插件,这些插件就在scriptHandler的dependencies方法中配置
    scriptHandler.dependencies {
        classpath 'com.android.tools.build:gradle:3.0.1' //引入这个gradle之后就可以将我们的工程指定为android类型module或者是library类型module
        classpath 'com.tencent.tinker:tinker-patch-gradle-plugin:1.9.1' //腾讯热修复插件,引入热修复补丁包生成插件工具
    }
}


//引用一个外部依赖需要使用 group, name 和 version 属性. 如下
dependencies {
    compile group: 'com.android.support', name: 'appcompat-v7', version: '26.1.0'
}

//有一种简写形式,只使用一串字符串 "group:name:version".如下,一般都是使用这种方式进行配置
dependencies {
    compile 'com.android.support:appcompat-v7:26.1.0'
}


//app工程的build.gradle中
dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs') //添加文件树依赖(本地的jar包),另外还可以添加file和files依赖,取决于添加的依赖是文件树还是单一文件或者多个文件
    implementation 'com.android.support.constraint:constraint-layout:1.1.3' //依赖远程jar包
    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' //android中的测试
    implementation project(':ec')  //依赖源码库工程
    implementation project(':core') //依赖源码库工程
    annotationProcessor project(':compiler') //编译期注解的依赖
    //optional, help to generate the final application 
    provided('com.tencent.tinker:tinker-android-anno:1.9.1') //为了在编译的时候生成一个Application类,使用时不需要
    //tinker's main Android lib
    compile('com.tencent.tinker:tinker-android-lib:1.9.1')
    debugCompile 'com.amitshekhar.android:debug-db:1.0.0' //数据库debug
}

  • Android Studio 2.x 版本和3.x依赖指令的区别

  • 2.x版本

  1. compile
    参与编译和打包,使用compile方式依赖的第三方库中所有的类,资源文件等都会被打包到项目的最终产物中(apk,jar,aar等)
  2. provided
    编译占位,只参与编译的过程,不会打包到apk,适用于只在编译的时候起作用在运行期不需要使用的库,比如Tinker的tinker-android-anno,只为了在编译的时候生成需要的Application类,运行时就不再需要这个工具了,另外适用于当前工程需要引入一个主工程已经引入的库时,比如在library module中作为工具类,先用provided占位,编译通过,在使用的时候由用户根据需求,如果要使用该功能的话,再引入一次。
  3. apk
    只在生成apk的时候参与打包,编译时不会参与
  4. testCompile
    参与单元测试代码的编译以及最终打包测试apk
  5. debugCompile
    参与debug模式的编译和最终的debug apk打包
  6. releaseCompile
    参与release模式的编译和最终的release apk打包
  • 3.x版本
  1. implementation
    对于使用了该命令编译的依赖,对该项目有依赖的项目将无法访问到使用该命令编译的依赖中的任何程序,即将该依赖隐藏在内部,而不对外部公开(非传递依赖)
    简单来说,从Android Studio 3.X开始,依赖首先应该设置为implement,如果没有错那就用implement,如果有错,那么使用api指令,这样会使编译速度有所增快。假设有A,B,C三个module,B,C依赖于A,那么在A中使用implementation依赖的库,在B,C中无法直接使用,在A中使
    用implementation依赖的库更改时,只在A中重新编译,在B,C中不需要编译所以会加快编译速度
  2. api
    同2.x版本的compile,参与编译和打包,使用compile方式依赖的第三方库中所有的类,资源文件等都会被打包到项目的最终产物中(apk,jar,aar等)
  3. compileOnly
    同2.x版本的provided,编译占位,只参与编译的过程,不会打包到apk,适用于只在编译的时候起作用在运行期不需要使用的库,另外适用于当前工程需要引入一个主工程已经引入的库时
  4. runtimeOnly
    同2.x版本的apk,只在生成apk的时候参与打包,编译时不会参与
  5. testImplementation
    同2.x版本的testCompile,参与单元测试代码的编译以及最终打包测试apk
  6. debugImplementation
    同2.x版本的debugCompile,参与debug模式的编译和最终的debug apk打包
  7. releaseImplementation
    同2.x版本的releaseCompile,参与release模式的编译和最终的release apk打包
  8. annotationProcessor
    编译注解依赖,只参与编译过程,不会打包到apk,目的是在编译时动态生成代码。
  • compileOnly(provided)annotationProcessor的区别
    都是只参与编译期,不会打包进apk产物,而annotationProcessor只是为了在编译期生成代码,compileOnly(provided)是在引入一个已经被其它工程(主工程)引入的库,那么使用该依赖指令让该类库在编译期使用,保证编译通过,而在运行期使用时使用的是被其它工程引入的库,保证只引入该类库一次。
  • 依赖冲突和传递依赖的解决

    当我们不小心重复引入了第三方库时,而这两个第三方库的版本不同的时候,就会导致依赖冲突

    我们依赖的第三方库又依赖了其它第三方库,而它依赖的第三方库是我们不需要使用的库,这个时候就需要解除传递依赖
//版本依赖冲突解决
//1. 强制依赖指定版本
configurations.all {
  resolutionStrategy.force 'com.android.support:support-annotations:26.1.0'
}

//完整写法,排除multidex
 configurations.all {
        resolutionStrategy.eachDependency { DependencyResolveDetails details ->
            def requested = details.requested
            if (requested.group == 'com.android.support') {
                if (!requested.name.startsWith("multidex")) {
                    details.useVersion '26.1.0'
                }
            }
        }
    }


//2. 排除
implementation ('com.squareup.retrofit2:adapter-rxjava:2.1.0'){
        exclude group: 'io.reactivex' //排除指定包下的所有库
    }
implementation 'io.reactivex.rxjava2:rxjava:2.0.6'
implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'

//另外如果冲突的是其中的一个module
implementation ('com.squareup.retrofit2:adapter-rxjava:2.1.0'){
        exclude module: '冲突的module name'  //排除指定module
    }

//传递依赖
//假设有工程A,B,C,工程A依赖了B,而B又依赖了C,这个时候就构成了传递依赖
//而在开发中一般是不允许A直接使用C的,因为可能B升级了之后,不需要再依赖C了,如果在A中直接使用了工程C中的东西的话,就会导致各种编译错误,因为B已经没有依赖C了,在A中使用的C中的东西都已经找不到了
conpile ('com.squareup.retrofit2:adapter-rxjava:2.1.0'){
        transitive false //禁止传递依赖
    }
//在Gradle中默认transitive就是false,另外在Android Studio 2.x 版本和3.x依赖指令的区别部分,我们也可以知道,使用implementation,本身也是非传递依赖,在B中使用implementation依赖C,A再依赖B,本身C中的API那些就无法在A中直接使用

  • 查看冲突的依赖库
  1. 打印出该模块所有的依赖树信息:gradlew -q <模块名>:dependencies
  2. 将项目切换至Project面板,在External Libraries目录下查看
  3. 项目的Gradle Projects面板中运行 Tasks->android->androidDependencies
  • 外部命令的执行相关API
// 使用exec()方法执行外部命令,对于其它的javaexec方法,只要配置好环境变量,实际上通过exec方法也是可以执行的

task(name:'apkcopy'){ //创建一个名为apkcopy的task
    doLast{  //在task的最后执行
        def sourcePath = this.buildDir.path+'/outputs/apk'
        def destinationPath = '/Users/wen/apks' //要存放的文件路径
        def command = "mv -f $sourcePath $destinationPath" //命令行
        exec{
            try {
                executable 'bash'  //要使用的可执行文件的名称
                args '-c',command  //要执行的命令的参数。默认为空列表
                println 'the command is execute success'
            } catch (GradleException e) {
                println 'the command is execute failed'
            }
        }
    }
}


  • 执行外部命令更多细节请点这里

  • Task相关API
    为Project提供新增Task和使用已有Task的能力。了解更多点击 Gradle Task

  1. 什么是Task
    一个Task表示构建的单个原子动作。构建工作就是由各种Task构成的。我们也可以定义各种各样的Task。在Gradle写的其它脚本都会在配置阶段被执行(利用生命周期的监听除外),并且执行的顺序不确定,而利用Task可以在执行阶段执行我们需要的逻辑,可并为Task指定执行顺序。除此之外我们还可以为我们自己编写的Task指定类型来让Task具有更强的功能。
  1. Task定义和配置
//定义

//1. 通过task方法创建 
task getDateTask {
    println new Date().dateString //在终端输入gradlew getDateTask 执行Task输出:18-10-16
}


//2.通过TaskContainer创建
this.tasks.create(name: 'getDateTask2') {
    println "getDateTask2 date: ${new Date().dateString}" 
}


//TaskContainer是用来管理一个Project中所有的Task的,提供创建和查找task功能。查找对应task最好是在配置阶段之后,此时所有的task都配置好了。否则可能出现找不到对应的task
task执行结果.png
//配置
//1. 创建的时候直接配置
task getDateTask(group:'learn',description:'task study') {
    println new Date().dateString
}

task clean(type: Delete) { //配置类型
    delete rootProject.buildDir
}

//2. 方法配置
task getDateTask2 {
    setGroup('learn') //配置组
    setDescription('task study') //配置描述
    println new Date().dateString
}

  • 配置组之后同一组的Task会被放到同一目录下(右侧gradle面板中可以查看),description相当于注释,解释该task的作用


    task分组.png
  • Task所有可配置信息
    String TASK_NAME = "name"; //名字

    String TASK_DESCRIPTION = "description"; //描述

    String TASK_GROUP = "group"; //所属组

    String TASK_TYPE = "type";  //类型

    String TASK_DEPENDS_ON = "dependsOn";  //task依赖(依赖于其它task)

    String TASK_OVERWRITE = "overwrite"; //是否重写其它task 默认false

    String TASK_ACTION = "action"; //配置task相关逻辑
  • doFirst和doLast方法

    Task可以执行在执行阶段,注意前面只执行getDateTask2,getDateTask也输出了,因为在配置阶段就会解析所有的Task用于构建task拓扑图,所以这些代码会在配置阶段就被执行。下面利用doFirst和doLast方法让脚本代码执行在执行阶段
task getDateTask(group: 'learn', description: 'task study') {
    println new Date().dateString
    //在闭包中调用
    doFirst { println "the group is:$group" }
}
//在闭包外调用
getDateTask.doFirst { println "the description is:$description" }
dofist.png
  • 可以看到,task中没有被doFirst包裹的代码依旧在配置阶段就被执行了,而doFirst中的代码是在执行app中的getDateTask时执行。并且在闭包外调用的doFirst先执行

  • 至于doFirst和doLast的执行区别看下面执行结果相信大家就能明白了

task getDateTask(group: 'learn', description: 'task study') {
    println new Date().dateString
    doLast {println "the description2 is:$description"}
    //在闭包中调用
    doFirst { println "the group1 is:$group" }
    doFirst { println "the group2 is:$group" }
}
//在闭包外调用
getDateTask.doFirst { println "the description1 is:$description" }
doFirst和doLast的区别.png
  • doFirst和doLast最常用的地方是让我们的脚本代码执行在已有Task的执行之前或执行之后
//统计项目构建时间
def startBuildTime, endBuildTime
this.afterEvaluate { Project project ->
    //在配置阶段之后(所有的task都被配置好,构建了task拓扑图)查找task 防止找不到对应task
    def preBuildTask = project.tasks.getByName('preBuild') //构建最先执行preBuild task
    def buildTask = project.tasks.getByName('build') //构建最终执行build task

    preBuildTask.doFirst {
        startBuildTime = System.currentTimeMillis()
        println "build start,current time is $startBuildTime"
    }

    buildTask.doLast {
        endBuildTime = System.currentTimeMillis()
        println "build end,current time is $endBuildTime"

        println "The build took ${(endBuildTime - startBuildTime) / 1000} seconds"
    }
}

项目构建时间.png
  • Task执行顺序

    在Gradle的配置阶段是构建所有Task的拓扑图,实际上可以理解为是在确定Task的执行顺序。那么Gradle是中各个Task的执行顺序是如何确定的。
  1. dependsOn配置依赖决定执行顺序

    在前面Task所有可配置内容中有一个dependsOn属性,这个属性是用于配置Task依赖的,执行一个task的时候,这个task所依赖的task会先被执行,但是执行它依赖的task对它是不会造成影响的
task taskA {
    doLast {
        println 'taskA run'
    }
}

task taskB {
    doLast {
        println 'taskB run'
    }
}

task taskC(dependsOn:[taskA,taskB]) { //为taskC配置依赖
    doLast {
        println 'taskC run'
    }
}
  • 可以看到单独执行没有依赖的taskB只会执行taskB,而执行依赖了taskA、taskB的taskC,执行之前首先会先执行它依赖的taskA、taskB,而taskA和taskB的执行顺序实际上是随机的


    Task依赖dependsOn.png
  • 当事先不太清楚具体要依赖哪些Task,可以动态指定
//动态的指定依赖

task taskA(group:'gradle'){
    doLast {
        println 'taskA run'
    }
}

task taskB(group:'gradle'){
    doLast {
        println 'taskB run'
    }
}

task taskC(group:'java'){
    doLast {
        println 'taskC run'
    }
}

task taskD { //动态的指定task依赖
    dependsOn this.tasks.findAll {task -> task.group == 'gradle' }
    doLast {
        println 'taskD run'
    }
}
动态指定依赖.png
  1. mustRunAfter()、shouldRunAfter() API指定执行顺序

    需要注意的是,使用mustRunAfter和shouldRunAfter是在两个task都会执行的情境下会按照该指定顺序执行,调用了该Api指定依赖执行顺序的两个task并没有依赖关系。单独执行其中一个对另外的任务并不会有影响,但如果mustRunAfter和task依赖之间发生了冲突,那么执行时将会报错
task taskA{
    doLast{
        println 'taskA run'
    }
}

task taskB{
    doLast{
        println 'taskB run'
    }
}

task taskC{
    doLast{
        println 'taskC run'
    }
}

taskB.mustRunAfter(taskC)
taskA.mustRunAfter(taskB)
API指定task执行排序.png
  • API指定task的排序,并不会指定它们之间的依赖关系,所以只执行其中一个task,'必须在它之前执行的'task不会先被执行,而执行task,它依赖的task才会先被执行,下面只执行taskA,taskB和taskC都不会执行
非依赖执行.png
  • 常利用mustRunAfter()和dependsOn将我们的task插入到生命周期的task之间去完成特定的需求
//假设Gradle构建的生命周期中有taskA和taskB,taskA执行之后会执行taskB,,那么需要将myTask,插入taskA和taskB中间

//以下是伪代码
project.afterEvaluate{
  
    //1.找到taskA和taskB
    //可以通过TaskContainer的findTaskByName方法或其它方式获取到响应的task
    //假设已找到taskA和taskB
    
    //2.将myTask插入到taskA之后
    myTask.mustRunAfter taskA
    
   //3. 将myTask插入到taskB之前
   taskB.dependsOn myTask
}

  1. finalizedBy

    如果需要实现执行某一个task之后必须执行另一个task,用上面的方法是没办法的,此时可以利用finalizedBy API
task taskA{
    doLast{
        println 'taskA run'
    }
}

task taskB{
    doLast{
        println 'taskB run'
    }
}

//执行完taskA之后必须执行taskB
taskA.finalizedBy taskB
执行taskA后taskB跟着执行.png
  • TaskInputs、TaskOutputs
    输入输出在Gradle中用于增量构建,执行一个任务,如果在编译时,gradle判断在从上一次编译中,该task的输入输出没有任何改变,那么gradle就会跳过该task,前提是这个task至少有一个输入或输出

  • Task源码中获取输入输出的方法

  /**
     * <p>Returns the inputs of this task.</p>
     *
     * @return The inputs. Never returns null.
     */
    @Internal
    TaskInputs getInputs();

    /**
     * <p>Returns the outputs of this task.</p>
     *
     * @return The outputs. Never returns null.
     */
    @Internal
    TaskOutputs getOutputs();
  • TaskInputs支持的输入类型
  /**
     * 返回所有task的输入文件
     *
     * @return 返回输入文件,如果task没有输入文件返回一个空的文件集合
     */
    FileCollection getFiles();

    /**
     * 指定该task的输入文件(多个文件)
     *
     * @param 输入文件路径
     * @return 返回property builder以进一步配置属性
     */
    TaskInputFilePropertyBuilder files(Object... paths);

    /**
     *指定该task的输入文件(一个文件)
     *
     * @param 输入文件路径. 
     * @return 返回property builder以进一步配置属性
     */
    TaskInputFilePropertyBuilder file(Object path);

    /**
     * 为该task注册一个输入目录。在给定目录下找到的所有文件都被视为输入文件
     *
     * @return 返回property builder以进一步配置属性
     */
    TaskInputFilePropertyBuilder dir(Object dirPath);

    /**
     * 返回该task的输入属性集。
     *
     * @return The properties.
     */
    Map<String, Object> getProperties();

    /**
     * 为该task注册一个输入属性。该属性值在task执行时持久化,并task的后
     * 调用的属性值进行比较,以确定task是否最新。
     *
     *属性的给定值必须是可序列化的,并且应该提供一个有效的equal()方法
     *
     *也可指定一个闭包作为属性的值,在这种情况下会执行该闭包用于确定属性的值
     *
     * @param name The name of the property. Must not be null.
     * @param value The value for the property. Can be null.
     */
    TaskInputs property(String name, Object value);

    /**
     * 为该Task注册一个输入属性集合
     *
     * @param properties The properties.
     */
    TaskInputs properties(Map<String, ?> properties);
    
    //注意TaskInputFilePropertyBuilder继承于TaskInputs,用于描述包含零个或多个文件的task的输入属性。
  • TaskOutputs支持的输出类型
  /**
     * 返回该Task的所有输出文件
     *
     * @return The output files. Returns an empty collection if this task has no output files.
     */
    FileCollection getFiles();

    /**
     * 指定该Task的输出是文件(多个)
     *
     *如果是一个map,key必须是有效的java标识符,否则task输出缓存会被禁用
     *
     * @param paths The output files.
     *
     * @see CacheableTask
     */
    TaskOutputFilePropertyBuilder files(Object... paths);

    /**
     * 该Task输出是文件目录
     *
     *
     * @param paths The output files.
     *
     * @see CacheableTask
     *
     */
    TaskOutputFilePropertyBuilder dirs(Object... paths);

    /**
     * 指定该Task的输出是文件(单个)
     *
     */
    TaskOutputFilePropertyBuilder file(Object path);

    /**
     * task输出文件是dir
     */
    TaskOutputFilePropertyBuilder dir(Object path);
    
    //注意TaskOutputFilePropertyBuilder继承于TaskOutputs,用于描述包含零个或多个文件的task的输出属性。
  • 总结:TaskInputs支持单个文件、多个文件和文件目录以及以key-value形式的单一属性和key-value形式的属性map,需要注意这些key-value都需要是serializable的,因为需要记录起来;TaskOutputs支持单个文件、多个文件和文件目录
  • SourceSet修改工程中各项资源文件的位置
    当我们新建一个Android项目的时候,AS帮我们创建一系列的目录和文件,比如放置代码的java目录,放置资源的res目录,main目录下AndroidManifest.xml文件等,那么为什么编写的代码文件就需要放置在java目录,资源就要放入res目录呢?是因为编译项目时,Gradle默认会从这些相应的目录中加载相应的代码和资源,这是约定好的,如果我们没有做任何改动,就会按照它默认的方式去加载代码和资源。所以我们可以根据我们的需要修改这些默认的选项,借助SourceSet提供的方法,我们可以按照我们想要的方式来配置代码和资源的加载目录
  //在AS中,gradle默认去加载jniLibs目录中的 *.so 文件
    sourceSets { //soutceSet方法重定位资源文件的位置
        main {//jniLibs在main目录下,所以需要调用main方法
            jniLibs.srcDirs = ['libs'] //修改到libs目录中加载*.so文件
        }
    }
    
 //修改res的目录,有时候为了便于管理,单独的一个模块的资源文件都放在一个单独的目录中,想要gradle能够正确识别到这些文件,需要而外添加进来   
     sourceSets {
        main {
        //指定从多个路径加载资源文件
            res.srcDirs = [  
                    'src/main/res/chat-res',
                    'src/main/res'
            ]
        }
    }
  • 构建变体
    对于Android开发者来说,项目最终打包成APK,了解这个APK的各个属性的配置尤为重要。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容