歡迎關注本人公眾號,掃描下方二維碼或搜尋公眾號 id: mxszgg
本文基於 Android Gradle plugin 3.0.1
前言
task 相當於開發者日常開發中所接觸到的函式、方法,它們是相同的一個概念。在前文寫給 Android 開發者的 Gradle 系列(一)基本姿勢已經提到過 task 的概念,例如 transformClassesAndResourcesWithProguardForRelease
task 是為了混淆 release 包中原始碼的。
接下來就開始實操,首先在 app/build.gradle
中新增如下依賴:
compileOnly 'com.android.tools.build:gradle:3.0.1'
即可在 External Libraries 中看到關於 Gradle plugin 的原始碼。
![這裡寫圖片描述](https://i.iter01.com/images/5e2abcf92431a049e267de926d524dcec024f04e4f3f3db30a5b449499857a7e.png)
Gradle 原始碼如何引入將會在下一節中介紹。
task 撰寫
task 宣告
根據官方文件和 Task#create() 可以知道,task 的基本寫法可以是如下四種:
task myTask
task myTask { configure closure }
task (myTask) { configure closure }
task (name: myTask) { configure closure }
複製程式碼
每一個 task 都有自己的名字,這樣開發者才能呼叫它,例如呼叫上面的 task:
./gradlew myTask
但是有一個問題,倘若當前專案的 app module 和 a module 都含有一個名為 myTask 的 task,那麼會不會起衝突,該如何呼叫它們?答案是不會衝突,呼叫方式如下:
./gradlew app:myTask
(呼叫 app module 的 myTask)
./gradlew a:myTask
(呼叫 a module 的 myTask)
通過 ProjectName:taskName
的形式便可以指定唯一絕對路徑去呼叫指定 Project 的指定 task 了。
擴充套件
根據 Task#create() 可以知道,task 的建立是可以宣告引數的,除了上述的 name
引數之外,還有如下幾種:
-
type
:預設為 DefaultTask。類似於父類。在後文中將會提及該引數。 -
dependsOn
:預設為[]。希望依賴的 tasks,等同於Task.dependsOn(Object... path)
中的 path。在後文中將會提及該引數。 -
action
:預設為 null。等同於Task.doFirst { Action }
中的 Action。task (name: actionTest, action: new Action<Task>() { @Override void execute(Task task) { println 'hello' } }) { } 複製程式碼
等同於
task (name: actionTest) { doFirst { println 'hello' } } 複製程式碼
-
override
:預設為 false。是否替換已存在的 task。 -
group
:預設為 null。task 的分組型別。 -
description
:預設為 null。task 描述。 -
constructorArgs
:預設為 null。傳給 task 建構函式的引數。
後面四種大部分開發過程中應該不怎麼會用到,有需要的讀者自行查閱文件。
task 內容格式
-
根據官方文件以及前一篇文章中可以知道,如果想給 task 新增操作,可以新增在
doLast {}/doFirst {}
等閉包中,例如:task myTask { doFirst { println 'myTask 最先執行的內容' } doLast { println 'myTask 最後執行的內容' } // warning // println 'Configuration 階段和 Execution 階段皆會執行' } 複製程式碼
切記大部分的內容是寫在
doLast{}
或doFirst{}
閉包中,因為寫在如果寫在 task 閉包中的話,會在Configuration
階段也被執行。
- 根據官方文件可知,為了提高 task 複用性,Gradle 還支援 Task 類的書寫——
2.1 將下述程式碼寫在 build.gradle
中,並用 @TaskAction
標記想要執行的方法。
class GreetingTask extends DefaultTask {
String greeting = 'hello from GreetingTask'
@TaskAction
def greet() {
println greeting
}
}
複製程式碼
2.2 在 build.gradle
中撰寫 task 呼叫 GreetingTask 類:
// Use the default greeting
task (name: hello , type: GreetingTask)
// Customize the greeting
task (name: greeting , type: GreetingTask) {
greeting = 'greetings from GreetingTask'
}
複製程式碼
2.3 呼叫該 task——
./gradlew hello
> Task :app:hello
hello from GreetingTask
./gradlew greeting
> Task :app:greeting
greetings from GreetingTask
複製程式碼
所以看到這裡應該不僅能夠理解 Task 類的書寫,並且應該能夠大致明白 type
這個引數的含義了。
不知道會不會和筆者一樣事兒逼的讀者此時會疑惑 @TaskAction
修飾的方法和 doLast {}
以及 doFirst {}
閉包的執行順序是怎樣的?
task (name: hello, type: GreetingTask) {
doFirst {
def list = getActions()
for (int i = 0; i < list.size(); i++) {
println list.get(i).displayName
}
}
doLast {
}
}
複製程式碼
首先宣告 doFirst {}
和 doLast {}
閉包;然後戳進 DefaultTask 原始碼並追蹤到頂級父類 AbstractTask 中可以看到內部通過使用 actions
儲存所有執行的 Action,並通過 getAction()
暴露;actions
是 List 型別,內部的元素型別是 ContextAwareTaskAction,該介面又實現了 Describable,Describable 僅宣告瞭一個 getDisplayName()
方法,所以可以直接通過 displayName 獲取該 Action 的名稱。
理解以上三步即可完成上述 task 撰寫,在命令列中試試——
./gradlew hello
> Task :app:hello
Execute doFirst {} action
Execute greet
Execute doLast {} action
複製程式碼
Gradle 內部將會自動為變數設定 setter、getter 方法,所以當一個 Gradle 有
getXxx()
方法時,可以直接使用 xxx 變數。如果不清楚這個細節,建議回顧上一篇文章的附錄。
task 依賴關係
開發者常使用 dependsOn
來指定依賴關係(另外兩種是指定 task 執行順序,詳見文件 Task Dependencies and Task Ordering),如下:
task a {
doLast {
println 'a'
}
}
task b {
dependsOn('a')
doFirst {
println 'b'
}
}
複製程式碼
不妨將以上程式碼寫在 app build.gradle
檔案下,當執行 task b 的時候,會輸出如下資訊:
./gradlew task app:b
> Task :app:a
a
> Task :app:b
b
可以看到,由於 task b 需要依賴 task a,所以 task b 執行的時候會先執行 task a。
有經驗的開發者如果在命令列中試過
assembleDebug
等命令會發現,它們的執行將會依賴於許多其他 task。所以不妨在命令列中試試./gradlew assembleDebug
觀察輸出結果。
task 實戰
install && launch apk
com.android.application
自帶 installDebug
task,開發者可以使用 installDebug
安裝當前專案 apk:
./gradlew installDebug
> Task :app:installDebug
Installing APK 'app-debug.apk' on 'xxxxx' for app:debug Installed on 1 device.
但是似乎看起來有些不盡人意的地方,例如開發者希望安裝的時候能夠順帶能夠啟動該 app。那麼該如何做呢?
首先從問題的可行性上來進行分析,開發者的直覺告訴我們是可以通過 gradle 實現的——命令列可以安裝、啟動 apk——adb install -r app-debug.apk
和 adb shell am start -n 包名/首 Activity
。所以關鍵點就是如何通過 gradle 呼叫命令列程式碼以及如何獲取到 包名/首 Activity
資訊。
-
開發者的直覺同樣告訴我們 Gradle 開發文件中有關於命令列呼叫的資訊,只需要使用
exec {}
閉包就好了。 -
如何獲取
包名/首 Activity
資訊?可以通過AndroidManifest.xml
來獲取。部分經驗豐富的開發者知道——打入 apk 中的AndroidManifest.xml
檔案並不是我們平常寫的AndroidManifest.xml
,而是 apk 編譯後位於Project/app/build/intermediates/manifests/full/debug/
包下的AndroidManifest.xml
(當然,如果是 Release 包的話,應該是Project/app/build/intermediates/manifests/full/release/
包下)。-
包名就是
android
閉包下的defaultConfig
閉包下的applicationId
。 -
目標 Activity 則是包含 action 為
android.intent.action.MAIN
的 Activity。
-
理解了以上內容,便不難理解下面的內容:
task installAndRun(dependsOn: 'assembleDebug') {
doFirst {
exec {
workingDir "${buildDir}/outputs/apk/debug"
commandLine 'adb', 'install', '-r', 'app-debug.apk'
}
exec {
def path = "${buildDir}/intermediates/manifests/full/debug/AndroidManifest.xml"
// xml 解析
def parser = new XmlParser(false, false).parse(new File(path))
// application 下的每一個 activity 結點
parser.application.activity.each { activity ->
// activity 下的每一個 intent-filter 結點
activity.'intent-filter'.each { filter ->
// intent-filter 下的 action 結點中的 @android:name 包含 android.intent.action.MAIN
if (filter.action.@"android:name".contains("android.intent.action.MAIN")) {
def targetActivity = activity.@"android:name"
commandLine 'adb', 'shell', 'am', 'start', '-n',
"${android.defaultConfig.applicationId}/${targetActivity}"
}
}
}
}
}
}
複製程式碼
-
install apk 的前提必須是得有一個 apk,所以勢必需要依賴
assembleDebug
task。實際上
installDebug
task 也是依賴assembleDebug
task 的,不妨可以試試——task showInstallDepends { doFirst { println project.tasks.findByName("installDebug").dependsOn } } 複製程式碼
./gradlew showInstallDepends
> Configure project :app
[task 'installDebug' input files, assembleDebug]
-
exec
閉包中的幾個引數提及下——2.1
workingDir
:工作環境,引數為 File 格式。預設為當前 project 目錄。2.2
commandLine
:需要命令列執行的命令,引數為 List 格式。 -
前一篇文章中提到 ——
說白了它們其實就是一些閉包、一些固定格式,正是因為它們的格式是固定的,task 才能夠讀取到相應的資料完成相應的事情。
在第二個
exec
閉包的第八行就很好的體現了這一點,通過{android.defaultConfig.applicationId}
直接獲取到 Gradle 檔案中android
閉包下的defaultConfig
閉包下的applicationId
的值。由此就獲得了當前應用的包名。當然,除了 Gradle 能夠呼叫命令列以外,實際上 groovy 也是可以呼叫命令列的,但在此就不做擴充套件了。
-
至於最先啟動的 Activity,肯定是 action 為
android.intent.action.MAIN
的 Activity,那麼問題就是變成如何在AndroidManifest.xml
中尋找到該 Activity 的事了——作為一個合格的老司機,應該能夠想到 groovy 一定會提供相應的 xml 解析 API 的,至於具體的使用筆者就不在此擴充套件了,留給各位讀者去原始碼中探索成長。 -
除去上面的資訊以外,還需要什麼?還需要知道一些 gradle 構建的資訊——例如 debug 包會最終出現在
${buildDir}/outputs/apk/debug
;例如 debug 包中的AndroidManifest.xml
並不是日常開發中寫的那個AndroidManifest.xml
(雖然可能它倆基本沒什麼差異),而是${buildDir}/intermediates/manifests/full/debug
下的AndroidManifest.xml
。所以一是希望各位讀者日常多去翻翻 build 資料夾,二是要知道${buildDir}
(build 資料夾)有多麼重要,因為 Gradle 構建 apk 的過程中,但凡有輸出檔案那麼基本都會存在這個資料夾中,所以多去翻一翻。
由此之後,可以在命令列輸入以下命令:
./gradlew installAndRun
> Task :app:installAndRun
[ 4%] /data/local/tmp/app-debug.apk
[ 8%] /data/local/tmp/app-debug.apk
[ 12%] /data/local/tmp/app-debug.apk ...
Success
Starting: Intent {com.test.Test/TestActivity}
至此便完成了一個安裝並啟動 apk 的 task 撰寫了。
hook assets
上面的 task 看起來似乎和 Android 的構建過程並無多大關係,沒錯,那麼接下來不妨深層次接觸試試——通過 hook 原生 task 實現更改打包中的檔案——在打包過程中向 assets
插入一張圖片。
儘管這看起來絲毫沒鳥用
在打包流程中,有一個 task 名為 packageDebug
,該 task 是打包檔案生成 apk 的——
![這裡寫圖片描述](https://i.iter01.com/images/1eb6f9a2bd1392e48d7920704de9a2f7155351399ad7f573e60d324ee73e8cd7.png)
接著,不妨在命令列鍵入以下命令:
./gradlew help --task "packageDebug"
Type
PackageApplication (com.android.build.gradle.tasks.PackageApplication)
可以看到,該 task 的 type 是 PackageApplication
——
![這裡寫圖片描述](https://i.iter01.com/images/da6a990440d85f52c39bd16d2b88308f83161ea6c4babbffc667ca5aa27b4e27.png)
不妨再看看它的父類 PackageAndroidArtifact
:
![這裡寫圖片描述](https://i.iter01.com/images/62b03472dbb6fcf23e691d56f0af729390d87ea52eb9d04c20d698b48be2ff7f.png)
看到關鍵資訊,該 task 中有一個型別為 FileCollection assets
欄位,這便是最終打入 apk 中的那個 assets 了。所以不難寫出以下程式碼——
task hookAssets {
afterEvaluate {
tasks.findByName("packageDebug").doFirst { task ->
copy {
from "${projectDir.absolutePath}/test.png"
into "${task.assets.asPath}"
}
}
}
}
複製程式碼
- 在 project
afterEvaluate
之後找到packageDebug
task - 不妨在 app 目錄下放入一個
test.png
,使用copy {}
閉包,from
填入的引數為test.png
的路徑,into
填入的引數為輸出的路徑,也就是assets
的路徑。
可以看到 /app/build/intermediates/assets/debug/
下存有 test.png
![這裡寫圖片描述](https://i.iter01.com/images/b45a88c476305def4e9ecfa9be92aa4dc52d7524ec9768d1640eaecf2b2d4857.png)
同樣地,解壓 apk 檔案也可以看到——
![這裡寫圖片描述](https://i.iter01.com/images/194d080a8e3a67951139f8cddd3b9650030cb5c7e39529bcd350e5e951f098f3.png)
一個實打實的 Gradle task hook 流程就這麼操作完了。
後記
如果說 Gradle task 是函式的話,那麼 Gradle plugin 就是函式庫,在後一節筆者將會對 Gradle plugin 進行闡述。
當然,如果各位讀者有疑問的話,歡迎加入筆者的微信群。 如果二維碼失效,可以檢視筆者最新文章的尾部。