最熟悉的陌生人 -- Gradle

96
阿_希爸
2016.11.14 16:34* 字数 7782

Gradle对于很多开发者来说有一种既熟悉又陌生的感觉,他是离我们那么近,以至于我每天做项目都需要他,但是他又是离我们那么的远,因为我从来都不知道他里面写的东西到底是啥意思。

对于这样的问题我也同样困惑,以前忙做这项目看到网上有一些教程拿过来就直接用了,并没有深入去理解,大部分精力还是消耗在了项目本身。

赶在这次GapYear的结尾,准备继续开发Android,便想着应该从Gradle下手,稍微深入理解下这东西。

在开篇之前先放出我参考的几篇博文:

Groovy脚本基础全攻略
Gradle脚本基础全攻略
深入理解Android之Gradle

有耐心的同学看完这三篇博文相信也不用继续读下面的内容了,我是将Android相关的内容总结了起来。不过添加了一些实例和理解在里面,希望能够对你理解Gradle有帮助。

1 Gradle

想要了解Gradle的话,先要知道下面的一些基础。

  1. Gradle使用的语言是GroovyGroovy语言相关内容请查阅Groovy官网
  2. Gradle有自己的DSL和构建流程。Gradle DSL
  3. Gradle插件Android Plugin有自己的DSL。Android Plugin DSL

其实如果你有时间有耐心有一定英文阅读能力,看完这些文档,相信对于Android开发下如何使用Gradle有非常深刻的理解,但是按照这三个文档的内容,估计没有十天半个月很难啃的透。这里也不会完全去带着你详细的介绍每个接口的用法,只是带你有一个明确的认识。

按照这个结构来一个一个讲。

1.1 Groovy

想要对一门语言理解透彻哪是一小段文字就能说得清道得明的呢,不过好在我们只是用他来做Gradle的配置,理解一些与Gradle相关的语法就可以了。

1.1.1 定义变量

Groovy作为一门动态语言,所以肯定是支持动态类型。那么在定义变量的时候是可以不指定变量类型的。

使用def关键字来定义变量,但是def关键字也是可以省略的。例如:

def a = 1           //使用def关键字定义变量a
a = 1               //省略def同样可以
def int a = 1       //指定变量类型
int a = 1       //省略def关键字,并指定变量类型

但是这并没有突出动态语言的特性,既然是动态语言,那么变量a可以是任意类型的,比如:

def a = 1       //声明的时候是一个int类型
a = 'hello groovy'      //然后赋值一个String
assert a instanceof String      //断言测试是没有问题的

但是如果在定义变量的时候指定了变量类型,那么这个变量就不在是动态变量,例如:

def int a = 1       //声明变量的时候指定变量a是一个int型
a = 'hello groovy'      //将一个String赋值给a的时候会报错

1.1.2 类型

由于Groovy是基于Java的,所以Java常用类型都有,而那些类型你肯定也都会用,所以就不一一列举了,这里先列出Groovy的类型,然后我们说几个Groovy中比较特别的部分。

  • String
  • char
  • byte
  • short
  • int
  • long
  • BigInteger
  • float
  • double
  • BigDecimal
  • boolean
  • List
  • Map
  • Array
1.1.2.1 String

Groovy里面的String比较的变态,因为他的表达方式太多,下面一一列举一下String几种形式:

def v = '支持占位符'

def a = '单引号形式'
def b = "双引号形式,${v}"
def c = '''三个单引号形式
支持多行
不支持占位符'''
def d = """三个双引号形式
支持多行
支持占位符,${v}"""
def e = /反斜杠形式
支持多行
支持占位符,${v}/
def f = $/美刀反斜杠形式
支持多行
支持占位符,${v}/$

String的形式虽然很多,但是还是建议你只使用'"这两种形式,以防别人看不懂你写的代码,并不是大家都有同样的Groovy语法基础,如果你一定要用的话,也请你注释清楚这是什么意思。

1.1.2.2 List

Groovy中的List使用的是java.util.List,默认使用ArrayList,如果你想要使用LinkedList需要特别指明;List是动态的,里面可以同时放不同类型的数据。

//动态类型,可以同时存放任意类型的值
def list = [0, "list", 1.2, false]      

//指定LinkedList的两种方式
LinkedList linkedList = [1, 2, 3]
def linkedList2 = [1, 2, 3] as java.util.LinkedList

//根据下标获得list数据的方式
assert list.get(2) == 1.2
assert list[2] == 1.2

关于List就说这么多,Java中的用法,这里基本都支持,所以也没有必要多讲,不过有关List的迭代我们到Closure的时候在讲。

1.1.2.3 Array数组

Groovy中,使用Array数组和使用List最大的区别就是,Array数组必须指定类型。

//声明数组的两种方式,必须指定类型
def String[] arrStr = ["a", "b", "c"]
def arrNum = [1, 2, 3] as int[]
//创建一个有界限范围的空数组
def String[] arrStr2 = new String[2]

//二维数组
def int[][] matrix2 = [[1, 2], [3, 4]]
1.1.2.4 Map

Groovy中的Map使用非常的简单。

//定义一个map
def person = [name: '阿,希爸', age: 30, sex: 'male']
//获取对应的key值
assert person['name'] == '阿,希爸'
assert person.age == 30
assert person.get('sex') == 'male'

//修改key对应的值
person.age == 31
//添加新的键值对
person.country =  '中国'
person.put('province', '安徽')

//空map
def emptyMap = [:]

1.1.3 函数

Groovy中的函数可以不指定返回值类型,可以不指定参数类型,函数代码中的最后一行表达式就是返回值,调用函数的时候可以省略圆括号。例如:

//定义一个不指定返回值类型的函数
def test(x, y){
    println "x=${x}, y=${y}"    //省略了圆括号
    x + y   //最后一行为返回值,想到于return x+y
}

//调用test时候省略圆括号,建议参数大于一个的时候还是加上的好
def result = test 1, 2
println result

1.1.4 Closure 闭包

首先说一下,这个闭包和javascript中的闭包不是一回事,如果你有javascript基础,看见闭包两个词就觉得没有看的必要那就得不偿失了,Groovy中的闭包更像是一个匿名函数。
Groovy官方的解释的话,闭包应该是一个能够接受参数,能够返回一个值,能够赋予一个变量的匿名代码块。
ClosureGroovy使用率非常非常高的一个语法,我们在build.gradle中写的大部分内容都是Closure,所以想要理解好我们build.gradle的内容,一定要好好理解Closure闭包这个概念。

1.1.4.1 定义一个闭包

定义一个闭包的语法如下:

{ [closureParameters -> ] statements }

[closureParameters -> ]部分能够接受参数,可以省略,参数可以是动态无类型的,也可以是指定类型的,但是如果指定类型了就必须按照类型传参。

如果你只有一个参数,那么你可以不用显示的指定这个参数,Groovy中有一个it参数可以指代。说出来可能有就绕,看例子吧:

//声明一个闭包,接受两个参数,返回他们相加的结果
{ a, b -> a + b }

//如果你只有一个参数,则可以省略,直接用it指代这个参数
{ it -> println it}
//完全可以写成
{ println it }
1.1.4.2 执行闭包

我们可以像执行函数一样去执行一个闭包,也可以调用闭包的call方法来执行闭包,看例子:

def closure = { a, b ->  a+b }
//执行函数的方式
closure(1, 2)
//可以省略括号
closure 1,2
//使用call方法执行
closure.call(3, 4)

//甚至可以这么写
({ println it }) "I am closure" //相当于匿名函数自执行
//等同于
def closure2 = { println it }
closure2 "I am closure"

有一个地方注意一下,{ println it }是默认有一个参数的,但是{ -> println it }这么写在执行的时候是会报错的,因为你显示声明了这个Closure没有参数。

1.1.4.3 Closure到底是个啥?

Closure是一个groovy.lang.Closure对象,这就解释了为什么他可以赋值给一个变量,也就是说,我们可以用这个来写函数式编程了。

让我们回到List,迭代List有两个方法,eacheachWithIndex,看例子:

[1, 2, 3].each {
    println "Item: $it"
}

['a', 'b', 'c'].eachWithIndex { it, i ->
    println "$i: $it"
}

我们可以看出,each方法接受一个类型是Closure类型的参数,然后执行这个参数。上面的方法会直接打印出每一个参数的值,看似很神奇,虽然我没看他源码是如何实现的,但是我们可以猜想一下each函数的样子。

def myList = [1, 2, 3, 4, 5]
//使用一个闭包作为参数
def each(Closure c){
    //迭代list,将list的item作为参数执行闭包
    for(int i = 0; i < myList.size(); i++){
        c(myList[i])
    }
}

def eachWithIndex(Closure c){
    for(int i = 0; i < myList.size(); i++){
        c(myList[i], i)
    }
}

//定义一个闭包,打印他的参数
def closure = { println it }
//执行each,注意:圆括号是可以省略的
each(closure)
eachWithIndex(closure)

//也就相当于
[1, 2, 3].each {
    println it
}

大概应该就是这个样子,如果你看到了源码可以跟我分享一下。

在Gradle中,基本上都是这样的方式。

有关Groovy的部分就说这么多,他还有很多功能语法,这里都没有涉及,毕竟我们不是要将他研究透,只是为了更好的理解Gradle,如果你有兴趣,还是仔细阅读一遍官网上的教程和说明,写的还是挺详细的,就是有点乱。你也可以看前面推荐的博客,我觉得写的还是挺细致的,我也是参考了很多。

1.2 Gradle基础

我想了很久这块应该写一些什么,Gradle的内容很多,但是,基本上都是写给那些利用Gradle写插件的人看的,比如开发Android Gradle插件的工程师们肯定要看,但是Google已经帮我们实现好了,对于小项目的APP工程师真的没有必要过多的去专研Gradle这东西,专研了之后其实你发现,没个屌用,别问我为什么知道。

如果你真的特别的想知道Gradle是怎么回事,去看下面的这篇文章:

Gradle脚本基础全攻略

文章作者基本上用的都是官网的例子,自己重新整理了顺序,还挺通熟易懂的。

如果你想亲手感受一下,建议你还是把Gradle的运行环境装好,亲自在命令行里面敲一下Gradle命令,写一些脚本文件执行一下。

如果你对Gradle框架确实没有什么太大兴趣的话,这里简单的说一下我认为Android开发者需要了解Gradle的一些基础内容。

Gradle的生命周期分为阶段:

  1. Initialization - 初始化阶段
  2. Configuration - 配置阶段
  3. Execution - 执行阶段

1.2.1 Initialization - 初始化阶段

初始化阶段会执行项目根目录下的settings.gradle文件,来分析哪些项目参与构建。

所以这个文件里面的内容经常是:

include ':app'
include ':libraries:someProject'

这是告诉Gradle这些项目需要编译,所以我们引入一些开源的项目的时候,需要在这里填上对应的项目名称,来告诉Gradle这些项目需要参与构建。

1.2.2 Configuration - 配置阶段

配置阶段会去加载所有参与构建的项目的build.gradle文件,会将每个build.gradle文件实例化为一个Gradle的project对象。然后分析project之间的依赖关系,下载依赖文件,分析project下的task之间的依赖关系。

他会先执行根目录下的build.gradle文件,一般这个文件的内容如下:

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.2.2'
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

buildscript中的dependencies是说这个项目依赖com.android.tools.build:gradle:2.2.2来构建。
allprojects 后面是一个闭包,相当于我们执行allprojects这个函数,传入了一个闭包作为参数。其实就是对所有的项目进行迭代,指定所有参与构建的项目使用的仓库。

1.2.3 Execution - 执行阶段

执行阶段来执行具体的task。

taskGradle中的最小执行单元,我们所有的构建,编译,打包,debug,test等都是执行了某一个task,一个project可以有多个task,task之间可以互相依赖。例如我有两个task,taskA和taskB,指定taskA依赖taskB,然后执行taskA,这时会先去执行taskB,taskB执行完毕后在执行taskA。

说到这可能会有疑问,我翻遍了build.gradle也没看见一个task长啥样,有一种被欺骗的赶脚!

其实不是,你点击AndroidStudio右侧的一个Gradle按钮,会打开一个面板,内容差不多是这样的:


里面的每一个条目都是一个task,那这些task是哪来的呢?

一个是根目录下的build.gradle中的

dependencies {
        classpath 'com.android.tools.build:gradle:2.2.2'
    }

一个是app目录下的build.gradle中的

apply plugin: 'com.android.application'

这两段代码决定的。也就是说,Gradle提供了一个框架,这个框架有一些运行的机制可以让你完成编译,但是至于怎么编译是由插件决定的。还好Google已经给我们写好了Android对应的Gradle工具,我们使用就可以了。

根目录下的build.gradledependencies {classpath 'com.android.tools.build:gradle:2.2.2'}是Android Gradle编译插件的版本。

app目录下的build.gradle中的apply plugin: 'com.android.application'是引入了Android的应用构建项目,还有com.android.library和com.android.test用来构建library和测试。

所有Android构建需要执行的task都封装在工具里,如果你有一些特殊需求的话,也可以自己写一些task。那么对于开发一个Android应用来说,最关键的部分就是如何来用AndroidGradle的插件了。

1.3 Android Gradle DSL

先扔出Android Gradle DSL的地址:

Android Gradle DSL

关于AndroidGradleDSL,这东西的主要功能还是提供了与构建相关的东西,他并不是一个特别神奇的东西,只是根据开发者的需求提供了很多便捷的方式,所以不用把他当成一个特别痛苦的东西而拒绝。

让我们从BuildType说起。

1.3.1 BuildType

BuildType的最大的应用场景是当你对测试版本和发布版本需要连接不同的服务的时候使用,他默认其实就有debug版本只是没有显示在build.gradle中,当然我们也可以根据需求创建多个buildType来使用。这里先贴出来Android生成的app目录下的build.gradle

apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.3"
    defaultConfig {
        applicationId "com.axiba.gradleDemo"
        minSdkVersion 15
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:23.4.0'
    testCompile 'junit:junit:4.12'
}

可以看到默认的buildTypes中就已经添加了一个release,我们来手动添加一个自定义的buildType,内容如下:

    buildTypes {
        myBuildType {
            //可调试
            debuggable true
            //给applicationId添加后缀,这样可同时在一部手机上存在多个版本
            applicationIdSuffix ".debug"
            //添加自定义变量
            buildConfigField "boolean", "isMyBuildType", "true"
        }
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

上面的代码我们创建了一个自定义的buildType名为myBuildType,其中的内容我相信你看注释应该就很明白了,其中的applicationIdSuffix是给你的包名添加一个后缀,这样你的手机中就可以同时存在多个版本,因为你可能正在开发新的版本,但是你已经发布的版本要在手机上随时观察,这样你是需要同时有多个版本在手机上的。

关于自定义变量和debuggable可能你会有疑惑,这东西配置了怎么用啊?

当修改了build.gradle文件之后我们需要sync一下,然后你会发现你的Gradle面板中,:app结构下的build中多了一个名为compileMyBuildTypeSources的task,如下图


然后双击执行这个task,完成之后我们到app/build/gengrated/source/buildConfig/myBuildType目录,如图


在该目录下生成了一个BuildConfig.java文件,文件内容如下:

public final class BuildConfig {
  public static final boolean DEBUG = Boolean.parseBoolean("true");
  public static final String APPLICATION_ID = "com.axiba.gradledemo.debug";
  public static final String BUILD_TYPE = "myBuildType";
  public static final String FLAVOR = "";
  public static final int VERSION_CODE = 1;
  public static final String VERSION_NAME = "1.0";
  // Fields from build type: myBuildType
  public static final boolean isMyBuildType = true;
}

可以看到我们在buildType定义的内容在这里都体现了出来,包括最后一行的自定义变量。但是除非你有特别的需要,否则真的不建议你使用这个变量。

最常用的可能就是DEBUG了,例如有写Log你只想在debug的时候显示,release以后就不在显示,可以在你的代码中这样写:

if (BuildConfig.DEBUG) {
    Log.d("tag", "something happened");
}

如果你每次都这么写log觉得累的话,可以自己封装一个log工具类。

有关BuildType先说到这里,后面涉及到的时候我们在慢慢补充。

关于BuildType更多的内容可以看官方DSL文档 -> BuildType

1.3.2 signingConfigs

如果你按照上面的内容添加了一个myBuildType之后你会发现,在你的Gradle面板中,:app下的build中是多了assembleMyBuildTypecompileMyBuildTypeSourcescompileMyBuildTypeUnitTestSources三个相关的Task,但是install结构下却找不到installMyBuildType这样的Task。原因是AndroidStudio不会给没有指定签名的BuildType生成install相关的Task。这里我们来为myBuildType添加一个签名,并介绍一下签名相关的内容。

我们在android闭包中添加这么一段代码:

    signingConfigs {
        debug {
            storeFile file("/Users/username/.android/debug.keystore")
        }
    }

这里的storeFile用的是AndroidStudio生成的一个默认的debug.keystore。如果你的debug没有特殊需求可以使用这个keystore文件。
Linux和MaxOS系统下,这个文件在~/.android/debug.keystore
Windows XP:C:\Documents and Settings\<user>.android\debug.keystore
Windows Vista and Windows 7, 8, and 10:C:\Users\<user>.android\debug.keystore

但是这么写肯定是有问题的,如果你使用git多人开发的话每个人的地址是不一样的,解决的办法是这样:

首先在app目录下创建一个keystore.properties文件
内容入下

debugKeyStoreFile=/Users/username/.android/debug.keystore

然后在.gitignore中添加keystore.properties,将keystore.properties文件排除

这样你的git中每个人可以根据自己的环境来配置keystore.properties这个文件。

最后在build.gradle中使用配置内容,这里直接贴出完整的代码:

apply plugin: 'com.android.application'

def keystoreProperties = new Properties()
//加载keystore.properties文件
keystoreProperties.load(new FileInputStream(file("keystore.properties")))


android {
    compileSdkVersion 23
    buildToolsVersion "23.0.3"
    defaultConfig {
        applicationId "com.axiba.gradledemo"
        minSdkVersion 15
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }


    signingConfigs {
        debug {
            //指定keystore    文件
            storeFile file(keystoreProperties['debugKeyStoreFile'])
//            storeFile file("/Users/username/.android/debug.keystore")
        }
    }

    buildTypes {
        myBuildType {
            //可调试
            debuggable true
            //给applicationId添加后缀,这样可同时在一部手机上存在多个版本
            applicationIdSuffix ".debug"
            //指定签名信息
            signingConfig signingConfigs.debug
            //添加自定义变量
            buildConfigField "boolean", "isMyBuildType", "true"
        }
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

如果没有特殊要求,signingConfigsdebugrelease就够了,release相关的配置信息你也可以写在keystore.properties文件中,如果你觉得不安全可以使用命令行输入来输入密码。

keystore.properties文件内容:

debugKeyStoreFile=/Users/username/.android/debug.keystore
releaseKeyStoreFile=yourReleaseKeyStoreFile
releaseStorePassword=yourStorePassword
releaseKeyAlias=yourKeyAliasName
releaseKeyPassword=yourKeyAliasPassword

build.gradle文件内容:

apply plugin: 'com.android.application'

def keystoreProperties = new Properties()
keystoreProperties.load(new FileInputStream(file("keystore.properties")))

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.3"
    defaultConfig {
        applicationId "com.axiba.gradledemo"
        minSdkVersion 15
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }

    signingConfigs {
        debug {
            storeFile file(keystoreProperties['debugKeyStoreFile'])
//            storeFile file("/Users/liukun/.android/debug.keystore")
        }
        release {
            storeFile file(keystoreProperties['releaseKeyStoreFile'])
            storePassword keystoreProperties['releaseStorePassword']
            keyAlias keystoreProperties['releaseKeyAlias']
            keyPassword keystoreProperties['releaseKeyPassword']
        }
    }

    buildTypes {
        myBuildType {
            //可调试
            debuggable true
            //给applicationId添加后缀,这样可同时在一部手机上存在多个版本
            applicationIdSuffix ".debug"
            //指定签名信息
            signingConfig signingConfigs.debug
            //添加自定义变量
            buildConfigField "boolean", "isMyBuildType", "true"
        }
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.release
        }
    }
}

有关签名的部分就说到这,更多相关的内容请参考官方文档

1.3.3 install之后自动打开

到目前为止,在你的Gradle -> :app -> install结构下应该可以找到installMyBuildType这个Task了,双击执行这个Task,app会装到目标手机或者模拟器中,但是安装是安装了,为什么没有自动打开呢?我们点击运行按钮的时候不都是自动打开安装好的app的么。

其实点击运行之后,我们看一下输出可以知道adb做了哪些事,下面的内容是点击运行按钮之后输出的内容。

11/13 22:24:52: Launching app
$ adb push /Users/username/Developer/workspace/androidStudio/GradleDemo/app/build/outputs/apk/app-debug.apk /data/local/tmp/com.axiba.gradledemo
$ adb shell pm install -r "/data/local/tmp/com.axiba.gradledemo"
    pkg: /data/local/tmp/com.axiba.gradledemo
Success

$ adb shell am start -n "com.axiba.gradledemo/com.axiba.gradledemo.MainActivity" -a android.intent.action.MAIN -c android.intent.category.LAUNCHER

他是先将编译好的APK文件推送到你的目标设备,在从目标设备中进行安装,安装完成之后启动。

install只会将文件安装到目标设备,并不会启动这个APP。

这个章节我们就来根据前面学习过的知识,让他启动起来。
既然目标是让他启动起来,那么肯定是要在Task中执行adb命令了,然后我们要在安装完成之后启动app,那这个Task就要依赖于install这个task,我们在android域中添加一个task,名为openApp,并让他依赖installMyBuildType,代码如下:

android {
    ...
    task openApp (dependsOn: "installMyBuildType") << {

    }
}

这样就创建好Task了,sync一下之后你可以在Gradle -> :app -> other结构下找到这个Task。接下来解决执行adb命令的问题。

其实我们是想让adb执行这段命令adb shell am start -n "com.axiba.gradledemo/com.axiba.gradledemo.MainActivity"来启动MainActivity。让Gradle在命令行中执行命令可以使用project.exec这个函数,内容如下:

android {
    ...
    task openApp (dependsOn: "installMyBuildType") << {
        project.exec {
            executable = adbExecutable
            args 'shell'
            args 'am'
            args 'start'
            args '-n'
            args 'com.axiba.gradledemo/com.axiba.gradledemo.MainActivity'
        }
    }
}

其中的adbExecutable是你AndroidSDK中的adb工具所在的目录路径,后面的agrs是执行命令的参数,貌似只有有空格就要写一个args。

目前我们只是将命令行中的执行命令复制过来,但是这里有一个问题,就是在buildType.myBuildType中我们添加了applicationIdSuffix ".debug",这会导致我们最后一个路径有问题,我们可以这样来修改

task openApp (dependsOn: "installMyBuildType") << {
    def applicationId = android.defaultConfig.applicationId

    if(buildTypes.myBuildType.applicationIdSuffix){
        applicationId += buildTypes.myBuildType.applicationIdSuffix
    }

    project.exec {
        executable = adbExecutable
        args 'shell'
        args 'am'
        args 'start'
        args '-n'
        args "${applicationId}/com.axiba.gradledemo.MainActivity"
    }
}

现在双击openApp这个Task可以安装并执行app了。

这里也许你会想,那我是不是每个BuildType都要跟着建立这样一个task,多麻烦,我宁可到手机上点一下app去执行。那你就图样图森破了。Gradle提供了动态创建Task的功能,我们可以写一个函数来生成所有相关的Task。

build.gradle文件中添加这样一段代码,注意不要放到android的闭包里面。

def initTasksOpenAndInstall(){

    //遍历buildTypes
    android.buildTypes.each { buildType ->

        println buildType
        //buildType.name首字母大写
        def buildTypeName = getFirstLetterUpper(buildType.name);

        //如果有后缀,添加后缀
        def applicationId = android.defaultConfig.applicationId

        if(buildType.applicationIdSuffix){
            applicationId += buildType.applicationIdSuffix

        }

        //创建task
        task "installAndOpen${buildTypeName}" (dependsOn: "install${buildTypeName}") {

            //将task添加到install分组中
            group "install"

            //通过adb来执行启动应用
            doLast {

                //执行adb命令来启动app
                project.exec {
                    executable = android.adbExecutable
                    args 'shell'
                    args 'am'
                    args 'start'
                    args '-n'
                    args "${applicationId}/com.axiba.gradledemo.MainActivity"
                }
            }

        }
    }
}

initTasksOpenAndInstall()


//第一个字母大写
def getFirstLetterUpper(name){

    def firstLetter = name.charAt(0).toUpperCase().toString();

    if(name.length() > 1){
        name = firstLetter + name.substring(1)
    } else {
        name = firstLetter
    }

    return name;
}

我们创建了一个initTasksOpenAndInstall函数,他的主要功能是遍历buildTypes,给每个buildType添加一个installAndOpenTypeName的task,这个task依赖于installTypeName,并将这个task添加到install分组之中,点击sync按钮之后,你可以在Gradle -> :app -> install分组中发现多了几个installAndOpen的task。双击执行task,应用安装到了目标设备并自动打开。

1.3.4 productFlavors

productFlavor的应用场景最多的是为不同的渠道打包,比如你在用友盟的时候要分析渠道来源,你要和应用市场做首发活动的时候需要在启动页添加他们的LOGO。

创建一个productFlavor也是非常的简单,在android的闭包中添加代码:

    productFlavors {
        wandoujia {
            applicationIdSuffix '.wandoujia'
        }
    }

新增一个productFlavor名为wandoujia,我们希望这是专门在豌豆荚上线的应用。注意这里加了一个applicationId的后缀,他的功能是和buildType一样的。

现在sync一下之后你会发现Gradle里面多了很多task,例如:installWandoujiaDebuginstallWandoujiaReleaseinstallWandoujiaMyBuildType等task。这是因为AndroidGradle会将productFlavor对应每一个buildType都生成一个版本。那么问题来了,我在myBuildType中添加了一个applicationId的后缀.debug,现在我又给wandoujia添加了一个applicationId后缀.wandoujia,那么WandoujiaMyBuildType版本的applicationId应该是什么呢?
我们这个项目生成的applicationId是这样的
com.axiba.gradledemo.wandoujia.debug

也就是说,他会先添加productFlavor的后缀,在添加buildType的后缀。

productFlavor比较常用的一个用能就是用他来修改AndroidManifest.xml文件中的参数。

例如当你用友盟分析渠道的时候,你的AndroidManifest.xml文件中应该有这样的代码:

<meta-data
            android:name="UMENG_CHANNEL"
            android:value="${UMENG_CHANNEL_VALUE}" />

注意value部分要使用占位符的方式。

然后修改productFlavor的wandoujia部分。

    productFlavors {
        wandoujia {
            applicationIdSuffix '.wandoujia'
            manifestPlaceholders = [UMENG_CHANNEL_VALUE: "wandoujia"]
        }
    }

如果你的渠道很多,可以通过遍历productFlavors的方式来替换:

    productFlavors.all {
        flavor -> flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name]
    }

另外可以在defaultConfig中添加一个默认值

    defaultConfig {
        ....
        manifestPlaceholders = [UMENG_CHANNEL_VALUE: "axiba"]
    }

如果你想为你的输出文件格式化名称的话,可以参考下面的代码。

    applicationVariants.all { variant ->
        variant.outputs.each { output ->
            def outputFile = output.outputFile
            if (outputFile != null && outputFile.name.endsWith('.apk')) {
                def fileName = "GradleDemo_v${defaultConfig.versionName}_${variant.productFlavors[0].name}_release.apk"
                output.outputFile = new File(outputFile.parent, fileName)
            }
        }
    }

有关Android Gradle Plugin相关的内容就先写到这里,他的功能很多,而且还在扩展新的功能,这里就不一一介绍了,主要的原因是很多东西我也不懂,就不误导大家了。之前创建initTasksOpenAndInstall方法可以根据buildType来生成相关的Task,后面我将他补全了,添加了productFlavor+buildType的形式来建立Task,有兴趣的同学可以到我的git上面去看。

GradleDemo

如果你在执行Gradle的过程中,发生了错误,那么我建议你在终端(或者命令行)中输入相关的Gradle命令,如果出错了,他会给你详细的错误报告。

2 Gradle Warpper

为什么我在Github下了一个项目导入要这么久?

Gradle到底在干点啥浪费了我这么多青春?

为什么我对着硬盘里面的大姐姐撸了一发了还没导入成功?

一般这种情况的罪魁祸首是Gradle Wapper,他的主要作用是来适配不同的Gradle版本的。

比如你在github上面下了一个项目,项目的目录结构大概是这样的:

.
├── app
├── build.gradle
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
└── settings.gradle

其中gradlew文件是Linux和MacOS环境下运行的,gradlew.bat是window环境下运行的。而定义项目的执行版本在/gradle/wrapper/gradle-wrapper.properties文件中,内容如下:

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

这文件是说我这个项目需要你用2.10版本的gradle来编译。
如果你当前AndroidStudio所使用的Gradle版本与文件中指定的不一致,Gradle会去下载对应的版本,下载好了用这个版本去编译。

一个Gradle的压缩文件大概50M以上,如果你的网络环境差一点,是比较痛苦的。

解决这个问题的方法有两个,一个是让gradle-wrapper.properties中的Gradle版本与当前AndroidStudio所使用的Gradle版本一致,一个是自己下载好所需要的版本。但是两个方法都会有一些问题,我们来一个一个分析。

使用同一个版本的Gradle

首先查看你当前AndroidStudio所使用的Gradle是什么版本,然后将gradle-wrapper.properties中的Gradle改成同一个版本。

查看AndroidStudioGradle版本的方法如下图:


这里建议将Gradle home配置成本地环境。也就是说自己下载一个Gradle然后配置到系统环境中,这样在终端就可以使用了。

上图中所使用的Gradle版本是2.14.1。而刚才我们列出来的gradle-wrapper.properties中的Gradle版本是2.10。

gradle-wrapper.properties中的Gradle版本是2.14.1就可以避免去下载2.10版本。但是这样做有风险,如果新的版本对旧版本的一些语法不在支持了,那么就会编译错误,结果会得不偿失,除非你对版本变更了解的非常透彻,能够手动将文件变动的地方重新修改,那么用这种方法没有问题。但是如果你不了解,建议不要采用这种方法。

接着介绍第二种。

手动下载Gradle版本

既然对Gradle的版本变更不熟悉,我们就按照gradle-wrapper.properties文件中的配置内容,使用下载工具去下载,来避免他下载过慢的问题,而且如果我们积累了比较常用的版本,也就不用每次都去下载了。

gradle-wrapper.properties中基本都给出来了下载链接,https://services.gradle.org/distributions/可以查看所有版本。
如果你的网络访问这个页面或者使用下载工具还是下载很慢的话,可以到androiddevtools上面去下载相关的安装包。

但是这种做法也有一个问题,那就是他的下载机制。还是用刚才的栗子。

目前我的AndroidStudio使用的Gradle是2.14.1。
我的gradle-wrapper.properties中的Gradle版本是2.10。

那么执行编译,Gradle会先到gradle-wrapper.properties配置的目标路径下去找这个文件是否存在,其中的GRADLE_USER_HOME一般是对应下图中的Service directory path


那么连起来就是/Users/username/.gradle/wrapper/dists这个目录,为了后面引用我们给这个路径起名叫做wrapperPath

如果路径下不存在这个版本的目录,那么Gradle会去创建相关文件目录,并开始下载对应版本的压缩包。内容如下图。


如果你现在嫌他下载太慢,删除gradle-2.10-all目录,然后将自己下载的压缩文件解压出来以为就万事大吉了,那就太天真了,当年我就是这么天真的。

我们先来看他下载好了是什么样的。


我们可以看到多了一个gradle-2.10-all.zip.part变成了gradle-2.10-all.zip.ok。同时多了文件压缩包和解压出来的对应文件目录。并且在一个目录名为看似一串随机码的目录下。

解释一下,Gradle会在 /wrapperPath/gradle-2.10-all/随机码/ 目录下去检查是否有一个gradle-2.10-all.zip.ok文件,如果有就直接编译,即使你将gradle-2.10这个目录删除了,他的编译会报错,但是他还是不会去下载。
如果没有这个文件,那么Gradle就会开始去下载gradle-2.10-all.zip文件,gradle-2.10-all.zip.part文件就是正在下载的gradle-2.10-all.zip文件,下载完成自动解压,在执行编译。

其中的随机码目录根据官方的介绍应该是由SHA-256 hash生成的,只是我的猜想。

如果我们想要骗过他就让他先执行下载,生成相关的文件目录,然后将你下载好的压缩包解压后放进去,在创建一个gradle-2.10-all.zip.ok文件,就可以了。


我将1.7以后的版本都手动配置好了。

以上,相信你能够填上Gradle Wrapper给你挖的坑了。

3 加速编译过程

3.1 Gradle Daemon

Gradle Daemon是一个长期在后台执行的一个进程,用来避免每次开始编译在JVM启动Gradle所消耗的时间,同时也会在内存中保存一些你的项目数据来加速编译过程。Gradle3.0默认是开启Daemon的。

想要开启Daemon功能可以在.gradle/gradle.properties文件中添加org.gradle.daemon = true

3.2 Gradle Parallel

Gradle Parallel一般是对多个项目使用并行编译,他会在配置阶段对项目进行预编译,分析项目之间的依赖关系,而且已经编译过的项目,如果没有更改,直接用上次编译好的去构建目标项目,例如我们Android开发时在libraries里面的项目,只要编译一次就可以了,不会每次都去编译。

可以通过在项目根目录下的gradle.properties中添加org.gradle.parallel=true来开启此功能。

3.3 Configuration on demand

Configuration on demand简单的说就是能够缩短multi-projects的配置时间。

可以通过在项目根目录下的gradle.properties中添加org.gradle.configureondemand=true来开启此功能。

3.4 增加jvm进程的最大堆内存

通过修改项目根目录下的gradle.properties中的org.gradle.jvmargs属性来设置。根据你的配置可以改成org.gradle.jvmargs=-Xmx2048m 或者 org.gradle.jvmargs=-Xmx3072m

3.5 使用固定版本的依赖项

如果你在依赖项中使用了动态版本配置,那么编译的时候会去检查是否有更新的版本,如果有就会下载新版本的依赖项。所以尽量使用固定版本的依赖,减少 + 号的使用。

最后

这篇文写的稍微有点长了,也是超出了我的预料之外,前前后后写了大概有10几天,翻遍了groovy的文档,翻遍了gradle的文档,也翻遍了AndroidGradle DSL的文档,最后得出来的一些小心得,希望对你有帮助。文中的很多技巧和代码你在其他地方也许都能找到,但是本文意在你能够看懂Gradle里面的内容到底是怎么回事,做了些什么,遇到相关gradle的问题去哪翻文档。

如果有人发现哪里有写的不对的地方请联系我,我尽快修改,别误导了他人。

如果这篇文字有帮助到你,请到github上赏我一个star。

最后感谢你能坚持看到这里。

好了不多说了,哄我家希宝睡觉去了~

阿_希爸的技术博文