回顧
Android應用的構建過程是一個複雜的過程,涉及到很多工具。首先所有的資源文件都會被編譯,並且在一個R文件中引用,然後Java代碼被編譯,通過dex工具轉換成dalvik字節碼。最後這些文件都會被打包成一個APK文件,此應用被最終安裝到設備之前,APK會被一個debug或者release的key文件簽名。
一句話定義Gradle
Gradle是一種構建工具,其構建基於Groovy(DSL) ------ 一種基於JVM的動態語言,用於申明構建和創建任務,讓依賴管理更簡單。
年少時第一次對Gradle總結的微博:Gradle 與 Android的三生三世:是我構建了你,你必將依賴於我
Point
1.閉包和動態配置
【project/ build.gradle文件】
buildscript {
ext.kotlin_version = '1.3.41'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
jcenter()
}
}
【app/ build.gradle文件】
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
- buildscript: 針對於下方dependencies區塊中的依賴路徑,即插件的倉庫配置。
- allprojects: 針對所有project(app、module);
(1)閉包Closure
可以傳遞的代碼區塊。
java不能對方法進行引用
void buildscript(Closure configureClosure);
如上所示,buildscript
內的代碼區塊將傳遞到buildscript
中,稍後執行。
(2)動態配置
gradle中語法配置,是在runtime而不是build時段check,不同於編碼Java,例如在調用某個對象不存在的方法,編譯時就會報錯,gradle中方法是動態配置的。
buildscript {
......
dependencies {
classpath 'com.android.tools.build:gradle:3.5.0'
//add語法,等價於上面的寫法
add('classpath', 'com.android.tools.build:gradle:3.5.0');
......
}
}
來看上述build.gradle
文件中對plugin路徑語法配置 的一個例子,很少人知道路徑的配置還可以用 add(,)
這種語法,就像調用Java對象方法,傳入2個參數,更像是一個萬能鑰匙,第一個參數是配置key,第二個參數是配置value。
沒錯,你的確可以這樣理解,在上述第一點閉包中講到,將區塊傳入void dependencies(Closure configureClosure)
中 稍後執行,再快捷鍵點擊classpath 具體發現是DependencyHandler 接口,此接口具體實現類是DefaultDependencyHandler,類中有個方法叫做MethodMissing(String name, Object args)
,內部遍歷配置清單中是否有name方法,找到則內部繼續調用create(...)
方法。
可見,Gradle是利用Groovy的特性,把基於Java虛擬機的語言改造成最基本的配置語法。因此,這裏建議瞭解gradle配置規則即可,感興趣者再去了解其中實現。
拓展
allprojects {
repositories {
google()
jcenter()
}
}
//上下寫法等價----------------------
allprojects(new Action<Project>() { //很java的感覺
@Override
void execute(Project project) {
repositories {
google()
jcenter()
}
}
})
2.buildType 和 productFlavors
【app/ build.gradle文件】
android {
......
buildTypes {
release {
signingConfig signingConfigs.myConfigs
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
debuggable false
}
debug{
signingConfig signingConfigs.myConfigs
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
minifyEnabled false
debuggable true
}
}
......
}
3.compile、 implementation 和 api
- implementation:不會傳遞依賴;
- compile / api:會傳遞依賴;api 是 compile 的替代品,效果完全等同。
- 當依賴被傳遞時,⼆級依賴的改動會導致 0 級項⽬重新編譯;
- 當依賴不不傳遞時,二級依賴的改動 不會導致 0 級項⽬重新編譯;
減少傳遞依賴帶來的重複編譯應該是implementation 誕生的最大意義了,在往常開發Coder都是一個compile 依賴走天下,單module下的表現不明顯,但目前公司項目大部分採用多module項目,例如
主App -----依賴----> 業務module -----依賴----> 工具module
- 主App:一些基本APP信息配置、簽名、動態化處理;
- 業務module:業務邏輯/UI處理;
- 工具module:網絡請求、自定義控件、工具等;
以上三種是很常見的多module分配,這時使用implementation依賴是可以大大減少重複編譯的,因爲業務module會依賴 工具module,但主App中無需對工具module使用傳遞依賴。因此,修改工具module內容時,不會導致主App重新編譯。
4.task
./gradlew taskName
task的使用在平時開發過程中也是不可或缺的一部分,特別是用於編寫各種插件,例如靜態check、打包等需求支持,下面瞭解一下task重點。
Test1. clean task
首先看個簡單的例子,也是project/build.gradle
文件中一個現成的task ------ clean,我們在此基礎上加幾個Log對比查看下:
println("outside the task: println")
task clean(type: Delete) {
println("inside the task: before task")
delete rootProject.buildDir
println("inside the task: after task")
}
分別在終端terminal輸入:
./gradlew
:打印log(如上截圖),build文件夾沒有刪除;./gradlew clean
:打印log(如上截圖),build文件夾刪除;
爲何2個命令都輸出了Log,但./gradlew
執行後,文件夾並沒有被刪除?
在第一點中我們講解到gradle原理一大特點 ------ 閉包,將代碼塊傳入方法中,內部有自己的處理邏輯。兩個命令,Log都打印了,意味着所有語句都執行過了。可./gradlew
命令,delete語句似乎沒有起作用?突破口就在這裏,點擊delete進去,查看源碼實現:
package org.gradle.api.tasks;
public class Delete extends ConventionTask implements DeleteSpec {
private Set<Object> delete = new LinkedHashSet<Object>();
......
/**
* Sets the files to be deleted by this task.
*
* @param target Any type of object accepted by {@link org.gradle.api.Project#files(Object...)}
*/
public void setDelete(Object target) {
delete.clear();
this.delete.add(target);
}
......
}
看到這裏真的是非常有意思,在第一點也說了gradle把基於Java虛擬機的語言改造成最基本的配置語法,所以其內部原理實現Java Coder可謂是一目瞭然,在編寫task clean(type: Delete)
,可以直接理解爲class clean extends Delete
,這就是個繼承嘛。迴歸到問題本身,可見delete操作內部實則是個添加操作,內部維護着一個Set,在執行 ./gradlew
命令時,只是在配置任務,等到直接執行clean任務時./gradlew clean
時,纔會把Set集中的刪除任務取出,do it。
以上解釋也帶出了task的2個重要階段:
- configuration配置階段
- execution執行階段
Test2. task 配置與執行
在上一個例子的基礎上加深,task代碼塊內部新增一個doLast
閉包,輸入命令對比結果:
println("outside the task: println")
task clean(type: Delete) {
println("inside the task: before task")
delete rootProject.buildDir
println("inside the task: after task")
doLast{
println("inside the task: doLast")
}
}
分別在終端terminal輸入:
./gradlew
:打印log,但是並沒有打印出doLast
閉包內的Log,build文件夾沒有刪除;./gradlew clean
:打印log(如上截圖),build文件夾刪除;
在上一個Test的基礎上,我們得知configuration配置階段 會將所有配置讀取一遍,配備好對應的task,直接執行task時纔會真正do it 。而此次試驗的doLast
閉包正突出 execution執行階段 的特點:doLast
裏的內容在 task 執⾏過程中才會被執行。
./gradlew
命令還是配置階段,因此最後輸出並沒有打印出doLast
閉包中的內容;執行./gradlew clean
直接執行task任務時,纔會去執行doLast
閉包中的內容,打印出 > inside the task: doLast
。至此,相信task的2個階段已經分辨清楚。
Test3. doFirst 和 doLast
在Test2的基礎上繼續加深,既然在上一點中介紹了doLast
,相應地,doFirst
雖遲但到。上一點中我們點明doFirst
執行在execution階段,那麼doFirst
亦然,這2者的區別似乎通過名字也可瞭解一二。
下面通過一個更有趣的例子來了解其區別:
task clean(type: Delete) {
doFirst{
println("inside the task: doFirst")
}
delete rootProject.buildDir
doLast{
println("inside the task: doLast")
}
}
clean.doFirst {
println("outside the task: doFirst")
}
clean.doLast {
println("outside the task: doLast")
}
由於這兩個區塊只在task execution階段 執行,因此此次試驗輸入 ./gradlew clean
即可,查看Log輸出:
輸出結果表明(執行階段):
- 後面的
doFirst
中的Log輸出 先於 前面的輸出; - 後面的
doLast
中的Log輸出 後於 前面的輸出;
首先說明下後續新增的clean.dofirst
這種寫法,簡直就是Java中調用類的方法,其實在【Test1. clean task】中已經提過:
在編寫
task clean(type: Delete)
,可以直接理解爲class clean extends Delete
,這就是個繼承嘛。
因此,後續新增的這種寫法也是沒有問題的,重點還是放到doFirst()
、 doLast()
的調用順序上來,老規矩,查看這2個閉包方法的內部源碼實現:
package org.gradle.api.internal;
public abstract class AbstractTask implements TaskInternal, DynamicObjectAware {
......
private List<InputChangesAwareTaskAction> actions;
@Override
public Task doFirst(final Closure action) {
...
taskMutator.mutate("Task.doFirst(Closure)", new Runnable() {
public void run() {
getTaskActions().add(0, convertClosureToAction(action, "doFirst {} action"));
}
});
return this;
}
@Override
public Task doLast(final Closure action) {
...
taskMutator.mutate("Task.doLast(Closure)", new Runnable() {
public void run() {
getTaskActions().add(convertClosureToAction(action, "doLast {} action"));
}
});
return this;
}
......
}
一目瞭然,一個task中維護了一個Action集合List,而 doFirst
方法每次調用都會向列表頭部插入Action,而 doLast
方法每次調用都會向列表尾部插入Action。因此在此次實驗中,後面的doFirst
中的Log輸出 先於 前面的輸出,後面的doLast
中的Log輸出 後於 前面的輸出。
總結
至此,對於以上三個小實驗,做一個簡單的總結:
一個標準的task結構
task taskName {
初始化代碼
doFirst {
task 代碼
}
doLast {
task 代碼
}
}
doFirst() 、doLast() 和普通代碼段的區別
- **普通代碼段:**在 task 創建過程中就會被執行,發生在 configuration階段;
- **doFirst() 和 doLast():**在 task 執⾏過程中被執行,發生在 execution階段。如果用戶沒有 直接或間接 執行 task,那麼它的
doLast()
、doFirst()
代碼不會被執⾏;doFirst()
和doLast()
都是 task 代碼,其中doFirst()
是往隊列的前⾯插入代碼,doLast()
是往隊列的後面插入代碼。
拓展 ------ task 的依賴
可以使用 task taskA(dependsOn: b)
的形式來指定依賴。指定依賴後,task 會在⾃己執行前先執⾏依賴的 task。
5.gradle 執⾏的⽣命週期
(1)三個階段
-
**[Initialization] 初始化階段:**執行 settings.gradle,確定主 project 和子 project ;
根據項⽬結構來確定項目組成,如下:
-
單 project:確定根目錄下的 build.gradle 文件即可;
-
多 project:由配置了多個module的 settings.gradle 文件開始查找 settings 的順序:
-
當前⽬錄
-
兄弟⽬錄 master
-
父目錄
-
-
-
**[Configuration] 配置階段:**執行每個 project 的 bulid.gradle,確定出所有 task 所組成的 有向⽆環圖;
-
[Execution] 執行階段:按照上⼀階段所確定出的有向無環圖來執⾏指定的 task;
(2)階段之間插入代碼
- ⼀二階段之間:settings.gradle 的最後;
- 二三階段之間:
afterEvaluate {
插⼊入代碼
}
Plugin實踐
Gradle Plugin到底是什麼?
**本質就是將一些獨立邏輯的代碼封裝並抽取出來,加以複用。**但不同於module、library,它所處理的邏輯並非業務性質,而是作爲一個項目組織者,更關心各module的配置信息,因此提供了一系列配置、task執行相關API。
一個Plugin的寫法:
- 直接寫到
/app/build.gradle
配置文件 - 獨立封裝到項目中的
buildSrc
目錄 - 獨立一個項目上傳到倉庫,項目直接引入即可
0. Groovy語法基礎要求
- getter / setter
每個 field,Groovy 會⾃自動創建它的 getter
和 setter
方法,從外部可以直接調用,並且在使用 object.fieldA
來獲取值或者使用 object.fieldA = newValue
來賦值的時候,實際上會自動轉⽽調⽤ object.getFieldA()
和 object.setFieldA(newValue)
。 (跟Kotlin一樣)
- 字符中的單雙引號
單引號是不帶轉義的,⽽雙引號內的內容可以使⽤ "string1${var}string2"
的⽅式來轉義。(跟Vue一樣)
1. 配置信息Extension
這一部分配置相當自由,從 “配置類名” 到 “屬性”都是自主定義,後續從plugin中獲取,就像
/app/build.gradle
中對android編譯版本各種配置,以下舉個例子:
permissionsCheckList {
//明確暫禁用的權限列表
forbiddenPermissions = ['android.permission.GET_ACCOUNTS',
'android.permission.READ_CONTACTS']
}
2. Plugin實現
(1)直接在/app/build.gradle
實現
【注意:此部分需要寫到 apply 引入之前】
要不怎麼說Groovy是基於Java虛擬機而制定的DSL,寫法部分不同,但是直接寫implements實現,“like class”理解。
如下代碼,這裏實現一個只有print功能的PermissionCheck插件,
- 實現Plugin接口,內部實現
void apply(Project target)
方法. - 可以通過參數target的
target.extensions.create
可以獲取到項目配置的Extension信息,根據配置信息實例化創建 XXXExtension類。 - 因此也需要構建相關的XXXExtension類,注意需要定義到Plugin前。
- 通過類的
get/set
方法獲取具體屬性信息,做自定義題配置邏輯處理。- 邏輯處理幾個重點:執行順序。
class PermissionsCheckListExtension {
def forbiddenPermissions = []
}
class PermissionCheck implements Plugin<Project> {
@Override
void apply(Project target) {
println 'PermissionCheck apply'
def extension = target.extensions.create('permissionsCheckList', PermissionsCheckListExtension)
println "PermissionCheck (forbiddenPermissions): ${extension.forbiddenPermissions}"
target.afterEvaluate {
println "PermissionCheck afterEvaluate (forbiddenPermissions): ${extension.forbiddenPermissions}!"
}
}
}
apply plugin: PermissionCheck
permissionsCheckList {
//明確暫禁用的權限列表
forbiddenPermissions = ['android.permission.GET_ACCOUNTS',
'android.permission.READ_CONTACTS']
}
以上,執行gradle配置命令./gradlew
,輸出見下圖:
輸出發現執行到apply plugin: PermissionCheck
時,配置gradle階段,注意此時還沒有讀取到項目中permissionsCheckList的配置信息,因此此時輸出的forbiddenPermissions爲[],也是初始化定義時的值。
而調用 target.afterEvaluate方法內傳入閉包內容稍後執行,Point中grad le生命週期有提到,afterEvaluate執行時期是在「配置階段」之後和「執行階段」之前,也就是說所有配置結束後,最後一個“配置”來執行閉包裏的內容,所以此時自定義配置已經可以獲取到了 。而後輸出的forbiddenPermissions列表也就是我們後續配置的GET_ACCOUNTS、READ_CONTACTS權限。
(2)project中封裝實現到buildSrc 目錄下
當然真正實踐到項目中,並不會像第一種寫法一股腦寫在 build.gradle
文件中,即冗雜又缺失服用性,下面介紹第二張實現方式。
創建一個Java Library,修改 /src
裏的目錄如下圖所示,具體如何實現,網上教程太多,這裏重點強調幾個點,爲什麼要創建這樣的目錄。
目錄結構
main文件夾下:
-
groovy
文件夾替代初始java
文件及,並創建包名目錄,新增Plugin類。 -
創建資源文件
resources/META-INF/gradle-plugins
,其下的*.properties
中的*
代指插件名稱,即最終引入插件語句:apply plugin: '*'
。最後,在*.properties
文件中只需要進行一個配置:Plugin的路徑地址,具體格式如下,
implementation-class=com.hencoder.plugin_demo.DemoPlugin
下面先做一個小測試,在buildSrc 目錄下的build.gradle
配置文件中新增一個print輸出,輸入./gradlew
命令查看輸出結果:
有趣的是buildSrc 目錄下的配置文件中的輸出語句被執行了兩次,爲何?之前講到 gradle生命週期的三個階段,難道是配置階段被執行了兩次?
並非如此,只是因爲buildSrc 目錄下的配置內容被執行了兩次!buildSrc**,是一個默認的目錄,gradle在執行的時候首要Top1優先級就是讀取此文件夾下配置。因此如果setting.gradle
中還有buildSrc文件夾的配置信息,,buildSrc中配置內容則會被執行兩次。(注:在創建buildSrc 目錄時,IDE會自動將此library名稱添加到項目根目錄下的setting.gradle
配置文件中)
綜上,將根目錄下的setting.gradle
配置文件中的 :buildSrc
刪除,則輸出就只有一句了。
buildSrc 目錄重點總結
- 這是 gradle 中的⼀個特殊⽬錄,此⽬錄下的
build.gradle
配置會自動被執行,即使不配置到settings.gradle
- buildSrc 的執⾏早於任何⼀個 project,也早於
settings.gradle
,它是⼀個獨立的存在。 - buildSrc 所配置出來的 Plugin 會被 自動添加到編譯過程中的每⼀個 project 的 classpath, 因此它們纔可以直接使用
apply plugin: 'xxx'
的⽅式來便捷應⽤這些 plugin settings.gradle
中如果配置了了':buildSrc'
,buildSrc ⽬錄就會被當做是⼦ Project , 因此會被執⾏兩遍。所以在settings.gradle
⾥面應該刪掉':buildSrc'
的配置
(3)單獨抽成項目發佈
3. Transform工具
(1)定義
Android 提供的一個⼯具,在項⽬構建過程中,可以將編譯後的⽂件(jar 文件和 class 文件) 添加自定義中間處理過程。
(2)添加依賴
注意:Transform是Android提供的一個工具類,在com.android.build
包下,但是按理說其他module或者library添加時,也不需要特殊考慮build包的依賴,因爲在項目本目錄下的build.gradle
配置文件中已有 allProject的倉庫地址統一配置:
【根目錄/build.gradle】
allprojects {
repositories {
google()
jcenter()
}
}
但是在上一點也強調過,buildSrc 的執⾏早於任何⼀個 project,也早於settings.gradle
,它是⼀個獨立的存在。因此 settings.gradle
中倉庫的配置無效,需要額外在buildSrc目錄下的 build.gradle
配置文件中添加庫依賴:
// 因爲 buildSrc 早於任何一個 project 執⾏行,因此需要⾃己添加倉庫
repositories {
google()
jcenter()
}
dependencies {
implementation 'com.android.tools.build:gradle:3.1.4'
}
(3)類方法使用
import com.android.build.api.transform.Transform
......
class CustomTestTransform extends Transform{
@Override
String getName() {
return "CustomTestTransform"
}
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
@Override
Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
@Override
boolean isIncremental() {
return false
}
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
}
}
Transform實現方法介紹一覽
getName()
:對應的task名稱。後續打包的時候,會根據task名稱生成對應的任務。getInputTypes()
:指定轉換結果類型,例如字節碼或者資源文件or elsegetScopes()
:指定適用轉換範圍,例如整個project或者or else。transform(...)
: 自定義轉換邏輯。(重點方法,下面細講)
transform轉換方法內部機制
如上演示代碼,最基本構建一個CustomTestTransform類,且void transform(TransformInvocation transformInvocation)
方法中空實現,而後將其註冊到Plugin。此時安裝運行程序會直接報錯,如下圖:
如上圖可見程序安裝失敗,爲何?
尋常思路思考:註冊自定義轉換類,從父類繼承的transform
方法即使空實現(父類也應該會做基礎流程過渡的吧),也不應該影響程序正常運行呀。
但其實父類Transform的transform
方法就是空實現!因此這裏Android運行邏輯不是說把處理完的結果交給你自定義Transform去加工,而是類似於一種上下游機制,上游將結果傳遞給自定義Transform,下游在等着數據接收。因此如果自定義子類Transform中的transform
方法是空實現,會使得流程滯留,程序異常。
綜上,transform
方法可以先不顧自定義特殊邏輯的實現,但必須需要做的一點是 將從上游接受的數據結果(處理 or 未處理)返回給下游, 即入口接收數據再輸送給出口。以下的模版型代碼,無任何特殊自定義邏輯,僅做傳輸作用:
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
def inputs = transformInvocation.inputs
def outputProvider = transformInvocation.outputProvider
inputs.each {
//jarInputs: 各個依賴編譯的jar文件
it.jarInputs.each {
File dest = outputProvider.getContentLocation(it.name, it.contentTypes, it.scopes, Format.JAR)
println "jarInputs: ${it.file}"
FileUtils.copyFile(it.file, dest)
}
//directoryInputs: 本地project編譯成的多個class文件
it.directoryInputs.each {
File dest = outputProvider.getContentLocation(it.name, it.contentTypes, it.scopes, Format.DIRECTORY)
println "directoryInputs: ${it.file}"
FileUtils.copyDirectory(it.file, dest)
}
}
}
上述代碼邏輯簡單一覽:
- 從參數transformInvocation 分別獲取入口、出口
- 遍歷入口數據
- 獲取jarInputs ------ 編譯依賴的jar文件集合,再遍歷,copy輸出到出口;
- 獲取directoryInputs ------ 本地project編譯後的class文件集合,再遍歷,copy輸出到出口;
爲了更好地理解從入口獲取的這些class、jar文件集合,print文件路徑觀察log輸出結果,輸入./gradlew assembleDebug
打包。
- jarInputs集合路徑
jarInputs 集合路徑如上,這裏只截圖了一部分,觀察這些jar文件路徑不難發現,都是項目編譯所依賴的庫,且存於 /.gradle/caches/*
緩存文件夾中。(便於各個項目共用這些依賴庫)
- directoryInputs集合路徑
directoryInputs集合路徑如上,都存於項目名稱/app/build/*
即本地project編譯後的build目錄下,而且進一步點進classes目錄下,都是R文件。
其實這都是屬於各種依賴庫的文件,只是AS編譯完成項目後,屬於此項目project的class文件。
- 自定義Transform路徑
如下圖可見,這是我們自定義Transform ------ CustomTestTransform的路徑: 項目名稱/app/build/intermediates/transforms/CustomTestTransform
,而且此目錄下的文件就是自定義Transform轉換後的jar、class文件。(class文件在圖二)
你可以做一個小測試,將build文件夾刪除,再把CustomTestTransform 的transform
方法恢復空實現,也就是我們之前解釋過的導致dex文件打包失敗的**「上下流機制」**,運行./gradlew assembleDebug
:
此時程序安裝運行是失敗的,見上圖CustomTestTransform目錄下的文件是空的,沒有jar/class文件了,這也是程序爲何安裝失敗的原因:根據「上下流機制」,自定義Transform沒有將入口文件運輸(處理 or 未處理)到出口,而Android Plugin會將該目錄下的所有jar、class文件打包進一個dex文件,因此如果此目錄下沒有文件,打包後的Dex是一個空殼,屆時安裝肯定出錯。
祭出打包過程神圖如下,來源於《Gradle For Android》
(4)Transform落地業務場景
此部分提供的例子CustomTestTransform 只是模版化地將編譯完的內容原封不不動搬運到⽬目標位置, 無實際作用,在日常開發中,通常是結合javassist工具(面向切面編程),來修改字節碼。
其實在瞭解Transform提供的功能後,其落地業務場景皆由此爲基礎拓展,以下介紹幾個常見場景。
-
方法耗時統計
通過一個自定義Transform過濾每一個class/jar文件,將所有方法摘出來,插樁計時代碼。
-
方法、API搜索
黑名單方法搜索,例如Android系統升級,個別API失效。