歡迎關注本人公衆號,掃描下方二維碼或搜索公衆號 id: mxszgghtml
本文基於 Android Gradle plugin 3.0.1java
task 至關於開發者平常開發中所接觸到的函數、方法,它們是相同的一個概念。在前文寫給 Android 開發者的 Gradle 系列(一)基本姿式已經提到過 task 的概念,例如 transformClassesAndResourcesWithProguardForRelease
task 是爲了混淆 release 包中源碼的。android
接下來就開始實操,首先在 app/build.gradle
中添加以下依賴:shell
compileOnly 'com.android.tools.build:gradle:3.0.1'api
便可在 External Libraries 中看到關於 Gradle plugin 的源碼。bash
Gradle 源碼如何引入將會在下一節中介紹。微信
根據官方文檔和 Task#create() 能夠知道,task 的基本寫法能夠是以下四種:閉包
task myTask
task myTask { configure closure }
task (myTask) { configure closure }
task (name: myTask) { configure closure }
複製代碼
每個 task 都有本身的名字,這樣開發者才能調用它,例如調用上面的 task:app
./gradlew myTask
ide
可是有一個問題,假若當前項目的 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 添加操做,能夠添加在 doLast {}/doFirst {}
等閉包中,例如:
task myTask {
doFirst {
println 'myTask 最早執行的內容'
}
doLast {
println 'myTask 最後執行的內容'
}
// warning
// println 'Configuration 階段和 Execution 階段皆會執行'
}
複製代碼
切記大部分的內容是寫在
doLast{}
或doFirst{}
閉包中,由於寫在若是寫在 task 閉包中的話,會在Configuration
階段也被執行。
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 變量。若是不清楚這個細節,建議回顧上一篇文章的附錄。
開發者常使用 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
觀察輸出結果。
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 撰寫了。
上面的 task 看起來彷佛和 Android 的構建過程並沒有多大關係,沒錯,那麼接下來不妨深層次接觸試試——經過 hook 原生 task 實現更改打包中的文件——在打包過程當中向 assets
插入一張圖片。
儘管這看起來絲毫沒卵用
在打包流程中,有一個 task 名爲 packageDebug
,該 task 是打包文件生成 apk 的——
接着,不妨在命令行鍵入如下命令:
./gradlew help --task "packageDebug"
Type
PackageApplication (com.android.build.gradle.tasks.PackageApplication)
能夠看到,該 task 的 type 是 PackageApplication
——
不妨再看看它的父類 PackageAndroidArtifact
:
看到關鍵信息,該 task 中有一個類型爲 FileCollection assets
字段,這即是最終打入 apk 中的那個 assets 了。因此不難寫出如下代碼——
task hookAssets {
afterEvaluate {
tasks.findByName("packageDebug").doFirst { task ->
copy {
from "${projectDir.absolutePath}/test.png"
into "${task.assets.asPath}"
}
}
}
}
複製代碼
afterEvaluate
以後找到 packageDebug
tasktest.png
,使用 copy {}
閉包,from
填入的參數爲 test.png
的路徑,into
填入的參數爲輸出的路徑,也就是 assets
的路徑。能夠看到 /app/build/intermediates/assets/debug/
下存有 test.png
一樣地,解壓 apk 文件也能夠看到——
一個實打實的 Gradle task hook 流程就這麼操做完了。
若是說 Gradle task 是函數的話,那麼 Gradle plugin 就是函數庫,在後一節筆者將會對 Gradle plugin 進行闡述。
固然,若是各位讀者有疑問的話,歡迎加入筆者的微信羣。 若是二維碼失效,能夠查看筆者最新文章的尾部。