Gradle 是一款構建系統工具,它的 DSL 基于 Groovy 實現。Gradle 構建的大部分功能都是通過插件方式來實現,所以非常靈活,還可以自定義自己的插件。
Gradle 命令
- gradle wrapper 生成 Wrapper
- gradle -q 打印日志,-s 關鍵性堆棧信息,-S 全部堆棧信息
- gradle tasks 查看有哪些 Task 可以執行
- gradle help --task tasks 查看指定 Task 的幫助
- gradle --refresh-dependencies <task> 執行特定 Task 前強制刷新依賴
- gradle clean jar 多任務調用,只需按順序以空格分開即可,多個任務可以繼續添加
- gradle aR 可以縮寫調用基于駝峰命名的 Task,aR 表示 assembleRelease 可以調用的前提是沒有兩個及以上縮寫沖突的 Task
Groovy 語言基礎
通過 def 關鍵字定義變量和方法
1. 字符串的表示
單引號和雙引號都可以定義一個字符串,區別是單引號中的所有表達式都是常量字符串,而雙引號可以對字符串里的表達式做運算。一個美元符號加一對花括號,花括號里放表達式,例如:{a+b},只有一個變量時可以省略花括號,例如:name
2. 集合
// List 的定義:
def numList = [1,2,3,4,5,6]
// 訪問:
numList[1] // 第二個元素
numList[-1] // 倒數第一個元素
numList[-2] // 倒數第二個元素
numList[1..3] // 訪問第二個到第四個元素
// each 方法用來完成遍歷操作
task pritList << {
def numList = [1,2,3,4,5,6]
numList.each {
// it 表示遍歷到的元素
pritln it
}
}
// Map 的定義
def map = ['width':1024 , 'height':768]
// Map 的訪問
map['width']
map.height
// Map 的遍歷,遍歷到的是一個 Map.Entry 元素
task printMap << {
def map = ['width':1024 , 'height':768]
map.each {
println "Key:${it.key},Value:${it.value}"
}
}
3. 方法
方法的調用可以省略(), method(1,2) 可以寫成:method 1,2
有返回值的時候 return 語句不是必須的,沒有 return 的時候,Groovy 會把最后一句代碼作為其返回值
Groovy 支持閉包,如果最后一個參數時閉包,可以將閉包的花括號寫到 () 外面,并且如果只有閉包一個參數,() 可以不寫
list.each({println it})
// 簡化
list.each {
println it
}
4. JavaBean
Groovy 中并不一定要定義 getter/setter 方法才能訪問成員變量,并且定義了 getter/setter 方法后我們也可以訪問沒有聲明的成員變量
5. 向閉包傳遞參數
如果只有一個參數,直接放入調用閉包時的括號里面,默認使用時就是 it,,如果是多個參數,閉包中就必須要顯示聲明出來
// 定義一個多個參數的閉包,并傳入 eachMap 方法,閉包需要兩個參數,通過 k 和 v 區分
eachMap{k,v -> println "$k is $v"}
6. 閉包委托
Groovy 閉包支持閉包方法的委托,Groovy 的閉包有 thisObject owner delegate 三個屬性,當在閉包中調用方法時,由他們來確定哪個對象來處理,默認情況下 delegate 和 owner 是相等的,但是 delegate 是可以被修改的,Gradle 中的閉包的很多功能都是通過修改 delegate 實現的
thisObject 的優先級最高,默認情況下使用 thisObject 來處理閉包中調用的方法,如果有則執行。thisObject 其實就是這個構建腳本的上下文,和腳本中的 this 是相當的
owner 的優先級比 delegate 高,所以閉包內方法的調用順序是:thisObject > owner > delegate
在 DSL 中,比如 Gradle,我們一般會指定 delegate 為當前的 it,這樣就可以在閉包中對該 it 進行配置,或者調用其方法
task configClosure {
// 設置委托模式優先后,就可以在閉包中直接多該 Person 實例配置以及進行方法調用
person {
personName = "Jerry"
personAge = 20
dumpPerson()
}
}
class Person {
String personName
int personAge
def dumpPerson() {
println "name:$personName age:$personAge"
}
}
def person(Closure<Person> closure) {
Person p = new Person();
closure.delegate = p
// 委托模式優先
closure.setResolveStrategy(Closure.DELEGATE_FIRST);
closure(p)
}
Gradle 構建腳本基礎
1. Setting 文件
setting.gradle 用于初始化及工程樹的配置,放在根目錄下,該文件的大多數作用都是為了配置子工程,在 Gradle 中多工程是通過工程樹表示的,類比 AndroidStudio 中的 Project 和 Module,根工程相當于 Project,子工程相當于 Module,一個根工程可以有多個子工程
一個子工程只有在 Setting 文件中配置了 Gradle 才會識別,才會在構建時被包含進去。配置一個子工程時可以指定相應的目錄,如果不指定,默認目錄是 Setting 文件其同級的目錄
配置子工程時,子工程的名字可以不與目錄相同,只要其他工程添加依賴時使用配置子工程時的名字即可
// 配置一個子工程
include ':example02'
// 指定子工程的目錄
project(':example02').projectDir = new File(rootDir,'chapter01/example02')
2. Project 和 Task
Project 就是一個個獨立的模塊,多個 Project 組成了整個 Gradle 的構建,而 Project 是由一個個 Task 構成的,Task 就是一個操作,一個原子性操作,比如打一個 jar 包,復制一個文件等。
定義一個 Task
// 調用 Project 的 task 方法,接受一個 name(任務名稱) 為參數,返回一個 Task 對象
def Task exTask1 = task(exTask1)
// 以一個任務名字 + 一個對該任務配置的 Map 對象來創建
def Task exTask2 = task(exTask2, group: BasePlugin,BUILD_GROUP)
// 以一個任務名字加閉包的形式,閉包中也可以添加任務配置
task customTask1 {
doFirst {
println "first"
}
doLast {
println "last"
}
}
這里的 task 看著像一個 關鍵字,其實他是 Project 對象的一個函數,原型為 create(String name,Closure configureClosure) ,customeTask1 為任務的名字,第二個參數是一個閉包,將閉包提到括號外表再省略 () 就成了上面簡潔的寫法。其中 doFirst 和 doLast 是任務的兩個方法,分別在任務執行前后會調用,此外,Task 還有其他方法和屬性
還可以通過 TaskContainer 創建任務,其實以上提到的 Task 創建方式最終都是通過這種方式創建的,在 Gradle 里,Project 對象已經定義好了一個 TaskContainer,就是 tasks ,以上幾種創建方式的作用是一樣的
// TaskContainer 方式創建 Task
tasks.create("customTask2") {
doFirst{println "first"}
doLast{println "last"}
}
3. 任務依賴
創建任務的時候,通過 dependsOn 指定其依賴的任務,
task exHello {
println 'hello'
}
// 依賴一個任務
task exMain (dependsOn: exHello) {
doLast { println 'world'}
}
// 依賴多個任務,dependsOn 是 Task 類的一個方法,可以接收多個依賴的任務作為參數
task exMain2 {
dependsOn exHello exMain
doLast {println 'end'}
}
4. 任務間通過 API 控制、交互
創建一個任務類似定義一個變量,變量名就是任務名,類型是 Task,使用 Task 的 API 可以訪問他的方法和屬性或者對任務重新配置,和變量一樣,要使用任務名操作任務,必須先定義聲明,因為腳本是順序執行的
Project 在創建任務的時候,會同時把該任務對應的任務名注冊為 Project 的一個屬性,類型是 Task
task exHello {
println 'hello'
}
exHello.doFirst {
println 'fist'
}
5. 自定義屬性
Project 和 Task 都允許用戶添加額外的自定義屬性,通過應用所屬對應的 ext 屬性來添加額外的屬性。添加之后可以通過 ext 屬性對自定義屬性讀取和設置,如果要同時添加多個自定義屬性,可以通過 ext 代碼塊。
相比局部變量,自定義的屬性有更廣泛的作用域,可以跨 Project,跨 Task 訪問這些自定義屬性,前提時能訪問這些屬性所屬的對象。項目組一般使用它來自定義版本號和版本名稱等
// 自定義一個 Project 的屬性
ext.age = 18
// 通過代碼庫同時自定義多個屬性
ext {
phone = '18000000000'
name = 'Hen'
}
Gradle 任務 - Task
1. 多種任務訪問方式
首先,我們創建的任務都會作為 Project 的一個屬性,屬性名就是任務名,我們可以通過該任務名稱訪問和操作該任務
其次任務都是通過 TaskContainer 創建的,其實 TaskContainer 就是我們創建任務的集合,在 Project 中可以通過 tasks 屬性訪問 TaskContainer ,所以就可以以訪問集合元素的方式訪問我們創建的任務,訪問的時候,任務名就是 [] 操作符的參數,[] 在 Groovy 中是一個操作符
task exAccessTask
tasks['exAccessTask'].doLast {...}
然后還有通過路徑訪問和通過名稱訪問,這兩個都有 find 和 get 兩種方式,區別是 get 的時候如果找不到該任務就會拋出 UnkownTaskException 而 find 在找不到任務時會返回 null
task exAccessTask1
tasks['exAccessTask1'].doLast {
// 通過路徑訪問
tasks.findByPath(':exacple:exAccessTask')
tasks.getByPath(':exacple:exAccessTask1')
// 通過名稱訪問
tasks.findByName('exAccessTask')
tasks.getByName('exAccessTask1')
}
2. 任務分組和描述
任務的分組其實就是對任務的分類,便于對任務進行歸類整理。任務的描述就是說明這個任務的作用。
def Task myTask = task example
example.group = BasePlugin.BUILD_GROUP
example.description = '這是一個構建的引導任務'
3. << 操作符
<< 操作符在 Gradle 的 Task 上是 doLast 方法的對標記形式
// 定義一個 exDoLast 的 task 并調用 doLast 方法
task(exDoLast) >> {
...
}
4. 任務的執行分析
當我們執行一個 Task 的時候,其實就是遍歷執行其擁有的 actions 列表,這個列表保存在 Task 對象實例的 actions 成員變量中,其類型是一個 List。自定義的 Task 類型中可以聲明一個被 TaskAction 注解標注的方法,意思是該 Task 本身執行要執行的方法。當我們使用 Task 方法創建任務的時候,Gradle 會解析其中被 TaskAction 注解的方法作為其 Task 執行的 Action,然后把該 Action 添加到 actions List 里,doFirst 和 doList 會將對應的 action添加到第一位和最后一位,最后這個 action List 的執行順序就確定了。
采用非依賴的形式控制任務的執行順序,可以通過 shouldRunAfter 和 mustRunAfter 兩個方法來實現,這個限制在腳本中添加,通過 gradle 命令執行 task 時起作用
taskB.shouldRunAfter(taskA) 表示 taskB 應該在 taskA 執行之后執行,這里是應該而不是必須,所以又可能執行順序不會按預設的執行
taskB.mustRunAfter(taskA) 表示 taskB 必須在 taskA 執行之后執行
Task 中有個 enable 屬性,用于啟動和禁用任務,默認時 true,表示啟用,設置為 false 則禁止該任務執行,輸出會提示該任務被跳過,調用 taskName.enable = false 即可
5. 任務的 onlyIf 斷言
斷言就是一個條件表達式。Task 有一個 onlyIf 方法,他接受一個閉包作為參數,如果該閉包返回 true 則該任務執行,否則跳過,可以用來判斷控制打包等任務。
命令行中 -P 的意思時為 Project 指定 K-V 格式的屬性鍵值對,使用格式為 -PK-V
task example {
println "執行"
}
example.onlyIf {
def execute = false
if(project.hasProperty("build_apps"){
Object buildApps = project.property("build_apps")
if("all".equals(buildApps) {
execute = true
}
}
execute
}
// 命令
// 會執行
gradle -Pbuild_apps=all example
// 不會執行
gradle example
6. 任務規則
TaskContainer 繼承了 NamedDomainObjectCopllection, 是一個具有唯一不變名字的域對象的集合,他里面所有的元素都有一個唯一不變的名字,改名字時 String 類型,我們可以通過名字獲取該元素,NamedDomainObjectCopllection 的規則就是當想獲取的名字的元素不存在時,對應在 TaskContainer 中就是想獲取的任務不存在時,調用我們添加的規則來處理這種異常情況。
通過調用 addRule 來添加我們自定義的規則,addRule 有兩個重載方法,第一個是直接添加一個 Rule,第二個是通過閉包配置成一個 Rule 在添加
// 通過閉包的形式,對 tasks 這個 Task 添加任務規則
tasks.addRule("對該規則的一個描述,這里是處理任務找不到時的處理") { String taskName ->
task(taskName) << {
println("該 $taskName 任務不存在")
}
}
Gradle 插件
Gradle 本身提供一些基本的概念和整體核心的框架,其他用于描述真實使用場景邏輯的都以插件擴展的方式實現
把插件應用到項目中,插件會擴展項目的功能,幫助我們在項目構建的過程中做很多事情,我們只需要按照它約定的方式,使用它提供的任務、方法或者擴展買就可以對我們的項目進行構建。
- 添加任務,這些任務能幫我們做一下比如測試,編譯,打包等的功能
- 可以添加依賴配置到我們的項目中,通過插件配置我們項目在構建過程中需要的依賴,比如一些第三方庫等依賴。
- 可以向項目中現有的對象類型添加新的擴展屬性/方法等,我們可以通過她們幫助我們配置優化構建,比如 android{} 這個配置可就是 AndroidGradle 插件為 Project 對象添加的一個擴展
- 可以對項目進行一些約定,比如應用 Java 插件后,約定 src/main/java 目錄是我們的源代碼存放位置,等等
插件的應用
- 應用二進制插件,二進制插件就是實現了 org.gradle.api.Plugin 接口的插件,可以有 plugin id
// 下面三種應用方式效果相同
apply plugin:'java' // 通過唯一的 plugin id
apply plugin:org.gradle.api.plugins.JavaPlugin // 通過插件類型
apply plugin:JavaPlugin // org.gradle.api.plugins 是默認導入的
- 應用腳本插件,apply from:'version.gradle' ,應用腳本插件就是把這個腳本加載進來,使用 from 關鍵字,后面可以時本地的腳本文件也可以時一個網絡文件,如果是網絡文件的話要使用 HTTP URL
apply 的使用方式有三種,我們上面使用到的時第一種,接受一個 Map 類型參數的方式
常用的方式還有接受閉包的方式
apply {
plugin:'java'
}
還用一種 Action 的方式,這個知道即可,需要時再查文檔
- 應用第三方發布的插件,使用第三方發布的插件我們必須要在 buildscript{} 里配置其 classpath 才能使用,buildscript 是 Project 的一個方法,參數類型是一個閉包
// 配置 AndroidGradle 插件
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build.gradle:1.5.0'
}
}
buildscript{} 塊是在構建項目之前,為了項目進行前期準備和初始化相關配置依賴的地方,配置好 buildscript 后就可以應用插件了,如果沒有進行 buildscript 配置,則會提示找不到這個插件
buildscript{} 通過在 repositories 中配置倉庫,通過 dependencies 中配置插件的 classpath
apply plugin: 'com.android.applicaition'
- 使用 plugin DSL 應用插件,更簡潔一下,并且使用這種配置方式,如果插件已經托管到了 https://plugins.gradle.org/ 網站上我們就不需要在 buildscript 中配置 classpath
plugins {
id 'java'
}
自定義插件
自定義插件必須實現 Plugin 接口,這個接口有一個 apply() 方法,該方法在插件被應用的時候執行,所以我們可以實現這個方法,做一些想做的時,比如為 Project 創建新的 Task 等
Java 插件
源碼合集
SoureSet - 源代碼集合 - 源集,是 Java 插件用來描述和管理源代碼及其資源的一個抽象概念,是一個 Java 源代碼文件和資源文件的集合。通過源集,我們可以方便的訪問源代碼目錄,設置源集的屬性,更改源集的 Java 目錄或者自由目錄等。
main 和 test 是 Java 插件為我們內置的兩個源代碼集合,我們也可以通過 SourceSets 添加自己的源集,也可以通過 SourceSets 更改 main 和 test 這兩個內置源集的代碼路徑
源集有很多的屬性,通過這些屬性我們可以對源集進行配置,這些屬性包括 name,java,java.srcDirs,resources.srcDirs 等
新創建的源集必須配合多渠道使用,創建的源集的名字與匹配的渠道的名字應該是一樣的,這樣在編譯打包時就會自動匹配,否則創建的源集是無法使用的
// 創建新的源集
sourceSets {
vip{
java {
srcDir 'src/java'
}
resources {
srcDir 'src/resources'
}
}
}
// 新的渠道
productFlavors {
// vip 渠道
vip{}
}
配置第三方的依賴
要使用第三方依賴,必須告訴 gradle 如何找到這些依賴,就需要在 Gradle 中配置倉庫的依賴,這樣 Gradle 就知道在哪兒搜尋我們依賴,Gradle 中通過 repositories{} 塊來配置倉庫
repositories {
maveCentral()
}
有了倉庫配置以后,就可以在 dependencies{} 塊中添加依賴,指定 group name version
dependencies {
// 標準寫法
compile group: 'com.squareup.okhttp3', name: 'okhttp', version: '3.0.1'
// 簡寫方式,group,name,version 中間通過冒號分割
compile 'com.squareup.okhttp3:okhttp:3.0.1'
// 項目依賴,依賴后這個項目的 Java 類就會為你所用
compile project(':example')
// 文件依賴,例如 Jar 包依賴,如下所示,依賴了兩個 Jar 包
compile files('libs/example01.jar', 'libs/example02.jar')
// Jar 包統一依賴,指定文件夾下的指定擴展名文件都會依賴
compile fileTree(dir: 'libs',include: '*.jar')
}
依賴的方式
名稱 | 意義 |
---|---|
compile | 編譯時依賴 |
runtime | 運行時依賴 |
testCompile | 編譯測試用例時依賴 |
testRuntime | 僅僅在測試用例運行時依賴 |
archives | 該項目發布構件(JAR 包等)依賴 |
default | 默認依賴配置 |
Gradle 3.0 添加的新的依賴方式,添加了隔代隔離的概念,即 A 依賴 B,B implementation lib,此時編譯期 A 是不能直接訪問 lib 中的類的,運行期可以。隔離依賴只對 Java 代碼生效,對 resource 無效
名稱 | 意義 | 隔代隔離效果 |
---|---|---|
implementation | 編譯期隔代依賴不可見,運行期間可見 | “隔代”編譯期間隔離 |
api | 與 compile 一樣,編譯期/運行期 都可見 | 無 |
compileOnly | 依賴項僅編譯器對模塊可見,并且編譯和運行期對模塊的消費者不可用 | 無 |
runtimeOnly | 依賴項僅在運行時對模塊及其消費者可用 | 編譯期間隔離 |
為不同源集指定不同的依賴 sourceSet + 依賴指令
例如:vipCompile 'com.squareup.okhttp:okhttp:2.5.0'
多項目構建
Gradle 提供了基于根項目對其所有子項目通用配置的方法,Gradle 的根項目可以理解為是一個所有子項目的容器,subprojects 方法可以對其所有子項目進行配置,如果相對包括根項目在內的所有項目進行統一配置,可以使用 apllprojects 用法同 subprojects
subprojects 和 allprojects 都是 Project 中的方法,接受一個閉包,其中通過 Project 中的方法 repositories 和 dependencies 配置依賴倉庫的地址和依賴的項目。
這里需要注意 buildscript 中和 Project 中的 repositories 和 dependencies 方法是不同的
subprojects {
// 為所有子項目添加代碼倉庫
repositories {
mavenCentral()
}
// 為所有子項目添加插件的 classpath
dependencies {
classpath 'com.android.tools.build:gradle:3.0.1'
}
// 為所有子項目添加 java 插件
applu plugin:'java'
}
發布構件
我們的庫項目開發完畢后就可以配置構件然后上傳構件到本地目錄/maven 庫等里面,上傳就是發布。
// 配置
task publishJar(type: Jar)
version '1.0.0'
// artifacts 通過一個 Task 生成 jar 包
artifacts {
archives publishJar
}
// uploadArchives 是一個 upload task 用于上傳我們發布的構件到本地/maven庫等地方
uploadArchieves {
repositores {
...
}
}
生產 idea 和 Eclipse 配置
在一個已經初始化 wrapper 的項目中,通過 apply plugin:'idea' 就可以生產 IDEA 的工程配置文件,可以直接導入到 IDEA 中使用
Android Gradle 插件
Android Gradle 插件屬于第三方插件,所以需要在 buildscript 中配置 classpath,這部分配置可以寫到根工程的 build.gradle 文件中,這樣所有的子工程就都可以使用了
AndroidGradle 插件繼承 Java 插件,具有所有 Java 插件的特性
Android 插件默認提供了 androidTest,main,test 三種源集和三個渠道
分類
- App 插件,id: com.android.application
- Library 插件,id: com.android.libraay
- Test 插件,id: com.android.test
android{} 塊是 Android 插件提供的一個擴展方法,參數類型是一個閉包,在 Android 插件的構建任務執行時會調用這個方法,android{} 塊中可以讓我們自定義自己的 Android 工程。
android{} 擴展中的配置
- compileSdkVersion 編譯 Android 工程的 SDK 版本,原型是 android{} 中提功能的一個擴展方法
- buildSdkVersion 使用的 Android 構建工具的版本
- defaultConfig 是默認的配置,是一個 ProductFlavor,ProductPlavor 允許我們根據不同的情況同時生成多個不同的 APK,不過不針對自定義的 ProductPlavor 單獨配置,會為這個 ProductPlavor 使用默認的 defaultConfig 的配置,有關 ProductPlavor 中的配置下面再說
- buildTypes 是一個 NamedDomainObjectContainer,是一個域對象,和 sourceSet 類似,buildType 里有 release 和 debug 等,我們可以新增任意多個需要構件的類型,Gradle 會幫我們自動創建一個 BuildType,名字就是我們定義的名字,BuildType 中的屬性也可以設置,具體的配置下面再說
- productFlavors 是 AndroidGradle 提供的一個方法,用來添加不同的渠道,接受域對象類型的 ProductFlavor 閉包作為參數,可以通過 productFlavors{} 閉包添加渠道,每一個都是一個 ProductFlavor 類型的渠道,名字就是渠道名。
defaultConfig 屬性
- applicationId 指定生成的 App 的包名,默認為 null(構件時會在 Application 中的 package 獲取)
- minSdkVersion ,可以接受 int 和 String 兩種,會統一處理
- targetSdkVersion
- versionCode
- versionName
- testApplicationId 配置測試 App 包名,有默認值,applicationId + ".test"
- signingConfig 配置默認簽名信息,是 ProductPlavor 中的一個 SigningConfig 屬性,下面會詳細介紹
- proguardFile 混淆使用的 ProGuard 配置文件
- proguardFiles 可以同事接受多個 ProGuard 配置文件
- ...
配置簽名信息
Android Gradle 提供了 signingConfigsf 配置塊便于我們生成多個簽名配置信息,signingConfigs 是 Android 的一個方法,它接受一個域對象作為其參數。前面我們講過,其類型是 NamedDomainObjcctContainer,這樣我們在 signingConfigs{} 塊中定義的都是一個 SigningConfig ,一個 SigningConfig 就是一個簽名配置,其可配置的元素如下:
storeFile 簽名證書文件
storePassword 簽名證書文件的密碼
storeType 簽名證書的類型
keyAlias 簽名證書中密鑰別名
keyPassword 簽名證書中該密鑰的密碼
signingConfigs {
debug {
storeFile file('./keystore/debug.keystore')
storePassword "debug"
keyAlias "debug"
keyPassword "debug"
}
release {
storeFile file('./keystore/release.keystore')
storePassword "release"
keyAlias "release"
keyPassword "release"
v2SigningEnabled false
}
}
// 使用一
defaultConfig {
// 在 signingConfigs 中選擇需要的簽名信息
signingConfig signingConfigs.release
}
// 使用二
buildTypes {
release {
// 對構建類型設置簽名信息
signingConfig signingConfigs.release
}
}
隱藏簽名文件
為了防止簽名文件的泄漏,可以在服務器端存儲簽名文件及其密鑰,并在 signingConfigs 中通過在服務端存儲的位置獲取,如果本地需要調試,就在獲取不到服務端的密鑰時使用本地 debug 的密鑰,保證了簽名文件的安全性
構建的應用類型 BuildType
Android Gradle 幫我們內置了 relase 和 debug 兩個構建類型,差別在于能否在設備上調試和簽名不同,其代碼和資源是一樣的
buildTypes {
release {
}
debug {
}
}
如果需要新增構建類型在 buildTypes{} 中繼續添加元素即可,buildTypes 也是 android 中的一個方法,接受一個域對象,添加的每一個都是 BuildType 類型
構建類型中的配置
- applicationSuffix 用于配置默認 applicationId 的后綴
- debugable 用于配置是否生成一個可供調試的 Apk ,值可以是 true 或者 false
- jniDebugable 用于配置是否生成一個可供 Jni 調試的 Apk ,值可以是 true 或者 false
- minifyEnable 是否啟用 Proguard 混淆
- miltiDexEnable 是否啟用自動拆分多個 Dex
- proguardFile 配置混淆文件
- profuardFiles 配置多個混淆文件
- shrinkResources 是否自動清理未使用的資源文件,默認 false
- signingConfig 簽名配置
- zipalignEnable 是否啟用 zipaline 優化,是一個優化整理 apk 的工具,可以提高系統和應用的運行效率,更快讀寫 apk 中的資源,降低內存使用等,默認開啟
每一個 BuildType 都會生成一個 SourceSet ,默認位置 src// 這樣我們就可以針對不同 BuildType 單獨指定期 Java 源代碼,res 資源等,構建時,AndroidGradle 會使用它們代替 main 中的相關文件
每一個 BuildType 都會生成一個 assemble 任務,執行相應的 assemble 任務就可以生成對應 BuildType 的 APK
批量修改生成的 apk 名字
applicationVariants.all { variant ->
variant.outputs.each { output ->
def outputFile = output.outputFile
def outputName = new String(outputFile.name);
if (outputFile != null && outputName.endsWith('.apk')) {
def fileName = outputFile.name.replace('.apk', "-${versionName}_.apk");
output.outputFile = new File(outputFile.parent, fileName)
}
}
}
動態配置 AndroidManifest 文件
在構建的過程中動態修改 AndroidManifest 中的內容,Android Gradle 提供了 manifestPlaceholder、Manifest 占位符來替換 AndroidManifest 中的內容
ManifestPlaceholder 是一個 Map 類型,所以可以同時定義多個占位符,ManifestPlaceholder 是 BaseConfigImpl 中的一個屬性,ProductFlavor 繼承了 BaseConfigImpl,所以可以在 BaseConfigImpl 中訪問,在 BuildType 中也直接訪問 BaseconfigImpl 中的 ManifestPlaceholder
// BuildType 中定義
buildTypes {
debug {
// 替換原來的 ManifestPlaceholder
manifestPlaceholders = ["UMENG_CHANNEL","google"]
// 添加新的元素
manifestPlaceholders.put("UMENG_CHANNEL","google")
}
}
在 gradle 配置文件中定義了 manifestPlaceholder 后,在構建時,它會把 AndroidManifest 文件中所有的占位符變量為 manifestPlaceholder 中定義的占位符替換為 manifestPlaceholder 中對應的 value 值
// AndroidManifest 文件
<meta-data android:value="${UMENG_CHANNEL}" android:name=""UMENG_CHANNEL" />
如果在每一個 Flavor 中都需要配置相同占位符的不同 value,可以通過 productFlavors.all 方法遍歷所有的 ProductFlavor 完成批量修改,name 為 ProductFlavor 的名字
productFlavors.all { flavor ->
manifestPlaceholders.put("UMENG_CHANNEL",name)
}
BuildConfig 文件
BuildConfig 文件時 Gradle 編譯后生成的,自動生成的包含包名、是否 Debug、構建類型、Flavor、版本號、版本名稱的常量,開發中我們可以很方便的訪問
Android Gradle 提供了 buildConfigField(String type,String name,String value) 方法讓我們添加自己的常量到 BuildConfig 文件中,type 是要生成的字段類型,name 是要生成常量的名字,value 是生成字段的常量值,在 Gradle 腳本中就可以添加
注意 value 中的值,是單引號中的部分,是什么就寫什么,要原封不動放入單引號,如果是 String ,雙引號不能省略
// 比如在不同 productFlavor 中配置同一常量的不同值,這樣在編譯不同的 ProductFlavor 時就會生成不同的值
productFlavors {
google {
buildConfigField 'String','WEB_URL','"http://google.com"'
}
baidu {
buildConfigField 'String','WEB_URL','"http://baidu.com"'
}
}
動態添加自定義的資源
開發中遇到的資源包括圖片、動畫、字符串等,這些資源我們可以在 res 文件中定義,除了這種方式,針對 res/values 類型,里面有 arrays、ids、attrs、colors、dimens、strings 等類型,Gradle 提供了 resValue(String type,String name,String vlue) 方法在 ProductPlavor 和 BuildType 中可以定義這些資源的方法,這樣我們就能根據不同渠道不同構建類型來自定義特有資源,也可以替換 res/valuse 文件夾中已經定義的資源,可以定義 id、bool、dimen、integer、color 等類型
// 以 productFlavors 為例,這樣在構建時不同渠道生成的 App 名字就會不同
productFlavors {
google {
resValue 'String','app_name','GoogleApp'
}
baidu {
resValue 'String','app_name','BaiduApp'
}
}
Java 編譯選項
Android Gralde 提供了一個 compileOptions 方法,接受一個 CompileOptions 類型的閉包為參數,來對構建過程中 Java 編譯選項進行配置
compileOptions {
// 配置源文件編碼為 UTF-8
encoding = 'utf-8'
// 配置 Java 源代碼的編譯版本
sourceCompatibility JavaVersion.VERSION_1_8
// 配置生成的字節碼的版本
targetCompatibility JavaVersion.VERSION_1_8
}
adb 操作選項配置
Gradle 對 Android 手機的操作,最終還是調用了 adb 命令,Android Gralde 只不過是對 adb 命令的調用做了一層封裝,可以通過 android 提供的 adbOptions 方法配置 Gradle 調用 adb 時的一些配置,該方法接受一個 AdbOptions 類型的閉包,AdbOptions 可以對 adb 操作選項添加配置
android {
adbOptions {
// 配置操作超時時間,單位毫秒
timeOutInMs = 5* 1000_0
// adb install 命令的選項配置
installOptions '-r','-s'
}
}
DEX 選項配置
Android 中 Java 代碼被編譯成 class 字節碼,生成 apk 時又通過 dx 指令優化成 Android 虛擬機可執行的 DEX 文件,AndroidGradle 插件會調用 SDK 中的 dx 命令,dx 命令最終調用了 Java 編寫的 dx.jar 庫,是 Java 編寫的,所以當調用 dx.jar 庫是如果內存不足就會出現異常,默認給 dx 分屏的內存是一個 G8,也就是 1024M
android 提供了 dexOptions 方法用來添加對 dx 操作的配置,接受一個 DexOptions 類型的閉包,配置由 DexOptions 提供
android {
dexOptions {
// 配置是否啟用 dx 增量模式,默認 false,增量模式速度塊,但是有很多限制,不建議開啟
increment true
// 配置執行 dx 命令是為其分配的最大堆內存
javaMaxHeapSize '4g'
// 配置是否開啟 jumbo 模式,代碼方法是超過 65535 需要強制開啟才能構建唱歌
jumboMode true
// 配置是否預執行 dex Libraries 工程,開啟后會提高增量構建速度,不過會影響 clean 構建的速度,默認 true
preDexLibraries true
// 配置 Andriod Gradle 運行 dx 命令時使用的線程數量
threadCount 8
}
}
自動清理未使用的資源
Android Gradle 提供了構建時自動清理未使用的資源的方法,會將項目中的和第三方依賴中的都清理
Resource Shrinking -- 資源清理 -- shrinkResource true
Code Shrinking -- ProGuard 混淆代碼清理 monifyEnabled true
shrinkResource 是在 BuildType 中配置的,默認不啟用
Android Gradle 提供了 keep 方法來配置我們不希望被清理的資源
新建一個 xml 文件,res/raw/keep.xml 然后通過 tools:keep 屬性來配置,可以通過都好分割配置資源列表,支持通配符 *
keep 中還有一個 tools:shrinkMode 屬性,用來配置自動清理資源的模式,默認是 safe 是安全的,這種情況下代碼中通過 getResource() 方法引用的資源也會被保留,如果設置成 shrink 模式,這些資源也會被清理
keep 文件在構建時也會自動被清理,如果想保留也要添加到 keep 屬性中
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:shrinkMode="safe"
tools:keep="@layout/getui_notification,@drawable/h_good,@drawable/h_ok"/>
除了 shrinkResource 外,Android Gradle 還提供了 resConfig 配置,屬于 ProductFlavor 的一個方法,用來配置哪些類型的資源會被打包到 apk 中,比如只打包中文資源,只打包 hdpi 中的圖片等,它的參數就是我們 Android 開發時用到的資源限定符,這樣在打包時可以清理大量無用資源
android {
defaultConfig {
// 接受一個
resConfig 'zh'
// 接受多個
resConfigs 'zh','hdpi'
}
}
Android Gradle 多項目構建
Android 的項目有三種,庫項目、應用項目、測試項目,對應三種項目也有三種插件,com.android.library,com.android.applistion,com.android.test,開發特定的項目需要依賴特定的插件
Android 多項目設置
通過根項目中的 setting.gradle 進行配置
庫項目的引用和配置
引用庫項目也是通過在項目的 build.gradle 中的 dependencies 中指定,庫項目引用時,庫項目會生成一個 aar 包,如果時 Java 項目就會生成一個 jar 包提供給其他項目引用
dependencies {
compile project(':lib')
}
庫項目默認發布出來的是一個 aar 包,默認發布的是 release 版本,可以在庫項目中通過 android 提供的 defaultPublishConfig 配置來修改,這個方式可以配置不同的版本,只要名字合法即可,例如 "flavor1Debug" ,這樣就可以針對不同的 Flavor 和 BuildType 來配置
android {
defaultPublishConfig "debug"
}
android {
defaultPublishConfig "flavor1Debug"
}
如果想同時發布多個版本的 aar 供不同項目引用,可以配置同時發布多個 aar 包,首先子項目進行配置,然后在其他項目引用該項目的生活,通過 path 指定子項目,通過 configration 指定 Flavar 和 BuildType,BuildType 又可以指向匹配 SourceSets ,這樣就可以同時發布多個版本的 aar ,且這些 aar 的內容可以不一樣
// 子項目配置
android {
publishNonDefault true
}
// 其他項目依賴子項目時的配置
dependencies {
flavor1Compile project(path: ':lib1', configration: 'flavor1Release')
flavor1Compile project(path: ':lib1', configration: 'flavor2Release')
}
庫項目的單獨發布
對于一些公共組件庫、工具庫等,我們可以將它們單獨發布,供其他項目引用,我們也可以將它們發布到 Maven 或者 jcenter 中
庫項目發布到 Maven 倉庫
首先要搭建 Maven 私服,使用 Nexus Repository Manager 并部署啟動
Maven 庫部署以后,就可以把項目發不到中心庫了,想要通過 Maven 發布,首先要在 build.gradle 中應用 Maven 插件,這個插件提供給 Product 一些擴展
應用 Maven 插件以后,需要配置 Maven 構建的三要素,group:artifact:version
版本號可以指定成快照形式,比如 1.0.0-SNAPSHOT ,這時候會發不到 snapshot 庫里,每次發布版本號不會變化,只會在版本號后按順序 + 1,例:1.0.0-1,1.0.0-2
引用的時候版本號寫成 1.0.0-SNAPSHOT 即可,Maven 會幫我們下載最新也就是序號最大的快照版本,在聯調測試的時候可以使用這種方式,當調試結束,就可以發布正式 release 版本
apply plugin: 'com.android.library'
apply plugin: 'maven'
version '1.0.0'
group 'org.snow.widget'
配置好 version 和 group 后,進行發布配置,比如發不到哪個 Maven 庫,用戶名密碼是什么,發布的格式是什么,它的 artifact 即項目 name 是什么等
uploadArchives {
repositories {
mavenDeployer {
// 快照發布, URL 用戶名 密碼
snapshotRepository(url: mavenServer + mavenSnapshots) {
authentication(userName: repoUsername, password: repoPassword)
}
// 正式發布
repository(url: mavenServer + mavenReleases) {
authentication(userName: repoUsername, password: repoPassword)
}
// 項目 name
pom.artifactId = "pull-view"
// 發布格式
pom.packaging = 'aar'
}
}
}
引用發布的項目庫
引用自己發布的庫項目,首先需要在 Gradle 中添加對應的 classapth,然后在 dependencies 中添加引用
repositories {
maven {
// 項目庫的 url
url ".../groups/relases"
}
}
dependencies {
compile 'org.snow.widget:pull-view:1.0.0'
}
為了使用快照版本,需要單獨配置 classpath,因為快照庫和 relase 庫地址不同,引用也要單獨添加
repositories {
maven {
// 項目庫的 url
url ".../groups/snapshots"
}
}
dependencies {
compile 'org.snow.widget:pull-view:1.0.0-SNAOSHOTS'
}
可以通過 group 類型的配置,同時將 relase 和 snapshots 類型的庫地址添加到 classpath, 引用還是區分 relase 和快照即可
repositories {
maven {
url ".../groups/public"
}
}
Andriod Gradle 多渠道構建
在 Android Gradle 中,定義了一個叫 Build Variant 的概念,即 構建變體,即不同的構件,也就是不同的 apk,一個 Build Variant 由一個 BuildType 和一個 ProductFlavor 構成,比如 release 和 debug,goole 和 baidu ,就可以構成四個變體。
Android Gradle 通過了 productFlavors 擴展來添加不同渠道,它接受域對象類型的 ProductFlavor 閉包作為參數,可以添加很多渠道,每一個 ProductFlavor 都是一個渠道,在 NamedDomainObjectContainer 中的名字就是渠道,比如 google ,baidu 等
渠道配置以后,Android Gradle 就會生成很多 Task ,基本都是基于 BuildType + ProductPlavor 的方法生成的,如 assembleGoogle assembleGoogleRelase assembleGoogleDebug 等,assembleRelease 運行后就會生成所有渠道的 release 包,assembleGoogle 運行后就會生成 google 渠道的 release 和 debug 包。出了 assemble 還有 install 系列的等
每個 ProductPlavor 還可以有自己的 SourceSet 和自己的 dependencies 依賴,這樣,每個渠道都可以定義自己的資源,源代碼以及第三方類庫
多渠道構建定制
通過配置 ProductFlavor 達到靈活控制每一個渠道
- applicationId 配置該渠道包名
- consumerProguardFiles ,對 Android 庫項目生效,配置混淆文件列表,會被打包到 aar,當應用項目開啟混淆時,會使用 aar 包里的混淆文件對 aar 包里的代碼進行混淆,這樣應用項目就不用對該 aar 進行混淆配置了
- manifestPlaceholders 上面已經介紹了
- multiDexEnabled 開啟多個 dex 的配置
- proguardFiles 混淆文件
- signingConfig 簽名文件
- versionCode 版本號
- versionName 版本名
- userJack 是否啟用 Jack 和 Jill 編譯器,Google 開發的編譯器,速度快性能高,但不支持注解、 JDK8 的特性,處于試驗階段
- testApplicationId 單元測試的 apk 的包名
- testFunctionalTest 是否為功能測試,testHandleprofiling 是否啟用分析功能
- testInstrumentationRunner 測試使用...
- testInstrumentationrunnerArguments 測試使用 ...
- dimension ...
dimension
開發中如果遇到這樣的情況,比如:項目區分收費和免費版并且代碼不同,還區分不同的機器架構 x86 和 arm 并且代碼不同,甚至還有其他區分緯度的區分,這時候,我們在配置 ProductFlavor 時,就會出現 收費版的配置在 x86 和 arm 中都各寫一份,如果緯度更多,那么需要寫的地方就更多,修改和增加時就更容易出錯
為了解決這個問題,Android Gradle 為我們提供了 dimension 來解決這個問題
dimension 是 ProductFlavor 的一個屬性,接受一個字符串,為該 ProductFlavor 的緯度,多個 ProductFlavor 可以同屬一個緯度,這個緯度必須先定義出來才能使用,在 android{} 中,productFlavors{} 之前通過 flavorDimensions 聲明緯度,定義后就可以在 ProductFlavor 中使用,在一個 ProductFlavor 中添加了 dimension 屬性后,就相當于把這個 ProductFlovar 添加進了這個緯度的分組中。
只要定義了一個緯度,并且這個緯度分組中有了內容,其他所有非當前緯度的 ProductFlavor 都會與這個緯度中每一個 ProductFlavor 進行組合。就比如 收費和付費是一個緯度 'version',我們在 paid 和 free 兩個 ProductFlavor 指定了 demision 為 version,padi 和 free 中還可增加其他配置。這樣我們再添加其他 ProductFlavor 后,比如添加了 arm ,Android Gradle 會以 BuildType+arm+version 的格式生成一系列 Task,這里就會生成 ArmPaidDebug ArmPaidRelease ArmFreeDebug ArmFreeRelease
這樣,我們只用緯度去分組,去配置,剩下的 AndroidGradle 來組合,實現了共性配置,維護起來也很方便
新版本的 AndroidGradle 插件,強制要求,必須為每一個 ProductFlavor 添加 dimension,否則編譯會報錯
android {
flavorDimensions "abi","version"
productFlavors {
free {
dimension 'version'
}
paid {
dimension 'version'
}
x86 {
dimension 'api'
}
arm {
dimension 'api'
}
}
}
以上配置中,定義了兩個分組,加上默認的兩種 BuildType 后兩兩結合以后就會生成八種組合方式
lint 檢查
Android 為我們提供了針對代碼、資源的優化工具 lint,它可以幫助我們檢查出哪些資源沒有被使用,使用了哪些新的 API,哪些資源沒有被國際化等。并且會生成一份報告,告訴我們哪些需要優化。
Lint 是一個命令行工具,是 Android Tool 目錄下的一個工具,可以在命令行中直接執行。Android Gradle 中也對 Lint 做了很好的支持,提供了 lintOptions{} 閉包來配置 lint,達到做檢查的目的
lintOptions 是 Android 對象的一個方法,接受一個類型為 LintOptions 的閉包,用于配置 Lint,Lint 提供了非常多的配置,以后可以根據項目配置想要的 Lint 檢查規則
執行 gradle lint 命令即可運行 lint 檢查,默認生成的報告在 outputs/lint-results.html 中
android {
lintOptions {
// 遇到 lint 檢查錯誤會終止構建,一般設置為 false
abortOnError true
// 將警告當作錯誤來處理
warningAsErros true
// 檢查新 API
check 'NewApi'
}
}
多渠道構建的效率
通過在 APK 包中的 METE-INF 文件夾下新增文件,文件名為有意義的渠道名字,通過 Python 腳本復制原始 APK 包,解壓后在 METE-INF 中添加文件,然后再打包
在使用時,Android 程序的 APP 中通過對 apk 進行讀取,就可以讀到 METE-INF 中文件的名字,這樣就得到了渠道標示,取到標示后可以存入 SP 中,這樣就不用每次都讀取 APK