Android拾萃Flutter学习

gradle使用教程,一篇就够

2021-08-30  本文已影响0人  三也视界

概述

Gradle是新一代构建工具,从0.x版本一路走来虽然国内可寻的资料多了一些,但都是比较碎片化的知识。官方的Userguide虽然是业内良心之作,但无奈太长,且版本变化较快,又鉴于很多同学一看到英文内心便已认定无法读懂,遂打算利用业余时间攒此本《跟我学gradle》,希望通过此书可以降低学习曲线能让希望使用Gradle的同学更轻易地入门。

简介

Gradle是继Maven之后的新一代构建工具,它采用基于groovy的DSL语言作为脚本,相比传统构建工具通过XML来配置而言,最直观上的感受就是脚本更加的简洁、优雅。如果你之前对Maven有所了解,那么可以很轻易的转换到Gradle,它采用了同Maven一致的目录结构,可以与Maven一样使用Maven中央仓库以及各类仓库的资源,并且Gradle默认也内置了脚本转换命令可以方便的将POM转换为gradle.build。

Gradle的优势

依赖管理:即将你项目中的jar包管理起来,你可以使用Maven或者Ivy的远程仓库、或者本地文件系统等
编译打包:可以通过脚本实现花样打包,包括修改文件、添加抑或排除某些类或资源、采用指定JDK版本构建、打包后自动上传等等等等
多项目支持: Gradle对多项目有着良好的支持,比如一个很具有代表性的实践就是spring framework
多语言支持:无论是java、groovy、scala、c++都有良好的支持
跨平台支持:gradle是基于jvm的,只要有jvm你就可以让gradle运行
灵活的的脚本:你可以使用groovy灵活的编写任务完成你想要做的任何事情

约定优于配置

约定优于配置(convention over configuration),简单而言就是遵循一定的固定规则从而可以避免额外的配置。虽然这一定程度上降低了灵活性,但却能减少重复的额外配置,同时也可以帮助开发人员遵守一定的规则。当然,约定并不是强制性约束,Gradle提供了各种灵活的途径可以让你更改默认的配置。

标准结构

Gradle遵循COC(convention over configuration约定优于配置)的理念,默认情况下提供了与maven相同的项目结构配置
大体结构如下

project root
src/main/java(测试)
src/main/resources
src/test/java(测试源码目录)
src/test/resources(测试资源目录)
src/main/webapp(web工程)

非标准结构配置

在一些老项目上,可能目录结构并不是标准结构,然而一般开发人员又不好进行结构调整.此时可以通过配置sourceSet来指定目录结构

sourceSets {
    main {
        java {
            srcDir 'src/java'
        }
        resources {
            srcDir 'src/resources'
        }
    }
}

或者采用如下写法也是可以的

sourceSets {
    main.java.srcDirs = ['src/java']
    main.resources.srcDirs = ['src/resources']
}

在android中

android {
    sourceSets {
        main {
            manifest.srcFile 'AndroidManifest.xml'
            java.srcDirs = ['src']
            resources.srcDirs = ['src']
            aidl.srcDirs = ['src']
            renderscript.srcDirs = ['src']
            res.srcDirs = ['res']
            assets.srcDirs = ['assets']
        }

        androidTest.setRoot('tests')
    }
}

当然如果你的资源目录与源码目录相同这样就比较....了,但你仍然可以按照如下方式搭配include和exclude进行指定

sourceSets {
  main {
    java {
      //your java source paths and exclusions go here...
    }

    resources {
      srcDir 'main/resources'
      include '**/*.properties'
      include '**/*.png'


      srcDir 'src'
      include '**/Messages*.properties'
      exclude '**/*.java'
    }
  }
}

一个简单的Gralde脚本,或许包含如下内容,其中标明可选的都是可以删掉的部分

//定义扩展属性(给脚本用的脚本)
buildScript {
    repositories {
         mavenCentral()
    }
}
//应用插件,这里引入了Gradle的Java插件,此插件提供了Java构建和测试所需的一切。
apply plugin: 'java'
//定义扩展属性(可选)
ext {
    foo="foo"
}
//定义局部变量(可选)
def bar="bar"

//修改项目属性(可选)
group 'pkaq'
version '1.0-SNAPSHOT'

//定义仓库,当然gradle也可以使用各maven库 ivy库 私服 本地文件等,后续章节会详细介绍(可选)
repositories {
    jcenter()
}

//定义依赖,这里采用了g:a:v简写方式,加号代表了最新版本(可选)
dependencies {
    compile "cn.pkaq:ptj.tiger:+"
}

//自定义任务(可选)
task printFoobar {
    println "${foo}__${bar}"
}

buildscript 配置该 Project 的构建脚本的 classpath,在 Andorid Studio 中的 root project 中可以看到
apply(options: Map<String, ?>)我们通过该方法使用插件或者是其他脚本,options里主要选项有:

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

apply plugin: 'com.android.application'
//使用插件,MyPluginImpl 就是一个Plugin接口的实现类
apply plugin: MyPluginImpl

//引用其他gradle脚本,push.gradle就是另外一个gradle脚本文件
apply from: './push.gradle'

扩展:
Gradle脚本是基于groovy的DSL,这里对上述脚本与Gradle api的关系稍作解释,希望可以帮助你更好的理解groovy与gradle dsl之间的关系

//project 的buildScript方法  实际返回的是一个ScriptHandler对象
buildScript {
    repositories {
         mavenCentral()
    }
}
//调用apply方法 参数是一个 map,调用时参数省略了括号
apply plugin: 'java'
//定义扩展属性(可选)
ext {
    foo="foo"
}
//定义局部变量(可选)
def bar="bar"

//修改项目属性(可选)
group 'pkaq'
version '1.0-SNAPSHOT'

//project 的repositories 方法  实际返回的是一个RepositoryHandler对象
repositories {
    jcenter()
}

//project 的dependencies 方法  实际返回的是一个DependencyHandler对象
dependencies {
    compile "cn.pkaq:ptj.tiger:+"
}

//调用Task task(String name, Closure configureClosure)方法
task printFoobar {
    println "${foo}__${bar}"
}

什么是依赖管理

通常而言,依赖管理包括两部分,对依赖的管理以及发布物的管理;依赖是指构建项目所需的构件(jar包等)。例如,对于一个应用了spring普通的java web项目而言,spring相关jar包即项目所需的依赖。发布物,则是指项目产出的需要上传的项目产物。

采用变量统一控制版本号

dependencies {
    def bootVersion = "1.3.5.RELEASE"
    compile     "org.springframework.boot:spring-boot-starter-web:${bootVersion}",  
                "org.springframework.boot:spring-boot-starter-data-jpa:${bootVersion}",
                "org.springframework.boot:spring-boot-starter-tomcat:${bootVersion}"
}

自动获取最新版本依赖

如果你想某个库每次构建时都检查是否有新版本,那么可以采用+来让Gradle在每次构建时都检查并应用最新版本的依赖。当然也可以采用1.x,2.x的方式来获取某个大版本下的最新版本。

dependencies {
    compile     "org.springframework.boot:spring-boot-starter-web:+"
}

依赖的坐标

仓库中构件(jar包)的坐标是由configurationName "group:name:version:classifier@extension"组成的字符串构成,如同Maven中的GAV坐标,Gradle可借由此来定位你想搜寻的jar包。

在gradle中可以通过以下方式来声明依赖:

testCompile group: 'junit', name: 'junit', version: '4.0'

在gradle中可以通过以下方式来声明依赖:

testCompile group: 'junit', name: 'junit', version: '4.0'
项目 描述
configurationName 依赖的作用范围,具体介绍看本章第二小节
group 通常用来描述组织、公司、团队或者其它有象征代表意义的名字,比如阿里就是com.alibaba,一个group下一般会有多个artifact
name 依赖的名称,或者更直接来讲叫包名、模块、构件名、发布物名以及随便你怎么称呼。druid就是com.alibaba下的一个连接池库的名称
version 见名知意,无它,版本号。
classifier 类库版本,在前三项相同的情况下,如果目标依赖还存在对应不同JDK版本的版本,可以通过此属性指明
extension 依赖的归档类型,如aar、jar等,默认不指定的话是jar

这是由于Gradle依赖配置支持多种书写方式,采用map或者字符串。

// 采用map描述依赖
testCompile group: 'junit', name: 'junit', version: '4.0'
// 采用字符串方式描述依赖
testCompile 'junit:junit:4.0'

显然采用字符串的方式更加简单直观,当然借助groovy语言强大的GString还可以对版本号进行抽离。如下面的示例,这里需要注意的是如果要用GString的话,依赖描述的字符串要用""双引号包起来才会生效。

def ver = "4.0"
testCompile "junit:junit:${ver}"

依赖的范围

上面的例子中采用的testComplie是声明依赖的作用范围,关于各种作用范围的功效可见下表。

tip:这里需要注意的是,provided范围内的传递依赖也不会被打包

名称 说明
compileOnly gradle2.12之后版本新添加的,2.12版本时期曾短暂的叫provided,后续版本已经改成了compileOnly,由java插件提供,适用于编译期需要而不需要打包的情况
providedCompile war插件提供的范围类型:与compile作用类似,但不会被添加到最终的war包中这是由于编译、测试阶段代码需要依赖此类jar包,而运行阶段容器已经提供了相应的支持,所以无需将这些文件打入到war包中了;例如Servlet API就是一个很明显的例子.
api 3.4以后由java-library提供 当其他模块依赖于此模块时,此模块使用api声明的依赖包是可以被其他模块使用
implementation 3.4以后由java-library提供 当其他模块依赖此模块时,此模块使用implementation声明的依赖包只限于模块内部使用,不允许其他模块使用。
compile 编译范围依赖在所有的classpath中可用,同时它们也会被打包。
providedRuntime 同proiveCompile类似。
runtime runtime依赖在运行和测试系统的时候需要,但在编译的时候不需要。比如,你可能在编译的时候只需要JDBC API JAR,而只有在运行的时候才需要JDBC驱动实现。
testCompile 测试期编译需要的附加依赖
testRuntime 测试运行期需要
archives -
default 配置默认依赖范围

依赖的分类

类型 描述
外部依赖 依赖存放于外部仓库中,如jcenter ,mavenCentral等仓库提供的依赖
项目依赖 依赖于其它项目(模块)的依赖
文件依赖 依赖存放在本地文件系统中,基于本地文件系统获取依赖
内置依赖 跟随Gradle发行包或者基于Gradle API的一些依赖,通常在插件开发时使用
子模块依赖 还没搞清楚是什么鬼

外部依赖

可以通过如下方式声明外部依赖,Gradle支持通过map方式或者g:a:v的简写方式传入依赖描述,这些声明依赖会去配置的repository查找。

dependencies {
 // 采用map方式传入单个
  compile group: 'commons-lang', name: 'commons-lang', version: '2.6'
 // 采用map方式传入多个
  compile(
      [group: 'org.springframework', name: 'spring-core', version: '2.5'],
      [group: 'org.springframework', name: 'spring-aop', version: '2.5']
  )
  // 采用简写方式声明
  compile 'org.projectlombok:lombok:1.16.10' 
  // 采用简写方式传入多个 
  compile 'org.springframework:spring-core:2.5',
          'org.springframework:spring-aop:2.5'

}

项目依赖

此类依赖多见于多模块项目,书写方式如下,其中:是基于跟项目的相对路径描述符。

 compile project(':project-foo')

文件依赖

依赖存在于本地文件系统中,举个栗子,如oracle的OJDBC驱动,中央仓库中没有又没有自建私服此时需要放到项目lib下进行手工加载那么便可采用此种方式,可以通过FileCollection接口及其子接口提供的方法加载这些依赖(支持文件通配符)

dependencies {
   // 指定多个依赖
   compile files('hibernate.jar', 'libs/spring.jar')

   // 读取lib文件夹下的全部文件作为项目依赖
   compile fileTree('libs')

   // 根据指定基准目录\包含\排除条件加载依赖
   compile fileTree(dir:'libs',include:'spring*.jar',exclude:'hibernate*.jar')
 }

内置依赖

跟随Gradle发行包或者基于Gradle API的一些依赖,通常在插件开发时使用,当前提供了如下三种

 dependencies {
   // 加载Gradle自带的groovy作为依赖
   compile localGroovy()

   // 使用Gradle API作为依赖
   compile gradleApi()

   /使用 Gradle test-kit API 作为依赖
   testCompile gradleTestKit()
 }

子模块依赖

简单来说就是声明依赖的依赖或者依赖的传递依赖,一般情况下如果依赖的库并未用构建工具构建(尤其是一些上古时代的老库),那么Gradle是无法透过源文件去查找该库的传递性依赖的,通常而言,一个模块采用XML(POM文 件)来描述库的元数据和它的传递性依赖。Gradle可以借由此方式提供相同的能力,当然这种方式也会可以改写原有的传递性依赖。这里让druid连接池依赖了ptj.tiger的一个库。

dependencies {
    // 让ptj.tiger作为druid的传递性依赖
    compile module("com.alibaba:druid:1.0.26") {
            dependency("cn.pkaq:ptj.tiger:+")
    }

    runtime module("org.codehaus.groovy:groovy:2.4.7") {
        // 停用groovy依赖的commons-cli库的依赖传递
        dependency("commons-cli:commons-cli:1.0") {
            transitive = false
        }
        // 让groovy依赖的ant模块的依赖ant-launcher停用传递性依赖并依赖ant-junit..........
        module(group: 'org.apache.ant', name: 'ant', version: '1.9.6') {
            dependencies "org.apache.ant:ant-launcher:1.9.6@jar",
                         "org.apache.ant:ant-junit:1.9.6"
        }
    }
 }

什么是传递依赖

在Maven仓库中,构件通过POM(一种XML文件)来描述相关信息以及传递性依赖。Gradle 可以通过分析 该文件获取获取所以依赖以及依赖的依赖和依赖的依赖的依赖,为了更加直观的表述,可以通过下面的输出 结果了解。

image

可以看到,我们的项目依赖了com.android.support-v4包,然而com.android.support-v4包却依赖了一众support的全家桶,借助Gradle的传递性依赖特性,你无需再你的脚本中把这些依赖都声明一遍,你只需要简单的一行,Gradle便会帮你将传递性依赖一起下载下来。

传递依赖特性可以轻松地通过transitive参数进行开启或关闭,上面的示例中如果要忽略com.android.support-v4的传递性依赖可以采用指定 transitive = false 的方式来关闭依赖传递特性,也可以采用添加@jar的方式忽略该依赖的所有传递性依赖。

 compile('com.android.support:support-v4:23.1.1'){
        transitive = false
 }

 compile 'com.android.support:support-v4:23.1.1'@jar

当然,你也可以全局性的关闭依赖的传递特性。

 configurations.all {
   transitive = false
}

排除依赖

有些时候你可能需要排除一些传递性依赖中的某个模块,此时便不能靠单纯的关闭依赖传递特性来解决了。这时exclude就该登场了,如果说@jar彻底的解决了传递问题,那么exclude则是部分解决了传递问题。然而实际上exclude肯能还会用的频率更更频繁一些,比如下面几种情况。

可以通过configuration配置或者在依赖声明时添加exclude的方式来排除指定的引用。

exclude可以接收group和module两个参数,这两个参数可以单独使用也可以搭配使用,具体理解如下:

compile('com.github.nanchen2251:CompressHelper:1.0.5'){
        //com.android.support:appcompat-v7:23.1.1
        exclude group: 'com.android.support'//排除组织依赖
        exclude module: 'appcompat-v7'//排除模块依赖
 }

强制使用版本

当然,有时候你可能仅仅是需要强制使用某个统一的依赖版本,而不是排除他们,那么此时force就该登场了。指定force = true属性可以冲突时优先使用该版本进行解决。

compile('com.github.nanchen2251:CompressHelper:1.0.5'){
        force = true
 }

全局配置强制使用某个版本的依赖来解决依赖冲突中出现的依赖

configurations.all {
   resolutionStrategy {
       force 'com.github.nanchen2251:CompressHelper:1.0.5'
   }
}

另一个例子

//解决冲突 同一版本
configurations.all {
    resolutionStrategy.eachDependency { DependencyResolveDetails details ->
        def requested = details.requested
        if (requested.group == 'com.android.support') {
            if (requested.name.startsWith("support-")||
                    requested.name.startsWith("animated")||
                    requested.name.startsWith("cardview")||
                    requested.name.startsWith("design")||
                    requested.name.startsWith("gridlayout")||
                    requested.name.startsWith("recyclerview")||
                    requested.name.startsWith("transition")||
                    requested.name.startsWith("appcompat")) {
                details.useVersion '25.0.0'
            }
        }
    }
}

使用动态版本

如果你想让你的工程始终采用最新依赖,那么Gradle提供了一种方式可以始终保证采用依赖的最新版本而无需每次手工检查修改版本。

使用加号+,可以让Gradle在每次执行构建时检查远程仓库是否存在该依赖的新版本,如果存在新版本则下载选用最新版本。当然也可以指定依赖某个大版本下的最新子版本,1.+表示始终采用该依赖最新的1.x版本的最新依赖。

  compile 'com.android.support:support-v4:+'//下载最新
  compile 'com.android.support:support-v4:23+'//基于23这个版本最新

一个综合案例

compile('com.github.nanchen2251:CompressHelper:1.0.5') {
   // 冲突时优先使用该版本
   force = true

   // 依据构建名称排除
   exclude module: 'CompressHelper' 
   // 依据组织名称排除
   exclude group: 'com.github.nanchen2251' 
   // 依据组织名称+构件名称排除
   exclude group: 'com.github.nanchen2251', module: 'CompressHelper' 

   // 为本依赖关闭依赖传递特性
   transitive = false
}

使用config.gradle文件统一管理项目依赖

  1. 在项目的根目录下创建config.gradle文件

    image
  2. 编辑 config.gradle,定义项目依赖

在app的build.gradle中,我们通常需要配置两个部分

  • Android 目录下的项目的版本/包名/编译版本等信息
  • dependencies 目录下的安卓Support库和我们自己引用的第三方库
    所以通常我们在config.gradle文件也将依赖分成两个部分 android/dependencies
ext {

    android = [
            compileSdkVersion      : 28,
            buildToolsVersion      : "28.0.0",
            applicationId          : "com.will.weiyue",
            minSdkVersion          : 19,
            targetSdkVersion       : 28,
            versionCode            : 1,
            versionName            : "1.0"
    ]
    //因为support库都是同一个版本,单独拎出来,方便修改
    dependVersion = [
            support: "28.0.0-alpha3"
    ]

    dependencies = [
            // android-support
            "support-v4"            : "com.android.support:support-v4:${dependVersion.support}",
            "appcompat-v7"          : "com.android.support:appcompat-v7:${dependVersion.support}",
            "design"                : "com.android.support:design:${dependVersion.support}",
            "recyclerview"          : "com.android.support:recyclerview-v7:${dependVersion.support}",
            "cardview"              : "com.android.support:cardview-v7:${dependVersion.support}",
    ]
}

  1. 在 项目的 build.gradle文件中引用config.gradle文件
// 在项目build.gradle文件的最外层添加引用
apply from: "config.gradle"

  1. 修改app的build.gradle文件中的项目引用
// 将android 和 dependencies下的引用都指向 config.gradle
// rootProject.ext.android/dependencies  config.gradle文件的路径
android {
    compileSdkVersion rootProject.ext.android.compileSdkVersion
    defaultConfig {
        applicationId rootProject.ext.android.applicationId
        minSdkVersion rootProject.ext.android.minSdkVersion
        targetSdkVersion rootProject.ext.android.targetSdkVersion
        versionCode rootProject.ext.android.versionCode
        versionName rootProject.ext.android.versionName
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation rootProject.ext.dependencies["appcompat-v7"]
    implementation rootProject.ext.dependencies["constraint-layout"]
    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'
}

更新依赖

在执行build、compile等任务时会解析项目配置的依赖并按照配置的仓库去搜寻下载这些依赖。默认情况下,Gradle会依照Gradle缓存->你配置的仓库的顺序依次搜寻这些依赖,并且一旦找到就会停止搜索。如果想要忽略本地缓存每次都进行远程检索可以通过在执行命令时添加--refresh-dependencies参数来强制刷新依赖。

gradle build --refresh-dependencies

当远程仓库上传了相同版本依赖时,有时需要为缓存指定一个时效去检查远程仓库的依赖笨版本,Gradle提供了cacheChangingModulesFor(int, java.util.concurrent.TimeUnit) ,cacheDynamicVersionsFor(int, java.util.concurrent.TimeUnit)两个方法来设置缓存的时效

configurations.all {
    //每隔24小时检查远程依赖是否存在更新
    resolutionStrategy.cacheChangingModulesFor 24, 'hours'
    //每隔10分钟..
    //resolutionStrategy.cacheChangingModulesFor 10, 'minutes'
    // 采用动态版本声明的依赖缓存10分钟
    resolutionStrategy.cacheDynamicVersionsFor 10*60, 'seconds'
}

dependencies {
    // 添加changing: true
    compile group: "group", name: "module", version: "1.1-SNAPSHOT", changing: true
    //简写方式
    //compile('group:module:1.1-SNAPSHOT') { changing = true }
}

缓存管理

缓存位置管理

Gradle在按照配置的仓库去搜寻下载依赖时,下载的依赖默认会缓存到USER_HOME/.gradle/caches目录下,当然也可以手工修改这个位置。
  具体可以参考如下三种方式:

离线模式(总是采用缓存内容)

Gradle提供了一种离线模式,可以让你构建时总是采用缓存的内容而无需去联网检查,如果你并未采用动态版本特性且可以确保项目中依赖的版本都已经缓存到了本地,这无疑是提高构建速度的一个好选择。开启离线模式只需要在执行命令时候添加--offline参数即可。当然,采用这种模式的也是有代价的,如果缓存中搜寻不到所需依赖会导致构建失败。

gradle build --offline

依赖-构件的上传与发布

借助maven-publish插件可以轻松地将jar包发布到仓库中。这个过程没啥幺蛾子直接上代码吧。了解更多配置可以查看 Maven plugin插件章节

apply plugin: 'maven-publish'
apply plugin: 'java'

// 打包源文件
task sourceJar(type: Jar) {
    from sourceSets.main.allSource
    classifier = 'sources'
}

task javadocJar(type: Jar, dependsOn: javadoc) {
    classifier = 'javadoc'
    from javadoc.destinationDir
}

publishing {
     // 目标仓库
    repositories {
        maven {
           url "xxx"
        }
    }   

    publications {          
        mavenJava(MavenPublication) {
            // 设置gav属性
            groupId 'org.pkaq'
            artifactId 'tiger'
            version '1.1'

            from components.java
            artifact sourceJar

             // 设置pom相关信息
            pom.withXml {
                Node root = asNode()              
                root.appendNode('description', 'bazinga!')
            }
        }
    }

}
//生成一个元的pom文件
model {
    tasks.generatePomFileForMavenJavaPublication {
        destination = file("$buildDir/generated-pom.xml")
    }
}

检查依赖

在引用的依赖或传递性依赖存在版本冲突时,Gradle采用的策略是优先选取最新的依赖版本解决版本冲突问题。解决此类问题我们可以通过上一章节中介绍的各种依赖管理方法进行排除、强制指定一个版本或者干脆禁用依赖传递特性解决。但如何知道哪些依赖传递了哪些子依赖,哪些传递的依赖又被Gradle进行了隐性升级呢。采用下面的命令可以查看各个范围的依赖树。

gradle  dependencies > dep.log

输出结果:

dependencies

------------------------------------------------------------
Root project
------------------------------------------------------------

archives - Configuration for archive artifacts.
No dependencies

compile - Dependencies for source set 'main'.
+--- org.springframework.boot:spring-boot-starter-web:1.4.2.RELEASE
|    +--- org.springframework.boot:spring-boot-starter:1.4.2.RELEASE
|    |    +--- org.springframework.boot:spring-boot:1.4.2.RELEASE
|    |    |    +--- org.springframework:spring-core:4.3.4.RELEASE
|    |    |    |    \--- commons-logging:commons-logging:1.2
|    |    |    \--- org.springframework:spring-context:4.3.4.RELEASE
|    |    |         +--- org.springframework:spring-aop:4.3.4.RELEASE
|    |    |         |    +--- org.springframework:spring-beans:4.3.4.RELEASE
|    |    |         |    |    \--- org.springframework:spring-core:4.3.4.RELEASE (*)
|    |    |         |    \--- org.springframework:spring-core:4.3.4.RELEASE (*)
|    |    |         +--- org.springframework:spring-beans:4.3.4.RELEASE (*)
|    |    |         +--- org.springframework:spring-core:4.3.4.RELEASE (*)
|    |    |         \--- org.springframework:spring-expression:4.3.4.RELEASE
....
....
省略的
....
....
\--- org.apache.tomcat.embed:tomcat-embed-jasper:8.5.4
     +--- org.apache.tomcat.embed:tomcat-embed-core:8.5.4 -> 8.5.6
     +--- org.apache.tomcat.embed:tomcat-embed-el:8.5.4 -> 8.5.6
     \--- org.eclipse.jdt.core.compiler:ecj:4.5.1

后面dep.log文件名可以随意,然而,你一定在想为什么有些带了(*)有的带了->有的什么都没有呢,这是什么鬼。前面已经说过,当发生版本冲突时Gradle会采用最新版本解决。仔细观察带了(*)的依赖你会发现这些依赖被不同的库重复依赖了若干次,这里(*)的意思即是表示该依赖被忽略掉了。而->则表示其它的定级依赖的传递依赖中存在更高版本的依赖,该版本将会使用->后面的版本来替代。

反向查找

如果你想知道某个依赖到底被哪个库引用过,可以采用下面的命令进行反向查找

gradle dependencyInsight  --dependency tomcat-embed-core > reverse.log 

:dependencyInsight
org.apache.tomcat.embed:tomcat-embed-core:8.5.6 (conflict resolution)
+--- org.apache.tomcat.embed:tomcat-embed-websocket:8.5.6
|    \--- org.springframework.boot:spring-boot-starter-tomcat:1.4.2.RELEASE
|         \--- org.springframework.boot:spring-boot-starter-web:1.4.2.RELEASE
|              \--- compile
\--- org.springframework.boot:spring-boot-starter-tomcat:1.4.2.RELEASE (*)

org.apache.tomcat.embed:tomcat-embed-core:8.5.4 -> 8.5.6
\--- org.apache.tomcat.embed:tomcat-embed-jasper:8.5.4
     \--- compile

(*) - dependencies omitted (listed previously)

BUILD SUCCESSFUL

Total time: 6.936 secs

上面的报告中可以看到8.5.6这个版本后面标注了(conflict resolution) 说明了该版本是用于解决冲突选用的版本。

冲突即停

Gradle默认采用自动升级版本的方式解决依赖冲突,有时这种隐式升级可能会带来一些不必要的麻烦,此时我们可以通过更改这种默认行为来让Gradle发现版本冲突时立即停止构建并抛出错误信息。
更改脚本:

configurations.all {
  resolutionStrategy {
    failOnVersionConflict()
  }
}

执行gradle build的输出结果:

:compileJava FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Could not resolve all dependencies for configuration ':compileClasspath'.
> A conflict was found between the following modules:
   - org.apache.tomcat.embed:tomcat-embed-core:8.5.4
   - org.apache.tomcat.embed:tomcat-embed-core:8.5.6

可以看到在执行gradle build时由于tomcat-embed-core存在版本冲突导致了构建失败,并提示了冲突的两个版本。

依赖报告

Gradle官方提供了一个名叫project-report插件可以让依赖查看更加简单方便,要用此插件只需要在脚本中添加apply plugin: 'project-report'即可。该插件提供的任务可以参考下面的表格,所有输出结果将被放在build/report下。

任务名称 描述
dependencyReport 将项目依赖情况输出到txt文件中 功能同gradle dependencies > build/dependenciestxt
htmlDependencyReport 生成HTML版本的依赖情况
propertyReport 生成项目属性报告
taskReport 生成项目任务报告
projectReport 生成项目报告,包括前四个

使用插件检查更新

使用三方插件进行检查,可以使依赖固定在一个相对新的版本,这里需要注意的是,plugins需要放置在脚本的顶部,更多关于plugins的内容可以查看官方文档

plugins {
   id "name.remal.check-dependency-updates" version "1.0.6" 
}

应用此插件后,可以执行gradle checkDependencyUpdatesgradle cDU 检查依赖最新版本 : }

> Task :web:checkDependencyUpdates
New dependency version: com.alibaba:druid: 1.0.29 -> 1.1.7

多项目构建

前面我们介绍的例子,都是单独执行某一个 build.gradle 文件。但是我们在 Android 应用开发中,一个 Project 可以包含若干个 module ,这种就叫做多项目构建。在 Android Studio 项目中,根目录都有一个名叫 settings.gradle 的文件,然后每个 module 的根目录中又有一个 build.gradle 文件,Gradle 就是通过 settings.gradle 来进行多项目构建的。

通过 settings.gradle 引入子项目

1.先创建如下几个目录及文件:

image

如图所示,在项目根目录创建一个 settings.gradle,在根目录、app以及library目录下也都创建一个 build.gradle 文件。

2.在 settings.gradle 里引入子项目

include ":app", ":library"

3.在 build.gradle 里增加测试代码

//在根目录 build.gradle 里增加
println "-----root file config-----"

//在 app/build.gradle 里增加
println "-----app config-----"

//在 library/build.gradle 里增加
println "-----library config-----"

4.在项目根目录执行命令 gradle -q,结果如下:

-----root file config-----
-----app config-----
-----library config-----

这是一个多项目构建的简单例子,可以看到结构与我们的 Android 项目是类似的。Gradle 在运行时会读取并解析 settings.gradle 文件,生成一个 Settings对象,然后从中读取并解析子项目的 build.gradle 文件,然后为每个 build.gradle 文件生成一个 Project 对象,进而组装一个多项目构建出来。

Settings 里最核心的API就是 include 方法,通过该方法引入需要构建的子项目。

include(projectPaths: String...)

这里我们为每个 build.gradle 文件生成了一个 Project 对象,跟总共3个 Project,根目录的 Project 我们称之为 root project,子目录的 Project 我们称之为 child project。

项目配置

在根项目里可以对子项目进行配置:

//通过path定位并获取该 Project 对象
project(path: String): Project
//通过path定位一个Project,并进行配置
project(path: String, config: Closure): Project

//针对所有项目进行配置
allprojects(config: Closure)
//针对所有子项目进行配置
subprojects(config: Closure)

我们修改根目录 build.gradle 文件如下:

println "-----root file config-----"

//配置 app 项目
project(":app") {
    ext {
        appParam = "test app"
    }
}

//配置所有的项目
allprojects {
    ext {
        allParam = "test all project"
    }   
}

//配置子项目
subprojects {
    ext {
        subParam = "test sub project"
    }
}

println "allParam = ${allParam}"

修改 app/build.gradle 文件如下:

println "-----app config-----"
println "appParam = ${appParam}"
println "allParam = ${allParam}"
println "subParam = ${subParam}"

修改 library/build.gradle 文件如下:

println "-----library config-----"
println "allParam = ${allParam}"
println "subParam = ${subParam}"

运行结果如下:

-----root file config-----
allParam = test all project
-----app config-----
appParam = test app
allParam = test all project
subParam = test sub project
-----library config-----
allParam = test all project
subParam = test sub project

文件操作

通过mkdir创建目录
File mkDir = mkdir("${buildDir}/test");
File mkDir2 = mkdir("${buildDir}/test2")
println "检测目录是否创建成功:${mkDir.exists()}, ${mkDir2.exists()}"

通过file、files 定位文件
//定位单个文件,参数可以是相对路径、绝对路径
File testDir = file("${buildDir}/test")
println "文件定位是否成功:${testDir.exists()}"

//文件集合,Gradle里用 FileCollection 来表示
FileCollection fileCollection = files("${buildDir}/test", "${buildDir}/test2")
println "-------对文件集合进行迭代--------"
fileCollection.each {File f ->
    println f.name
}
println "-------文件迭代结束-------"
//获取文件列表
Set<File> set = fileCollection.getFiles()
println "文件集合里共有${set.size()}个文件"

通过fileTree创建文件树

Gradle里用 ConfigurableFileTree 来表示文件树,文件树会返回某个目录及其子目录下所有的文件,不包含目录。

//先在build目录下创建3个txt文件
file("${buildDir}/t1.txt").createNewFile()
file("${buildDir}/test/t2.txt").createNewFile()
file("${buildDir}/t1.java").createNewFile()

//1.通过一个基准目录创建文件树,参数可以是相对目录,也可以是绝对目录,与file()方法一样
println "通过基准目录来创建文件树"
ConfigurableFileTree fileTree1 = fileTree("build")
//添加包含规则
fileTree1.include "*.txt", "*/*.txt"
//添加排除规则
fileTree1.exclude "*.java"
fileTree1.each { f ->
    println f    
}

//2.通过闭包来创建文件树
println "通过闭包来创建文件树"
ConfigurableFileTree fileTree2 = fileTree("build") {
    include "*/*.txt", "*.java"
    exclude "*.txt"
}
fileTree2.each { f ->
    println f    
}

//3.通过map配置来创建文件树,可配置的选项有:dir: ''、include: '[]、exclude: []、includes: []、excludes: []
println "通过Map来创建文件树"
def fileTree3 = fileTree(dir: "build", includes: ["*/*.txt", "*.java"])
fileTree3 = fileTree(dir: "build", exclude: "*.java")
fileTree3.each { f ->
    println f    
}

复制文件

复制文件需要使用复制任务(Copy)来进行,它需要指定要复制的源文件和一个目标目录,复制的规则都是定义在 CopySpec 接口里的,更详细的说明可参见 API 文档。

task testCopyFile(type: Copy) {
    //复制build目录下的所有文件
    from "build"
    //复制单独的某个文件
    from "test.java"
    //复制某个文件树下的所有文件
    from fileTree("build")

    include "*.txt"
    include "*.java"
    exclude "t1.txt"
    //指定目标目录
    into "outputs"

    //对复制的文件重命名:通过闭包来映射
    rename { fileName ->
        //增加 rename_ 前缀
        return fileName.endsWith(".java") ? "rename_" + fileName : fileName
    }

    //通过正则来映射文件名:abctest.java 会映射成 abchjy.java
    rename '(.*)test(.*)', '$1hjy$2'
}

删除文件
//删除 build 目录下所有文件
delete("${buildDir}")

下面通过一些例子来解释如何Hook Gradle的构建过程。

在根项目的build.gradle中添加如下代码:

gradle.beforeProject { project ->
  println 'apply plugin java for ' + project
  project.apply plugin: 'java'
}

这段代码的作用是为所有子项目应用Java插件,因为代码是在根项目的配置阶段执行的,所以并不会应用到根项目中。
这里说明一下Gradle的beforeProject方法和Project的beforeEvaluate的执行时机是一样的,只是beforeProject应用于所有项目,而beforeEvaluate只应用于调用的Project,上面的代码等价于:

allprojects {
  beforeEvaluate { project ->
    println 'apply plugin java for ' + project
    project.apply plugin: 'java'
  }
}

after***也是同理的,但afterProject还有一点不一样,无论Project的配置过程是否出错,afterProject都会收到回调。

gradle.taskGraph.beforeTask { task ->
  task << {
    println '动态添加的Action'
  }
}

task Test {
  doLast {
    println '原始Action'
  }
}

在任务Test执行前,动态添加了一个doLast动作。

long beginOfSetting = System.currentTimeMillis()

gradle.projectsLoaded {
  println '初始化阶段,耗时:' + (System.currentTimeMillis() - beginOfSetting) + 'ms'
}

def beginOfConfig
def configHasBegin = false
def beginOfProjectConfig = new HashMap()
gradle.beforeProject { project ->
  if (!configHasBegin) {
    configHasBegin = true
    beginOfConfig = System.currentTimeMillis()
  }
  beginOfProjectConfig.put(project, System.currentTimeMillis())
}
gradle.afterProject { project ->
  def begin = beginOfProjectConfig.get(project)
  println '配置阶段,' + project + '耗时:' + (System.currentTimeMillis() - begin) + 'ms'
}
def beginOfProjectExcute
gradle.taskGraph.whenReady {
  println '配置阶段,总共耗时:' + (System.currentTimeMillis() - beginOfConfig) + 'ms'
  beginOfProjectExcute = System.currentTimeMillis()
}
gradle.taskGraph.beforeTask { task ->
  task.doFirst {
    task.ext.beginOfTask = System.currentTimeMillis()
  }
  task.doLast {
    println '执行阶段,' + task + '耗时:' + (System.currentTimeMillis() - task.beginOfTask) + 'ms'
  }
}
gradle.buildFinished {
  println '执行阶段,耗时:' + (System.currentTimeMillis() - beginOfProjectExcute) + 'ms'
}

将上述代码段添加到settings.gradle脚本文件的开头,再执行任意构建任务,你就可以看到各阶段、各任务的耗时情况。

有时我们需要在一个已有的构建系统中插入我们自己的构建任务,比如在执行Java构建后我们想要删除构建过程中产生的临时文件,那么我们就可以自定义一个名叫cleanTemp的任务,让其依赖于build任务,然后调用cleanTemp任务即可。
但是这种方式适用范围太小,比如在使用IDE执行构建时,IDE默认就是调用build任务,我们没法修改IDE的行为,所以我们需要将自定义的任务插入到原有的任务关系中。

  1. 寻找插入点
    如果你对一个构建的任务依赖关系不熟悉的话,可以使用一个插件来查看,在根项目的build.gradle中添加如下代码:
buildscript {
  repositories {
    maven {
      url "https://plugins.gradle.org/m2/"
    }
  }
  dependencies {
    classpath "gradle.plugin.com.dorongold.plugins:task-tree:1.2.2"
  }
}
apply plugin: "com.dorongold.task-tree"

然后执行gradle <任务名> taskTree --no-repeat,即可看到指定Task的依赖关系,比如在Java构建中查看build任务的依赖关系:

:build
+--- :assemble
|    \--- :jar
|         \--- :classes
|              +--- :compileJava
|              \--- :processResources
\--- :check
     \--- :test
          +--- :classes *
          \--- :testClasses
               +--- :compileTestJava
               |    \--- :classes *
               \--- :processTestResources

我们看到build主要执行了assemble包装任务和check测试任务,那么我们可以将我们自定义的cleanTemp插入到build和assemble之间。

  1. 动态插入自定义任务
    我们先定义一个自定的任务cleanTemp,让其依赖于assemble。
task cleanTemp(dependsOn: assemble) {
  doLast {
    println '清除所有临时文件'
  }
}

接着,我们将cleanTemp添加到build的依赖项中。

afterEvaluate {
  build.dependsOn cleanTemp
}

注意,dependsOn方法只是添加一个依赖项,并不清除之前的依赖项,所以现在的依赖关系如下:

:build
+--- :assemble
|    \--- :jar
|         \--- :classes
|              +--- :compileJava
|              \--- :processResources
+--- :check
|    \--- :test
|         +--- :classes
|         |    +--- :compileJava
|         |    \--- :processResources
|         \--- :testClasses
|              +--- :compileTestJava
|              |    \--- :classes
|              |         +--- :compileJava
|              |         \--- :processResources
|              \--- :processTestResources
\--- :cleanTemp
     \--- :assemble
          \--- :jar
               \--- :classes
                    +--- :compileJava
                    \--- :processResources

可以看到,cleanTemp依赖于assemble,同时build任务多了一个依赖,而build和assemble原有的依赖关系并没有改变,执行gradle build后任务调用结果如下:

:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:jar UP-TO-DATE
:assemble UP-TO-DATE
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test UP-TO-DATE
:check UP-TO-DATE
:cleanTemp
清除所有临时文件
:build

BUILD SUCCESSFUL

使用Gradle Wrapper来统一构建版本

另外需要说明的一点是Gradle、Gradle Wrapper与Android Plugin for Gradle不一定要和Android Studio一起使用,你可以完全脱离Android Studio,使用三者独立进行Android项目的构建。下面是三者官方的指导文档(从地址可以看出Gradle Wrapper是Gradle项目的一部分):

image

其中GRADLE_USER_HOME一般指~/.gradle,从图示项目中可以知道我要使用gradle-4.1版本,从https://services.gradle.org/distributions/gradle-4.1-all.zip下载,下载到本地的~/.gradle/wrapper/dists目录。那是不是各个项目的Gradle都要通过Gradle Wrapper下载,能不能所有的项目共用一个Gradle?这样理论上是可以的,但是由于Gradle本身不一定保持完全的兼容性,所以新老项目共用一个Gradle有时可能会遇到意想不到的问题。指定对应版本的Gradle,而不通过Gradle Wrapper下载的设置方式是勾选如下图中的Use local gradle distribution,同时指定Gradle home:

image

Gradle对应版本下载完成之后,Gradle Wrapper的使命基本完成了,Gradle会读取build.gradle文件,该文件中指定了该项目需要的Android Plugin for Gradle版本是什么,从哪里下载该版本的Android Plugin for Gradle。如下图所示:

image

从图示项目中可以知道我们要使用3.0.1版本,从google和jcenter处下载,那么下载到我们本地的哪里呢?它会下载到~\.gradle\caches\modules-2\files-2.1\com.android.tools.build中。有时候大家网络装填不好,选择下图中的Offline work时可能出现"No cached version of com.android.tools.build:gradle:xxx available for offline mode"问题,此时你只要将对应版本的Android Plugin for Gradle下载到本地的C:\Program Files\Android\Android Studio\gradle\m2repository\com\android\tools\build中即可。

image

defaultConfig{ } 默认配置,是ProductFlavor类型。它共享给其他ProductFlavor使用

    sourceSets{ } 源文件目录设置,是AndroidSourceSet类型。
    buildTypes{ } BuildType类型
    signingConfigs{ } 签名配置,SigningConfig类型
    productFlavors{ } 产品风格配置,ProductFlavor类型
    testOptions{ } 测试配置,TestOptions类型
    aaptOptions{ } aapt配置,AaptOptions类型
    lintOptions{ } lint配置,LintOptions类型
    dexOptions{ } dex配置,DexOptions类型
    compileOptions{ } 编译配置,CompileOptions类型
    packagingOptions{ } PackagingOptions类型
    jacoco{ } JacocoExtension类型。 用于设定 jacoco版本
    splits{ } Splits类型

A:占位符

在使用友盟进行渠道统计常用的做法是 使用占位符,比如我们可以在AndroidManifest.xml文件使用meta-data进行信息的配置。

image

然后使用的话,如下图:

image

B:签名

签名,是android标签内大家经常使用到的,由于Android 7.0加入了新的签名机制(也就是V2签名)针对这个问题的解决方式可以参考这篇文章 Android-V1、V2签名,因此我们可以直接在debug和release标签中加入v1SigningEnabled true 、v2SigningEnabled true 规避使用风险

下面是加入新签名机制以及原来大家比较熟悉的写法:

image

这里还给大家提供一种关于签名信息的写法(拓展性比第一种较强),首先,我们在app文件的根目录下定义一个文件 signing.properties ,然后写上具体的属性值 (具体的说明如代码截图)

image

由于Gradle是一门脚本,既然是脚本那么它肯定内置了一些函数(注意:Gradle的函数是在app gradle文件内置的标签外 进行编写)给我们操作调用。因为将签名信息写到了这里的配置文件,所以可以通过Gradle去读取信息 然后进行赋值,代码如下:

image

有了读取签名文件的函数,我们就可以进行Alias、password的赋值。

image

C:多渠道包配置

Android Studio给我们提供的多渠道打包方案是使用productFlavors标签配置渠道信息,但是新版本下直接使用这个productFlavors标签会报错,工具要求我们在defaultConfig标签下新增flavorDimensions,如果不使用flavorDimensions关键字,编译会报错:

image

D:自定义apk输出路径

传统的打包操作流程执行编译以及签名后生成的apk默认是 项目 \build\outputs\apk 这个路径下面,那现在我想指定apk输出的文件位置(比如我现在想让这个apk输出到 c盘下面的out_apk文件夹下)该如何操作?

image

其中这里的红色矩形是对上面签名第二种方法的补充说明,这里的buildTypes标签需要写在signingConfigs标签后面,否则很容易编译错误,这个是笔者遇到的问题;蓝色矩形的代码块主要是首先判断是debug还是release版本,如果是release版本就将apk输入到这个指定盘符。

好了,说了这么多gradle文件的配置,下面就开始我们的打包工作。

首先是我们的传统打签名包:

点击Android Studio 顶部Tab , Build —— Generate Sign Apk 然后出现下面的界面:

image

配置好基本信息以后,点击next,勾选V1、V2等一些配置即可完成签名打包。

第二种:在Android Studio 底部Tab的 Terminal窗口 输入 gradle a 命令本质就是dos操作),来帮助我们打包

image

以上两种打包方式都是传统的打包方式,那么有没有更简单的操作?这种简单的操作类似我 在PC上面点击一个快捷方式就给我生成apk包?答案在开头已经说了是有的。

A:首先,我们在项目根目录下创建一个指定的文件夹(用于编译跑脚本用),这个文件一般定义为 .config文件(记住:前面有个 . )

B:然后,在这个文件夹内创建 build.bat文件。可能你会问什么是bat文件?bat文件是dos下的批处理文件。批处理文件是无格式的文本文件,它包含一条或多条命令。当我们双击bat文件的时候,系统就会调用cmd.exe按照该文件中各个命令出现的顺序来逐个运行它们。所以,我们只需要在这个bat文件里面写下相应的命令进行操作即可

C:既然现在我们需要在bat文件里面写命令,那么gradle里面有那些可以执行的打包命令?下面是收集到的一些资料,命令如下:

1:编译所有productFlavor及对应所有buildType的apk:

gradle assemble //仅仅执行项目打包所必须的任务集

gradle build //执行项目打包所必须的任务集,以及执行自动化测试,所以会较慢

如果当前Project包含多个Module,在Project根目录执行gradle assemble会编译所有的Module

2:编译指定productFlavor及buildType的apk

gradle assemble[productFlavor][buildType] //如果缺失某参数,则会把该参数的所有配置都进行编译,即如果运行gradle assembleflavor,则会编译出flavor所有buildType的apk

例如:

gradle assemble

gradle assembleflavorRelease

gradle assembleflavorDebug

注意:gradle支持命令缩写,上面两个命令也可以写成如下格式

$gradle a

$gradle ass

$gradle aR

$gradle assflavorR

$gradle aD

                $gradle assflavorD

D:既然dos命令确定了,我们就可以写bat文件进行测试了:

image

E:接下来我们就可以将这个build.bat文件,设置为桌面快捷方式,然后双击,双击以后会弹出cmd窗口进行打包然后去指定的文件夹拿到apk即可。

以上步骤的前提是你需要配置好Gradle运行环境、熟悉Gradle的基本命令以及必要的耐心和不怕失败的勇气。

  1. 根目录下的 build.gradle
      1) repositories 闭包,声明了 jcenter() 的配置;

2) dependencies 闭包,声明了一个 Gradle 插件。

buildscript {
    
    repositories {  //repositories闭包
        google()
        jcenter() //代码托管库:设置之后可以在项目中轻松引用jcenter上的开源项目
    }
    dependencies {  //dependencies闭包
        classpath 'com.android.tools.build:gradle:3.0.0' ////声明gradle插件,插件版本号为3.0.0
        //gradle是一个强大的项目构建工具,不仅可以构建Android,还可以构建java,C++等
        //此处引用android的插件
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        google()
        jcenter() //代码托管库:设置之后可以在项目中轻松引用jcenter上的开源项目
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}
  1. app 目录下的build.gradle
      1) apply plugin,声明是 Android 应用程序还是库模块;

2) android 闭包,配置项目构建的各种属性,compileSDKVersion 用于指定项目的变异 SDK 版本, buildToolsVersion 用户指定项目构建工具的版本。

defaultConfig 闭包:默认配置、应用程序包名、最小sdk版本、目标sdk版本、版本号、版本名称;

buildTypes 闭包:指定生成安装文件的配置,是否对代码进行混淆;

signingConfigs 闭包:签名信息配置;

sourceSets 闭包:源文件路径配置;

lintOptions 闭包:lint 配置;

3) dependencies 闭包,指定当前项目的所有以来关系,本地以来,库依赖以及远程依赖;

4) repositories 闭包,仓库配置。

// 声明是Android程序,
// com.android.application 表示这是一个应用程序模块,可直接运行
// com.android.library 标识这是一个库模块,是依附别的应用程序运行
apply plugin: 'com.android.application'

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

    // 编译sdk的版本,也就是API Level,例如API-19、API-20、API-21等等。
    compileSdkVersion 27
    // build tools的版本,其中包括了打包工具aapt、dx等等。
    // 这个工具的目录位于你的sdk目录/build-tools/下
    buildToolsVersion '27.0.3'

    //关闭Android Studio的PNG合法性检查
    aaptOptions.cruncherEnabled = false
    aaptOptions.useNewCruncher = false

    defaultConfig {  // 默认配置
        applicationId "com.demo.test" // 应用程序的包名
        minSdkVersion 22  / 最小sdk版本,如果设备小于这个版本或者大于maxSdkVersion将无法安装这个应用
        targetSdkVersion 27 // 目标sdk版本,充分测试过的版本(建议版本)
        versionCode 1  // 版本号,第一版是1,之后每更新一次加1
        versionName "1.0" // 版本名,显示给用户看到的版本号

        archivesBaseName = "demo-$versionName" // 指定打包成Jar文件时候的文件名称
        ndk {
            moduleName "testwifisafe"                   // 设置库(so)文件名称
            ldLibs "log", "z", "m", "jnigraphics", "android"
            // 引入库,比如要用到的__android_log_print
            abiFilters "armeabi", "x86", "armeabi-v7a"      // "x86"  显示指定支持的ABIs
            cFlags "-std=c++11 -fexceptions"                // C++11
            stl "gnustl_static"
        }

        // 当方法数超过65535(方法的索引使用的是一个short值,
        // 而short最大值是65535)的时候允许打包成多个dex文件,动态加载dex。这里面坑很深啊
        multiDexEnabled true

        // Instrumentation单元测试
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }

    // 默认的一些文件路径的配置
    sourceSets {
        main {
            manifest.srcFile 'AndroidManifest.xml' // 指定清单文件
            res.srcDirs = ['res'] // 指定res资源目录
            assets.srcDirs = ['assets']    // asset资源目录
            jni.srcDirs 'src/main/jni'     // jni代码目录
            jniLibs.srcDir 'src/main/jniLibs' // jni库目录
            java.srcDirs = ['src'] // 指定java源代码目录
            resources.srcDirs = ['src'] // 指定resource目录
            aidl.srcDirs = ['src'] // 指定aidl目录
            renderscript.srcDirs = ['src'] // 指定source目录
        }
        debug.setRoot('build-types/debug') // 指定debug模式的路径
        release.setRoot('build-types/release') // 指定release模式的路径
    }

    // multiDex的一些相关配置,这样配置可以让你的编译速度更快
    dexOptions {
        // 让它不要对Lib做preDexing
        preDexLibraries = false
        // 开启incremental dexing,优化编译效率,这个功能android studio默认是关闭的。
        incremental true
        javaMaxHeapSize "4g" // 增加java堆内存大小
    }

    signingConfigs {// 签名配置
        release {// 发布版签名配置
            storeFile file("fk.keystore") // 密钥文件路径
            storePassword "123456" // 密钥文件密码
            keyAlias "fk" // key别名
            keyPassword "123456" // key密码
        }
        debug {// debug版签名配置
            storeFile file("fk.keystore")
            storePassword "123456"
            keyAlias "fk"
            keyPassword "123456"
        }
    }

    // 指定生成安装文件的配置,常有两个子包:release,debug,注:直接运行的都是debug安装文件
    buildTypes {
        // release版本的配置,即生成正式版安装文件的配置
        release {
            zipAlignEnabled true  // 是否支持zip
            shrinkResources true  // 移除无用的resource文件
            minifyEnabled false // 是否对代码进行混淆,true表示混淆
            // 指定混淆时使用的规则文件;
            // proguard-android.txt指所有项目通用的混淆规则,proguard-rules.pro当前项目特有的混淆规则
            // release的Proguard默认为Module下的proguard-rules.pro文件
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            debuggable false  //是否支持调试
            //ndk的一些配置
            ndk {
                // cFlags "-std=c++11 -fexceptions -O3 -D__RELEASE__" // C++11
                // platformVersion = "19"
                moduleName "hebbewifisafe" // 设置库(so)文件名称
                ldLibs "log", "z", "m", "jnigraphics", "android"
                // 引入库,比如要用到的__android_log_print
                abiFilters "armeabi", "x86", "armeabi-v7a"// "x86"
                cFlags "-std=c++11 -fexceptions" // C++11
                stl "gnustl_static"
            }
            // 采用动态替换字符串的方式生成不同的 release.apk (3.0之后版本的修改方式)
            applicationVariants.all { variant ->
                variant.outputs.all { output ->
                    if (!variant.buildType.isDebuggable()) {
                        // 获取签名的名字 variant.signingConfig.name
                        // 要被替换的源字符串
                        def sourceFile = "app-release";
                        // 替换的字符串
              def replaceFile = "Demo-V${variant.versionName}.${releaseTime()}"
              outputFileName = output.outputFile.name.replace(sourceFile, replaceFile)
                    }
                }
            }
            jniDebuggable false  // 关闭jni调试
        }
        debug { // debug版本的配置
            minifyEnabled false
            zipAlignEnabled true
            shrinkResources true // 移除无用的resource文件
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            debuggable true
//          jniDebuggable true
            ndk {
                cFlags "-std=c++11 -fexceptions -g -D __DEBUG__" // C++11
            }
            jniDebuggable true
        }
    }

    packagingOptions {
        exclude 'META-INF/ASL2.0'
        exclude 'META-INF/LICENSE'
        exclude 'META-INF/NOTICE'
        exclude 'META-INF/MANIFEST.MF'
    }
    
    compileOptions {
        // 在这里你可以进行 Java 的版本配置,
        // 以便使用对应版本的一些新特性
    }
    productFlavors {
        // 在这里你可以设置你的产品发布的一些东西,
        // 比如你现在一共软件需要发布到不同渠道,
        // 且不同渠道中的包名不同,那么可以在此进行配置;
        // 甚至可以设置不同的 AndroidManifest.xml 文件。
        hebbe {
        }
        googlePlay {
        }
        solo {
        }
    }
    productFlavors.all {
        flavor -> flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name]
    }
    // 所谓ProductFlavors其实就是可定义的产品特性,
    // 配合 manifest merger 使用的时候就可以达成在一次编译
    // 过程中产生多个具有自己特性配置的版本。
    // 上面这个配置的作用就是,为每个渠道包产生不同的 UMENG_CHANNEL_VALUE 的值。
}

// 指定当前项目的所有依赖关系:本地依赖、库依赖、远程依赖
// 本地依赖:可以对本地 Jar 包或目录添加依赖关系
// 库依赖:可以对项目中的库模块添加依赖关系
// 远程依赖:可以对 jcenter 库上的开源项目添加依赖
// 标准的远程依赖格式是 域名:组织名:版本号
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar']) // 本地依赖
    // 远程依赖,com.android.support是域名部分,appcompat-v7是组名称,26.1.0是版本号
    implementation 'com.android.support:appcompat-v7:26.1.0'
    implementation 'com.android.support.constraint:constraint-layout:1.0.2'
    implementation project(':hello') // 库依赖
    testImplementation 'junit:junit:4.12' // 声明测试用列库
    androidTestImplementation 'com.android.support.test:runner:1.0.1'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
}

// 声明是要使用谷歌服务框架
apply plugin: 'com.google.gms.google-services'

// 第三方依赖库的本地缓存路径
task showMeCache << {
    configurations.compile.each { println it }
}
// 使用maven仓库。android有两个标准的library文件服务器,一个jcenter一个maven。两者毫无关系。
// jcenter有的maven可能没有,反之亦然。
// 如果要使用jcenter的话就把mavenCentral()替换成jcenter()
repositories {
    mavenCentral()
}
205 // 获取日期方法
def releaseTime() {
   return new Date().format("MMdd.HHmm")
}

gradle-wrapper.properties

distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip

//主要是要上面的地址改成自己本地有的版本

Mac系统默认下载到:/Users/(用户名)/.gradle/caches/modules-2/files-2.1
Windows系统默认下载到:C:\Users(用户名).gradle\caches\modules-2\files-2.1

在开发sdk生成jar在eclipse里相对比较容易操作,只要导出class时指定哪里导出就可以,
但在用AndroidStudio开发时要导出jar就没那么容易,需要写gradle脚本,但gradle脚本
的入门成本还是比较高,网上也有打包jar的脚本参考,但大多是通过生成classes.jar重命名
来生成,这样生成的jar是包含工程里所有类的,有时我们需要指定一些包和类来生成jar。
下面是参考脚本:

  task clearJar(type: Delete) { 
        delete 'libs/sdk.jar' 
  } 

  task makeJar(type:org.gradle.api.tasks.bundling.Jar) {
        //指定生成的jar名
        baseName 'sdk'
        //从哪里打包class文件
        from('build/intermediates/classes/debug/org/cmdmac/cloud/pluginsdk/')
        //打包到jar后的目录结构 
        into('org/cmdmac/cloud/pluginsdk/')
        //去掉不需要打包的目录和文件 
        exclude('test/', 'BuildConfig.class', 'R.class')
        //去掉R$开头的文件 
        exclude{ it.name.startsWith('R$');} 
    } 

    makeJar.dependsOn(clearJar, build)

在build.gradle写上后,只要在命令行执行gradle makeJar就可以在build/libs目录下找到这个jar
如果要实现只打某个包下面的某些子包或者文件可参考如下示例:

    task makeSdkJar(type:org.gradle.api.tasks.bundling.Jar) {
        baseName 'pluginsdk'
        //只打包org.cmdmac下的org.cmdmac.pluginsdk.impl和org.cmdmac.gamecenter,其他子包不会被打包进去
        from('build/intermediates/classes/debug/org/cmdmac/') {
            include 'pluginsdk/impl'
            include 'gamecenter'
        }
        into('org/cmdmac/')
    //    exclude('R.class')
    //    exclude{ it.name.startsWith('R$');}
    }

要熟练构建变体,先需要了解Androidstudio是如何来自动化执行和管理构建流程。

构建流程

image

如图所示,典型 Android 应用模块的构建流程通常依循下列步骤:

  1. 编译器将您的源代码转换成 DEX(Dalvik Executable) 文件(其中包括运行在 Android 设备上的字节码),将所有其他内容转换成已编译资源。
  2. APK 打包器将 DEX 文件和已编译资源合并成单个 APK。不过,必须先签署 APK,才能将应用安装并部署到 Android 设备上。
  3. APK 打包器使用调试或发布密钥库签署您的 APK:
    1. 如果您构建的是调试版本的应用(即专用于测试和分析的应用),打包器会使用调试密钥库签署您的应用。Android Studio 自动使用调试密钥库配置新项目。
    2. 如果您构建的是打算向外发布的发布版本应用,打包器会使用发布密钥库签署您的应用。要创建发布密钥库,请阅读在 Android Studio 中签署您的应用。
  4. 在生成最终 APK 之前,打包器会使用 zipalign 工具对应用进行优化,减少其在设备上运行时的内存占用。

构建流程结束时,您将获得可用来进行部署、测试的调试 APK,或者可用来发布给外部用户的发布 APK。

自定义构建配置

通过Androidstudio我们可以自动构建哪些内容呢?

下面详细介绍如何去构建。

配置构建类型(buildTypes)

可以在模块级 build.gradle 文件的 android {} 代码块内部创建和配置构建类型,默认的类型为release和debug。

我们可以在realease项中很方便的对APK进行签名,也可以对applicationId根据类型添加后缀,例如:

//读取签名文件信息
def keystorePropertiesFile = rootProject.file("keystore.properties");
def keystoreProperties = new Properties()
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))

android {
    compileSdkVersion rootProject.ext.compileSdkVersion
    buildToolsVersion rootProject.ext.buildToolsVersion
    defaultConfig {
        applicationId "com.abupdate.fota_demo_iot"
        minSdkVersion rootProject.ext.minSdkVersion
        targetSdkVersion rootProject.ext.targetSdkVersion
        versionCode 1
        versionName verName
    }

    //签名配置信息
    signingConfigs {
        config {
            storeFile file(keystoreProperties['ReleaseStoreFile'])
            storePassword keystoreProperties['ReleaseStorePassword']
            keyAlias keystoreProperties['ReleaseKeyAlias']
            keyPassword keystoreProperties['ReleaseKeyPassword']
        }
    }

    buildTypes {
        release {
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            //签名
            signingConfig signingConfigs.config
        }
        debug {
            minifyEnabled false
            shrinkResources false
            //对applicationId根据类型添加后缀
            applicationIdSuffix ".debug"
        }
    }
}

修改后记得点一下工具栏中的同步。

配置产品风味(productFlavors)

创建productFlavor与创建构建类型类似:只需将它们添加到 productFlavors {} 代码块并配置您想要的设置。productFlavor支持与 defaultConfig 相同的属性,这是因为 defaultConfig 实际上属于 ProductFlavor 类。这意味着,您可以在 defaultConfig {} 代码块中提供所有productFlavor的基本配置,每种productFlavor均可更改任何这些默认值,例如 applicationId。

比如,现在我们有两种产品风味,一种为车机产品,一种为手机产品,那么可以这么配置:

productFlavors{
    Car{
        //根据不同的产品提添加不同的字段,以便在代码中进行判断
        buildConfigField("String", "APP_LAUNCH_ACTIVITY", APP_LAUNCH_ACTIVITY_CAR)
        buildConfigField("String", "APP_TYPE", APP_TYPE_CAR)
        applicationIdSuffix ".car"
        versionNameSuffix "-car
    }
    Phone{
        buildConfigField("String", "APP_LAUNCH_ACTIVITY", APP_LAUNCH_ACTIVITY_IOT)
        buildConfigField("String", "APP_TYPE", APP_TYPE_IOT)
        applicationIdSuffix ".phone"
        versionNameSuffix "-phone
    }
}

组合多个产品风味(flavorDimensions)

某些情况下,您可能希望组合多个产品风味中的配置。

你应该经常有这种需求,要构建有启动图标和没有启动图标的两个APP,那么这个productFlavor和上文提到的productFlavors(car和phone)是两个维度的东西,那么我们创建两个维度的productFlavors,示例如下:

//两个维度:设备类型和图标类型
flavorDimensions "device_type", "icon_type"
productFlavors{
    Car{
        //指定该productFlavor是哪个维度
        dimension  'device_type'
        //根据不同的产品提添加不同的字段,以便在代码中进行判断
        buildConfigField("String", "APP_LAUNCH_ACTIVITY", APP_LAUNCH_ACTIVITY_CAR)
        buildConfigField("String", "APP_TYPE", APP_TYPE_CAR)
        applicationIdSuffix ".car"
        versionNameSuffix "-car
    }
    Phone{
        dimension  'device_type'
        buildConfigField("String", "APP_LAUNCH_ACTIVITY", APP_LAUNCH_ACTIVITY_IOT)
        buildConfigField("String", "APP_TYPE", APP_TYPE_Phone)
        applicationIdSuffix ".phone"
        versionNameSuffix "-phone
    }

    with_icon{
        dimension  'icon_type'
    }

    no_icon{
        dimension  'icon_type'
    }
}

选择变体

完成以上步骤后,点击Androidstudio左侧的Build Variant,可以看到如图所示的内容:

image

根据变体打包成不同的APP

根据上文选择的变体,点击工具栏中的Build APK可以生成对应的APP。

android.applicationVariants.all { variant ->
    variant.outputs.all {
        outputFileName = "${variant.name}-${variant.versionName}.apk"

    }
}

buildAPK后可以生成如图的APP:

image

清单文件合并(AndroidManifest)

各个变体肯定需要差异化,不同的APPName、icon、或者是业务逻辑,要做到这些,需要先学习如何合并AndroidManifest。

首先建立对应的源集,默认的方式如图:

image

然后建立对应的AndroidManifest文件

image

合并优先级

可能你没有合并过AndroidManifest,但是肯定有这样的体验,以前我们使用jar包时需要在AndroidManifest中添加jar包中的组件和权限,但是在使用model或者远程依赖或者aar时就不需要添加,其实编译时就有合并的操作。

合并 3 个清单 文件(从优先级最低的文件(左)合并至优先级最高的文件(右))的流程:

image

那我们的变体是如何去合并的呢?

优先级如下(由高到低)(其实是定制程度高低决定):

  1. 构建变体清单(如 src/demoDebug/)
  2. 构建类型清单(如 src/debug/)
  3. 产品定制清单(如 src/demo/)

合并冲突解决

内容较多,可以参考官方指南:https://developer.android.com/studio/build/manifest-merge.html#_3

官方文档介绍比较清楚了,这里不再重述。

构建差异

noIcon变种去掉启动图标

在对应noIcon的变种集下,AndroidManifest代码如下:

<activity
        android:name=".view.activity.CarActivity"
        android:configChanges="keyboard|screenSize|orientation"
        android:launchMode="singleTask"
        android:theme="@style/AppTheme.NoActionBar"
        tools:node="merge">
        <intent-filter>
            <action android:name="android.intent.action.MAIN"/>

            <category
                android:name="android.intent.category.LAUNCHER"
                tools:node="remove"/>
        </intent-filter>
    </activity>

即采用上文提到的 合并冲突解决 中的方式将启动LAUNCHER去掉。

不同变种启动的activity不同

可以将main中的启动activity去掉,在变体源集中将对应的activity添加进来,那么,按照合并优先级,变体源集中的activity会被合并进来。

不同的变体代码逻辑不同

可以看到上文 组合多个产品风味 中,有在产品风味中增加buildConfigField("String", "APP_TYPE", APP_TYPE_CAR)代码。

那么我们可以在代码中拿到这个标识,进行逻辑区分。

BuildConfig.APP_TYPE;

不同的代码和资源文件

在对应的源集下进行编码和存放资源文件即可,但是注意不要类路径和类名不要一样,不然会有冲突。

image
上一篇下一篇

猜你喜欢

热点阅读