从一个build.gradle的成长史出发

从一个build.gradle的成长史出发

从一个build.gradle的成长史出发

自从切换到Android Studio开发环境后,Gradle也开始逐渐进入我们的视野。Gradle是一个构建框架,它采用Groovy作为主要的脚本语言。Gradle的构建工作是由很多task一起完成的,每个task又包含一个或多个action。并且task间存在依赖关系,被依赖的task会在依赖它的task之前执行,众多task相互依赖完成整个构建工作。Groovy其实也是一种基于JVM的语言,这意味着它与Java一样最终会被编译为字节码运行在JVM之上,Groovy与Java语法风格类似,并且Groovy语法比Java要更加灵活方便。理解Groovy(特别闭包的概念)对理解Gradle框架有很大帮助,关于Groovy详细的介绍可以参阅精通Groovy
本文的重点是记录一个build.gradle文件的成长过程,读完本文你可以使用Gradle来完成一些很灵活的构建需求。并且以build.gradle为切入点学习Android Gradle Plugin

出生


在Android Studio中创建一个Project后,默认会为你的Project生成一个叫app的Module。在Project目录下会有一个build.gradle和一个settings.gradle。在app目录下也会有一个build.gradle文件。目录的结构大致如图:

Alt text

sourceSets

Android Studio自动为app Module在src目录下创建了三个sourceSets,分别是androidTest,main和test,其中包含了不同用途的源代码结构。构建的时候Gradle可以使用不同的sourceSets指定的路径去构建不同类型的apk。

  • main sourceSets : 包含了应用的实现代码及资源。在没有创建其他sourceSets目录时,构建产生的所有apk都使用该sourceSets路径下的代码及资源。
  • androidTest sourceSets : 包含了用于instrumented test的实现代码及资源。
  • test sourceSets : 包含了用于单元测试的实现代码及资源。
    可以通过sourceSets task查看当前项目的sourceSets使用情况:
    ./gradlew sourceSets
------------------------------------------------------------
Project :app
------------------------------------------------------------

androidTest
-----------
Compile configuration: androidTestCompile
build.gradle name: android.sourceSets.androidTest
Java sources: [app/src/androidTest/java]
Manifest file: app/src/androidTest/AndroidManifest.xml
Android resources: [app/src/androidTest/res]
Assets: [app/src/androidTest/assets]
AIDL sources: [app/src/androidTest/aidl]
RenderScript sources: [app/src/androidTest/rs]
JNI sources: [app/src/androidTest/jni]
JNI libraries: [app/src/androidTest/jniLibs]
Java-style resources: [app/src/androidTest/resources]

debug
-----
Compile configuration: debugCompile
build.gradle name: android.sourceSets.debug
Java sources: [app/src/debug/java]
Manifest file: app/src/debug/AndroidManifest.xml
Android resources: [app/src/debug/res]
Assets: [app/src/debug/assets]
AIDL sources: [app/src/debug/aidl]
RenderScript sources: [app/src/debug/rs]
JNI sources: [app/src/debug/jni]
JNI libraries: [app/src/debug/jniLibs]
Java-style resources: [app/src/debug/resources]

main
----
Compile configuration: compile
build.gradle name: android.sourceSets.main
Java sources: [app/src/main/java]
Manifest file: app/src/main/AndroidManifest.xml
Android resources: [app/src/main/res]
Assets: [app/src/main/assets]
AIDL sources: [app/src/main/aidl]
RenderScript sources: [app/src/main/rs]
JNI sources: [app/src/main/jni]
JNI libraries: [app/src/main/jniLibs]
Java-style resources: [app/src/main/resources]

release
-------
Compile configuration: releaseCompile
build.gradle name: android.sourceSets.release
Java sources: [app/src/release/java]
Manifest file: app/src/release/AndroidManifest.xml
Android resources: [app/src/release/res]
Assets: [app/src/release/assets]
AIDL sources: [app/src/release/aidl]
RenderScript sources: [app/src/release/rs]
JNI sources: [app/src/release/jni]
JNI libraries: [app/src/release/jniLibs]
Java-style resources: [app/src/release/resources]

test
----
Compile configuration: testCompile
build.gradle name: android.sourceSets.test
Java sources: [app/src/test/java]
Java-style resources: [app/src/test/resources]

testDebug
---------
Compile configuration: testDebugCompile
build.gradle name: android.sourceSets.testDebug
Java sources: [app/src/testDebug/java]
Java-style resources: [app/src/testDebug/resources]

testRelease
-----------
Compile configuration: testReleaseCompile
build.gradle name: android.sourceSets.testRelease
Java sources: [app/src/testRelease/java]
Java-style resources: [app/src/testRelease/resources]


BUILD SUCCESSFUL

sourceSets task告诉我们目前项目可以根据不同的变种构建出多少种apk以及每种变种对应的sourceSets路径。
可以看到除了androidTest和test的sourceSets其实默认还为我们提供了debug和release两种buildType(buildType后面会详细介绍)所对应的sourceSets。只是Android Studio没有自动为我们创建他们的目录。
sourceSets有什么用?举个例子,比如我们希望debug和release版本的apk在应用启动的时候分别加载一个不同内容的assets资源来初始化一些配置信息。一个优雅的实现方式是使用sourceSets,可以在src目录下为debug及release创建对应的sourceSets目录:

Alt text
分别新建app/src/debug/assets/config.xml文件和app/src/release/assets/config.xml文件。他们的内容可以完全不同,只要保证名字相同,我们就可以在代码中不用任何if-else的结构直接使用config.xml即可。在构建的过程中Gradle会帮我们将对应的config.xml打包到对应版本的apk中。也就是说,不创建debug或者release的sourceSets目录时,Gradle会使用main sourceSets中的代码和资源构建apk,当特定类型的sourceSets目录存在时,构建该特定类型的apk时会使用对应sourceSets下的代码或者资源而不是main sourceSets。

Project

  • settings.gradle : 根目录下的settings.gradle是不可缺少的。它的作用在于声明构建Project时需要包含的Module(在这里默认的Project中只有app这一个Module)。
  • build.gradle : 根目录下的build.gradle是为Project全局做配置,一般使用默认自动生成的配置就可以了。其中主要完成两件事情:
    1. 声明了Project依赖的Android Gradle Plugin版本。
    2. 指定了Module依赖的远程库的repositories。
      Note : 根目录下的build.gradle中配置的是Android Gradle Plugin的版本,不是Gradle的版本。Gradle的版本是在gradle/wrapper/gradle-wrapper.properies文件中配置的。但是Android Gradle Plugin的版本和Gradle的版本是有匹配关系的,在实际项目中要保证二者的匹配。
      Alt text

Module

Module目录下也有一个build.gradle,这个build.gradle是本文说明的重点。其内容如下:
app/build.gradle

// 1.应用android提供的application插件
apply plugin: 'com.android.application'

// 2.在android block中声明跟编译打包应用相关的一些属性
android {
compileSdkVersion 25
buildToolsVersion "25.0.3"
// 2.1 defaultConfig block中配置默认的编译配置属性
defaultConfig {
applicationId "com.catsuo.gradledemo"
minSdkVersion 15
targetSdkVersion 25
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
// 2.2 buildTypes block中配置该Module的编译类型
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}

// 3.定义Module的依赖关系
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:25.3.1'
compile 'com.android.support.constraint:constraint-layout:1.0.2'
testCompile 'junit:junit:4.12'
}
这是它刚出生时的样子,注释中将其分为3个部分,下面依次介绍。

apply plugin: ‘com.android.application’

这里其实是调用了Gradle对象的apply方法,plugin: ‘com.android.application’ 是传入的参数,apply有多个重载,这里调用的是参数为Map形式的重载。可能你会觉得怎么看都不像在调用一个方法,也许这么写会更容易理解:
apply(plugin: ‘com.android.application’)
事实上这么写也是可以正常编译的,只是Groovy在调用方法时允许省略括号。在编译过程中一个项目只有一个全局的Gradle对象,但可能会有多个Project对象,每个有build.gradle的目录就对应了一个Project对象。避免您被拉跑了,本文就不扯这些有的没的了。你只要知道在build.gradle文件中的这些配置项都不是空穴来风,因为Gradle基于Groovy,Groovy同java一样是基于JVM的语言,所以最终在交给虚拟机调用时也一样是有类有对象有属性有方法的。关于更多细节可以参阅Gradle APIAndroid Gradle Plugin 文档。这里我们还是聚焦回来,关注一个build.gradle是怎么慢慢长大的。
Android Gradle Plugin包含了三个扩展插件:

  • AppExtension : com.android.application,用于编译app工程。
  • LibraryExtension : com.android.library,用于编译library工程。
  • TestExtension : com.android.test,用于编译Test工程。
    我们的例子中apply传递进来的参数是com.android.application,说明我们的这个build.gradle是用来编译一个app的。

android block

android block主要用于配置该Module的编译属性,在上面刚出生的build.gradle宝宝里首先配置了该Module的编译SDK版本,然后配置了所使用的编译工具的版本。这些都是根据自己的项目需求可调整的。接下来内部包含2个子block:

  • defaultConfig : defaultConfig block为该应用的所有渠道包提供了一个默认的设置,它内部设置的属性可以在编译的时候动态的去覆盖AndroidManifest.xml中设置的属性,同时后面介绍多渠道打包时你会看到,在defaultConfig block内部配置的属性也可以被具体的某一渠道配置的属性覆盖。
    这里注意applicationId属性,很多人认为它跟packageName是一回事。非也非也。
    applicationId是在设备上和应用市场中对应用的唯一标识,一个应用发布后其applicationId就决不能再更改,否则在应用市场中将认为是不同的应用。而packageName指的是代码级别的包名,是一个代码的命名空间。只不过在生成build.gradle时studio自动将applicationId设置为和packageName相同,但后面说道多渠道打包时你会看到,这里的applicationId是可以更改的,而且更改后再运行时通过Context.getPackageName()获取的名字其实是applicationid而不是packageName。因为packageName对应的只是代码级别的包名。

  • buildTypes : 这个block设置Module的编译类型。默认提供了release和debug两种类型的buildType。我们也可以定义自己的buildType。在这个block中我们可以灵活的配置不同编译类型的定制化需求,后面build.gradle宝宝长大后你会看到它的应用。目前的build.gradle宝宝在这个block中只是对Release的buildType做了配置,minifyEnabled配置是否开启混淆,并且对包体的大小也有优化。proguardFiles则配置了混淆文件的路径。

dependencies

dependencies block配置该Module的依赖关系。该block是由Gradle框架提供的,它其实是Project对象的一个方法。
dependencies有三种类型:

  • 本地Module依赖
    • compile project(‘:mylibrary’)
      该配置指定该Module依赖名为mylibrary的Module,这个mylibrary必须在Project的settings.gradle中被包含进来。
  • 本地库依赖

    • compile fileTree(dir: ‘libs’, include: [‘*.jar’])
      Gradle是按照相对当前build.gradle的路径来读取文件的,所以上面的配置告诉编译系统将当前Module的libs/路径下的jar都添加到依赖中。
    • compile files(‘libs/foo.jar’, ‘libs/bar.jar’)
      指定依赖单独的jar文件。
  • 远程库依赖

    • compile ‘com.example.android:app-magic:12.3’
      指定Module依赖命名空间为com.example.android,名字是app-magic,版本为12.3的远程库。Gradle会根据Project根目录的build.gradle中配置的repositories去查找远程库。
依赖方式

上面都只用到了一种依赖方式,即compile,实际上在dependencies block中我们还可以声明使用其他的一些依赖方式,每一种依赖方式都告诉Gradle以怎样的方式去依赖指定的库,下面列出了几种依赖方式:

  • compile : Gradle将指定的依赖添加到classpath和apk中。也是通常使用最多的一种方式。
  • apk : Gradle只将指定的依赖添加到apk中。这种方式只能用来依赖jar库,它不支持本地模块依赖或者aar库的依赖。
  • provided : Gradle只将指定的依赖添加到classpath中。

上面提到的依赖方式作用于应用的main sourceSets,所以如果这时候我们有多个渠道包,并且没有为每个渠道包新建sourceSets路径的话,依赖方式会对所有的渠道包都生效,你当然也可以为每个渠道包指定特定的依赖方式,只要将依赖方式大写,并在前面加上渠道名作为前缀组合成一个新的针对渠道的依赖方式,如下代码:

dependencies {
freeCompile 'com.google.firebase:firebase-ads:9.8.0'
}

表示free这个渠道编译打包时对com.google.firebase:firebase-ads:9.8.0库的依赖采用compile这种方式。
如果你需要结合渠道包和编译类型两个维度来定制依赖方式时,需要先在configurations block中初始化依赖方式的名字,如下代码:

configurations {
// Initializes a placeholder for the freeDebugApk dependency configuration.
freeDebugApk {}
}

dependencies {
freeDebugApk fileTree(dir: 'libs', include: ['*.jar'])
}

在configurations block中初始化了freeDebugApk这种依赖方式,该依赖方式表明对free渠道的buildType为debug的包采用apk的依赖方式依赖libs目录下的所有jar库。

Gradle本身还提供了test前缀和androidTest前缀的依赖方式来指明应用于test sourceSets和androidTest sourceSets的依赖。比如上面刚出生的build.gradle宝宝就使用了androidTestCompile依赖方式表明针对androidTest sourceSets以compile的方式依赖com.android.support.test.espresso:espresso-core:2.2.2库。用testCompile依赖方式表明针对test sourceSets以compile的方式依赖junit:junit:4.12库。
关于渠道包后面会详细介绍,关于依赖方式的更多细节可以参阅官方文档

到这里刚出生的build.gradle宝宝基本介绍完毕,接下来看看长大后的build.gradle发生了哪些变化。

长大


善变的产品经理给善良忠厚的开发小哥提了很多需求,这其中可能包括:

  1. 开发的产品需要同时提供给内部和外部测试(假如该产品与外部公司合作),内部和外部测试的版本需要访问不同的url。
  2. 开发的产品需要提供专业版(收费)和普通版(免费),并且两个版本需要同时能在应用市场上线。

当然你可以通过一些比较原始的方法达到上述的目的,但是通过Gradle构建框架完全可以更高效的帮助我们完成这些事情。

需求一

直接上长大了的build.gradle:
app/build.gradle

apply plugin: 'com.android.application'

android {
compileSdkVersion 25
buildToolsVersion "25.0.3"
defaultConfig {
applicationId "com.catsuo.gradledemo"
minSdkVersion 15
targetSdkVersion 25
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}

buildTypes {
debug {
buildConfigField "String", "URL", "\"http://debug.xxx\""
}
innerDebug {
initWith debug
buildConfigField "String", "URL", "\"http://innerdebug.xxx\""
}
outerDebug {
initWith debug
buildConfigField "String", "URL", "\"http://outerDebug.xxx\""
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
buildConfigField "String", "URL", "\"http://release.xxx\""
}
}
}

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:25.3.1'
compile 'com.android.support.constraint:constraint-layout:1.0.2'
testCompile 'junit:junit:4.12'
}

build.gradle长大了些,除了默认提供的release和debug外,我们在buildTypes block中定义了自己的buildType,在innerDebug block和outerDebug block中的initWith和buildConfigField其实都是buildType类的方法,这里只不过依旧采用了省略括号的方式调用:

  • initWith : 表示新定义的buildType将从参数指定的buildType复制所有的属性。使用debug为参数,说明innerDebug和outerDebug两种buildType都以Android默认提供的debug buildType来初始化。
  • buildConfigField : 该方法的三个参数依次是属性类型,属性名称,属性值。它表示将在当前buildType所对应的BuildConfig类中添加一个指定类型的成员变量属性。比如上面四种buildType中分别用buildConfigField表明:
    1. 在build/generated/source/buildConfig/debug/com/catsuo/gradledemo/BuildConfig.java中生成一个String类型的成员变量URL,它的值是”http://debug.xxx“。
    2. 在build/generated/source/buildConfig/innerDebug/com/catsuo/gradledemo/BuildConfig.java中生成一个String类型的成员变量URL,它的值是“http://innerDebug.xxx”。
    3. 在build/generated/source/buildConfig/outerDebug/com/catsuo/gradledemo/BuildConfig.java中生成一个String类型的成员变量URL,它的值是“http://outerDebug.xxx”。
    4. 在build/generated/source/buildConfig/release/com/catsuo/gradledemo/BuildConfig.java中生成一个String类型的成员变量URL,它的值是“http://releaseDebug.xxx”。

BuildConfig.java是一个编译后生成的类。几个BuildConfig.java虽然位于不同的路径,但是包名都是com.catsuo.gradledemo,并且其中定义的成员变量都是public/static的修饰变量,所以可以直接在代码中使用该类中定义的成员变量。在打包时会根据打包的buildType动态的选择使用哪个BuildConfig.java。但这对开发者来说是透明的,开发者可以认为只有一个BuildConfig.java存在。但是有一点需要注意,如果是自定义的成员变量(比如这里的URL),你必须保证在你所有的buildType中都使用buildConfigField定义了该成员变量,如果有一个buildType中没有定义,那么在代码中引用BuildConfig的该成员变量时是找不到的。

完成上面的配置并编译完成后,就可以在代码中直接使用BuildConfig.URL了:
Utils.java

package com.catsuo.gradledemo.utils;

import com.catsuo.gradledemo.BuildConfig;

public class Utils {


public static void upload() {
String url = BuildConfig.URL;

// upload something to url.

}
}

直接引用BuildConfig.URL指定上传数据的url。在开发时不需要关注BuildConfig.URL的值到底是什么,因为在构建的时候Gradle会为不同的buildType提供准确的BuildConfig.URL值提供保证。
这时候执行assemble task可以看到Gradle根据不同的buildType为我们构建出了不同的apk:

Alt text
Gradle在构建每个apk时会使用与对应buildType对应的BuildConfig.java。

小结

通过自定义buildType并使用其buildConfigField方法完成了针对不同的buildType定义一个名字相同但属性值不同的BuildConfig类的成员变量,它能够使各个buildType间需要通用但又存在差异的属性值对开发者透明化,只要在构建脚本中告诉Gradle每个buildType对应的该属性值是什么,Gradle会聪明的帮助你决定什么时候使用哪一个属性。开发者不需要在业务代码中堆一坨if-else来判断。

需求二

直接上又长大了一些的build.gradle:

apply plugin: 'com.android.application'

android {
compileSdkVersion 25
buildToolsVersion "25.0.3"
defaultConfig {
applicationId "com.catsuo.gradledemo"
minSdkVersion 15
targetSdkVersion 25
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}

buildTypes {
debug {
buildConfigField "String", "URL", "\"http://debug.xxx\""
}
innerDebug {
initWith debug
buildConfigField "String", "URL", "\"http://innerdebug.xxx\""
}
outerDebug {
initWith debug
buildConfigField "String", "URL", "\"http://outerDebug.xxx\""
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
buildConfigField "String", "URL", "\"http://release.xxx\""
}
}

// 配置不同的产品渠道
productFlavors {
free {
applicationId 'com.catsuo.gradledemo.free'
manifestPlaceholders=[app_label:"@string/app_name_free", app_icon:"@mipmap/ic_launcher_free"]
}

pro {
applicationId 'com.catsuo.gradledemo.pro'
manifestPlaceholders=[app_label:"@string/app_name_pro", app_icon:"@mipmap/ic_launcher_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:25.3.1'
compile 'com.android.support.constraint:constraint-layout:1.0.2'
testCompile 'junit:junit:4.12'
}

在需求一的基础上,build.gradle增加了productFlavors block的配置,它是多渠道构建的关键。它内部可以定义多个不同的渠道名称,在每一个渠道名称内又可以初始化该渠道包的多个属性。
先来分解一下需求二:

  1. 首先我们需要构建出两个app,一个用于专业版,一个用于免费版。并且两个app能够同时在应用市场上架。
    结合前面介绍的知识,要保证两个应用能同时在应用市场上架,两个app的applicationId必须不同。所以在build.gradle中通过productFlavors block定义了两个渠道free和pro,并且在每个渠道block内部用新的applicationId覆盖了defaultConfig block中默认的applicationId,将free渠道的applicationId指定为com.catsuo.gradledemo.free,pro渠道的applicationId指定为com.catsuo.gradledemo.pro从而使得构建出来的两个渠道的apk的applicationId是不同的,保证他们可以同时在应用市场上架。
  2. 站在用户的角度,两个app的icon和name应该是有所区别的,不然很容易给用户造成困扰。
    利用Gradle也能优雅的帮我们完成这部分工作。这里用到了productFlavor的manifestPlaceholders属性,它是一个Map容器,可以通过key-value的形式包含多个占位符和对应的值对。首先需要对AndroidManifest.xml做一些改造:
    AndroidManifest.xml


    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.catsuo.gradledemo">

    <application
    android:allowBackup="true"
    <!-- ${name}表明此处是一个占位符,真正的值需要在构建时传入,name表示占位符的名字 -->

    android:icon="${app_icon}"
    android:label="${app_label}"

    android:theme="@style/AppTheme">
    <activity android:name=".MainActivity">
    <intent-filter>
    <action android:name="android.intent.action.MAIN" />
    <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
    </activity>
    </application>
    </manifest>

    在AndroidManifest.xml中对应用的icon和label做了占位符处理,表示真正使用的值由构建时决定,此处只是一个占位符。在构建时给占位符赋值就是productFlavor的manifestPlaceholders属性完成的事。manifestPlaceholders是一个map所以给它赋值时每个key-value对之间用逗号隔开,key和value之间用冒号隔开即可。比如在free block中指定的manifestPlaceholders=[app_label:”@string/app_name_free”, app_icon:”@mipmap/ic_launcher_free”]表明构建时将用@string/app_name_free替换app_label占位符,用@mipmap/ic_launcher_free替换app_icon占位符。pro block中同理,所以我们只要在AndroidManifest.xml中配置占位符,在具体的渠道block内部就可以灵活的替换这些占位符达来满足我们的需求。最终效果就是通过占位符的灵活配置,我们可以使用Gradle构建出基于一套代码的多个“应用”。

这时候执行assemble task可以看到Gradle根据buildType和productFlavor两个维度为我们构建出了不同的apk:

Alt text
与之前相比,除了buildType这个维度,还加上了productFlavor的维度,我们有pro和free两种productFlavor。debug,innerDebug,outerDebug,release四种buildType。所以最终组合后Gradle会为我们构建8个的apk。并且每个apk都按照我们在编译脚本中的配置做到了不同的定制需求,最厉害的是他们基于一套代码。

小结

使用Project的productFlavors方法可以完成对应用的多渠道构建,在每个渠道block内部通过指定相应的属性可以针对每一个渠道完成定制需求,甚至可以通过修改applicationId达到构建多个应用包名不同的apk。结合占位符的使用,使得构建流程的配置灵活性大大提升。

更多


build.gradle中还有很多可供我们配置的选项,本文只是抛砖引玉。如果通过本文引起了你对Gradle构建的兴趣,那么强烈建议多参考Android Gralde PluginGradle API。你会发现利用提供的API我们可以做很多事情,比如:

  • hook构建过程的关键点,在合适的时机可以完成我们想做的事情。
  • 使用Android Gralde Plugin提供的方法遍历我们的所有产品类型,对构建的结果进行二次处理。

在使用Gradle构建过程中遇到的问题,参阅官方的API文档都能够给你一个满意的答案。

谢谢~

参考


深入理解Android-Gradle详解
精通Groovy
Android Gralde Plugin
Gradle API