Android Gradle
Android項目使用 Gradle 作爲構建框架,Gradle 又是以Groovy爲腳本語言。所以學習Gradle之前需要先熟悉Groovy腳本語言。
Groovy是基於Java語言的腳本語言,所以它的語法和Java非常相似,但是具有比java更好的靈活性。下面就列舉一些和Java的主要區別。
Android Gradle 的 Project 和 Tasks
這個Gradle中最重要的兩個概念。每次構建(build)至少由一個project構成,一個project 由一到多個task構成。項目結構中的每個build.gradle文件代表一個project,在這編譯腳本文件中可以定義一系列的task;task 本質上又是由一組被順序執行的Action`對象構成,Action其實是一段代碼塊,類似於Java中的方法。
Android Gradle 構建生命週期
每次構建的執行本質上執行一系列的Task。某些Task可能依賴其他Task。那些沒有依賴的Task總會被最先執行,而且每個Task只會被執行一遍。每次構建的依賴關係是在構建的配置階段確定的。每次構建分爲3個階段:
- Initialization: 初始化階段
這是創建Project階段,構建工具根據每個build.gradle文件創建出一個Project實例。初始化階段會執行項目根目錄下的settings.gradle文件,來分析哪些項目參與構建。
所以這個文件裏面的內容經常是:
include ':app'
include ':libraries'
這是告訴Gradle這些項目需要編譯,所以我們引入一些開源的項目的時候,需要在這裏填上對應的項目名稱,來告訴Gradle這些項目需要參與構建。
- Configuration:配置階段
這個階段,通過執行構建腳本來爲每個project創建並配置Task。配置階段會去加載所有參與構建的項目的build.gradle文件,會將每個build.gradle文件實例化爲一個Gradle的project對象。然後分析project之間的依賴關係,下載依賴文件,分析project下的task之間的依賴關係。
- Execution:執行階段
這是Task真正被執行的階段,Gradle會根據依賴關係決定哪些Task需要被執行,以及執行的先後順序。
task是Gradle中的最小執行單元,我們所有的構建,編譯,打包,debug,test等都是執行了某一個task,一個project可以有多個task,task之間可以互相依賴。例如我有兩個task,taskA和taskB,指定taskA依賴taskB,然後執行taskA,這時會先去執行taskB,taskB執行完畢後在執行taskA。
說到這可能會有疑問,我翻遍了build.gradle也沒看見一個task長啥樣,有一種被欺騙的趕腳!
其實不是,你點擊AndroidStudio右側的一個Gradle按鈕,會打開一個面板,內容差不多是這樣的:
裏面的每一個條目都是一個task,那這些task是哪來的呢?
一個是根目錄下的 build.gradle
中的
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
}
一個是 app 目錄下的 build.gradle
中的
apply plugin: 'com.android.application'
這兩段代碼決定的。也就是說,Gradle提供了一個框架,這個框架有一些運行的機制可以讓你完成編譯,但是至於怎麼編譯是由插件決定的。還好Google已經給我們寫好了Android對應的Gradle工具,我們使用就可以了。
根目錄下的build.gradle中 dependencies { classpath ‘com.android.tools.build:gradle:3.2.1’ } 是Android Gradle編譯插件的版本。
app目錄下的build.gradle中的apply plugin: ‘com.android.application’ 是引入了Android的應用構建項目,還有com.android.library和com.android.test用來構建library和測試。
所有Android構建需要執行的task都封裝在工具裏,如果你有一些特殊需求的話,也可以自己寫一些task。那麼對於開發一個Android應用來說,最關鍵的部分就是如何來用Android Gradle的插件了。
認知Gradle Wrapper
Android Studio中默認會使用 Gradle Wrapper 而不是直接使用Gradle。命令也是使用gradlew而不是gradle。這是因爲gradle針對特定的開發環境的構建腳本,新的gradle可能不能兼容舊版的構建環境。爲了解決這個問題,使用Gradle Wrapper 來間接使用 gradle。相當於在外邊包裹了一箇中間層。對開發者來說,直接使用Gradlew 即可,不需要關心 gradle的版本變化。Gradle Wrapper 會負責下載合適的的gradle版本來構建項目。
Android 三個文件重要的 gradle 文件
Gradle項目有3個重要的文件需要深入理解:項目根目錄的 build.gradle , settings.gradle 和模塊目錄的 build.gradle 。
-
1.settings.gradle 文件會在構建的 initialization 階段被執行,它用於告訴構建系統哪些模塊需要包含到構建過程中。對於單模塊項目, settings.gradle 文件不是必需的。對於多模塊項目,如果沒有該文件,構建系統就不能知道該用到哪些模塊。
-
2.項目根目錄的 build.gradle 文件用來配置針對所有模塊的一些屬性。它默認包含2個代碼塊:buildscript{…}和allprojects{…}。前者用於配置構建腳本所用到的代碼庫和依賴關係,後者用於定義所有模塊需要用到的一些公共屬性。
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
}
}
allprojects {
repositories {
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
buildscript:定義了 Android 編譯工具的類路徑。repositories中, jCenter是一個著名的 Maven 倉庫。
allprojects:中定義的屬性會被應用到所有 moudle 中,但是爲了保證每個項目的獨立性,我們一般不會在這裏面操作太多共有的東西。
模塊級配置文件 build.gradle 針對每個moudle 的配置,如果這裏的定義的選項和頂層 build.gradle定義的相同。它有3個重要的代碼塊:plugin,android 和 dependencies。
定製項目屬性(project properties)
在項目根目錄的build.gradle配置文件中,我們可以定製適用於所有模塊的屬性,通過ext 代碼塊來實現。如下所示:
ext {
compileSdkVersion = 28
buildToolsVersion = "28.0.0"
}
然後我們可以在模塊目錄的build.gradle配置文件中引用這些屬性,引用語法爲rootProject.ext.{屬性名}。如下:
android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
}
Android studio gradle Task
//構建
gradlew app:clean //移除所有的編譯輸出文件,比如apk
gradlew app:build //構建 app module ,構建任務,相當於同時執行了check任務和assemble任務
//檢測
gradlew app:check //執行lint檢測編譯。
//打包
gradlew app:assemble //可以編譯出release包和debug包,可以使用gradlew assembleRelease或者gradlew assembleDebug來單獨編譯一種包
gradlew app:assembleRelease //app module 打 release 包
gradlew app:assembleDebug //app module 打 debug 包
//安裝,卸載
gradlew app:installDebug //安裝 app 的 debug 包到手機上
gradlew app:uninstallDebug //卸載手機上 app 的 debug 包
gradlew app:uninstallRelease //卸載手機上 app 的 release 包
gradlew app:uninstallAll //卸載手機上所有 app 的包
這些都是基本的命令,在實際項目中會根據不同的配置,會對這些task 設置不同的依賴。比如 默認的 assmeble 會依賴 assembleDebug 和assembleRelease,如果直接執行assmeble,最後會編譯debug,和release 的所有版本出來。如果我們只需要編譯debug 版本,我們可以運行assembleDebug。
除此之外還有一些常用的新增的其他命令,比如 install命令,會將編譯後的apk 安裝到連接的設備。
gradle項目實戰
最近參與基礎架構組的crashly收集項目,其中一個模塊就是收集項目中使用到的插件和sdk的四級(或三級)包路徑和混淆後的mapping文件, 然後調用python文件實現文件和數據字段的上傳。請求網絡上傳數據這塊python代碼量非常少就幾行代碼的事情,所以我在項目中使用了兩個.gradle文件分別掃描項目中使用到的插件和sdk的四級(或三級)包路徑,然後在.gradle文件中的task中調用python文件上傳數據,這樣工作就告一段落,然後當我找QA和負責打包的同事提交我的幾個文件時才知道我這做法是多麼lowbee, 這樣實現需要每個業務線的項目都接入我的幾個.gradle文件和.py文件,每次.gradle和.py文件修改在提交上去後各個業務線需要拉去最新的代碼,這樣的話我需要把各個業務線的項目源碼都下載到本地,挨個改一遍在提交,這種做法沒有做到通用性和可維護性。在同事的建議下,我又硬着頭皮開始研究自定義gradle插件,每個業務線只需要apply插件就可以,這個方案不錯,開始折騰gradle插件。
首先說下功能就是定義2個任務,其中一個任務就是掃描業務方的項目過濾出使用到的四級(或三級)包路徑,然後生成一個文件連同參數一起使用py腳本上傳到後臺。
但是這裏遇到了2個問題:
1. 在插件module下的python文件在插件中是沒法訪問的,插件中是很容易拿到業務方的目錄,如果將python文件放在業務方的項目中,同樣是擴展性不好;
2. 在插件中上傳文件和其他參數我引入了第三方庫的依賴,執行 uploadArchives 任務將代碼上傳到本地倉庫後,在業務方demo中報錯,無法訪問到插件中的依賴。
我在技術羣裏問了幾遍結果都沒有人迴應,僅有的一個同行回覆是遇到類似的問題,然後他的建議是去網上下載一個有類似功能的demo,然後修改下邏輯。順着這條線索我去網上找到第四個纔算是試成功了,下面將操作過程重演一遍。
1) 首先新建一個Library庫,刪掉裏面的文件最後保留和新建的文件夾如下圖所示:
2) 刪掉build.gradle中的內容,最終配置如下所示:
3) 在groovy文件夾下新建包名並在該名下新建文件CrashlyPlugin.groovy, 內容如下:
4) 在gradle-plugins文件夾下新建一個文件com.ke.crashly.plugin.properties, 內容如下:
5) sync一下項目,然後找到開發工具右側的Gradle下的:crashlyplugin -> Tasks -> upload -> 雙擊uploadArchives,就會在當前項目下生成一個目錄crashlyrepo, 接下來就可以在項目中使用這個插件了;
6) 在工程的根目錄下的build.gradle文件下添加如下代碼:
7) 在工程的主module的build.gradle文件最後添加如下代碼:
8) sync一下工程,同樣是在開發工具右側的Gradle標籤下,找到:app -> Tasks -> other -> uploadRepoNameForCrashly控制檯就會打印百度首頁網頁代碼,也可以在Terminal下輸入命令: gradle task -q uploadRepoNameForCrashly -s.
生命週期函數的應用
project.gradle.buildFinished {
// 可以上傳mapping.txt文件
}
project.afterEvaluate {
// 可以獲取在build.gradle文件中的自定義的配置信息
it.android.applicationVariants.all { variant ->
// 當存在多個變體時,在這裏可以遍歷多個變體和applicationId, map的形式保存
// 根據構建模式的信息最終確定applicationId
}
}
project.gradle.startParameter.getTaskNames().each {
// 命令行輸入 ./gradlew clean assembleRelease -s
// 會輸出 clean assembleRelease
// 可以利用這個信息確定構建模式
}
gradle插件的三種形式
1. 在build.gradle中編寫自定義插件
2. buildSrc工程項目
將插件的源代碼放在rootProjectDir/buildSrc/src/main/groovy目錄中,Gradle會自動識別來完成編譯和測試。Android Studio會找rootPrjectDir/buildSrc/src/main/groovy目錄下的代碼。
坑:當新建libraray module並命名爲buildSrc後會提示Plugin with id'com.android.library' not found.
這是因爲buildSrc是Android的保留名稱,只能作爲plugin插件使用,我們修改buildSrc的build.gradle文件後就不報錯了,且這個工程的構建時間最早。
3. 獨立項目或獨立Module
無論是在build.gradle中編寫自定義插件,還是buildSrc項目中編寫自定義插件,都只能在自己的項目中進行使用。如果想要分享給其他人或者自己用,可以在一個獨立的項目中編寫插件,這個項目會生成一個包含插件類的JAR文件,有了JAR文件就很容易進行分享了。
這裏主要講一下buildSrc工程項目, 這個工程可以支持多個gradle插件本地調試。
1. 目錄結構如下所示
2. buildSrc#build.gradle文件內容
apply plugin: 'groovy'
apply plugin: 'maven'
repositories {
mavenCentral()
}
dependencies{
//gradle sdk
implementation gradleApi()
//groovy sdk
implementation localGroovy()
}
// 如果有多個gradle插件, 在下面添加文件路徑
sourceSets {
main {
java.srcDirs = ['src/main/java'
]
groovy.srcDirs = ['src/main/groovy',
'../lifecycle_plugin/src/main/groovy'
]
resources.srcDirs = ['src/main/resources',
'../lifecycle_plugin/src/main/resources'
]
}
}
3. gradle插件的build.gradle文件
apply plugin: 'groovy'
dependencies{
//gradle sdk
implementation gradleApi()
//groovy sdk
implementation localGroovy()
}
//以上配置比較固定
//以下內容主要用來上傳插件
apply plugin: 'maven'
repositories {
mavenCentral()
}
// classpath: 'group:name:version' // buildSrc模式下, name爲module名稱
group = 'com.ke.plugin'
version = '1.0.0-SNAPSHOT'
uploadArchives{
repositories {
mavenDeployer{
repository(url: uri('../repositories')) // 和gradle插件同級目錄下
}
}
}
4. gradle插件實現的功能, 拷貝mapping.txt文件到指定目錄下
class LifeCyclePlugin implements Plugin<Project> {
@Override
void apply(Project project) {
project.extensions.create("pluginConfig", PluginConfig)
// 命令行輸入 ./gradlew clean assembleRelease -s
project.gradle.startParameter.taskNames.each {
println("taskName: " + it.toString())
// taskName: clean
// taskName: assembleRelease
}
// writeReleaseApplicationId 在這個tasks裏面
project.afterEvaluate {
project.tasks.each {
}
println("mappingPath ---> " + project.extensions.pluginConfig.mappingPath)
}
project.gradle.taskGraph.whenReady {
project.gradle.taskGraph.allTasks.each {
}
}
project.tasks.create(name: "copyMappingFileTask", type: Copy) {
// from(new File('build/outputs/mapping/release/mapping.txt'))
from(new File("build/outputs/mapping"))
include '*/mapping.txt'
into "build/output/mapping"
}
project.task('copyMappingFile') {
doLast {
try {
File resultFile = new File(project.buildDir, "/output/mapping.txt")
if (!resultFile.exists()) {
resultFile.parentFile.mkdirs()
resultFile.createNewFile()
}
List<File> plugMappings = searchFiles(new File(project.buildDir.absolutePath + "/outputs/mapping"), "mapping.txt")
if (plugMappings && plugMappings.size() > 0 && resultFile.exists()) {
// 使用NIO來操作流
FileChannel channel = new FileOutputStream(resultFile.absolutePath).getChannel()
for (File file : plugMappings) {
FileChannel fc = new FileInputStream(file).getChannel()
channel.transferFrom(fc, channel.size(), fc.size())
fc.close()
}
channel.close()
}
} catch (Exception e) {
e.printStackTrace()
}
}
}
project.tasks.whenTaskAdded { task ->
if ("assembleRelease".equals(task.name)) {
// task.dependsOn(project.tasks.getByName('copyMappingFile'))
}
if ('transformClassesWithDexBuilderForRelease'.equals(task.name)) {
// task.dependsOn(project.tasks.getByName('copyMappingFileTask'))
}
}
}
/**
* 獲取給定目錄下指定文件名的文件集合
*/
static List<File> searchFiles(File folder, final String keyword) {
List<File> result = new ArrayList<File>()
File[] subFolders = folder.listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
if (file.isDirectory()) {
return true
}
if (file.getName().toLowerCase().contains(keyword)) {
return true
}
return false
}
})
if (subFolders != null) {
for (File file : subFolders) {
if (file.isFile() && file.getName().toLowerCase().contains(keyword)) {
// 如果是文件則將文件添加到結果列表中
result.add(file)
} else {
// 如果是文件夾,則遞歸調用本方法,然後把所有的文件加到結果列表中
result.addAll(searchFiles(file, keyword))
}
}
}
return result
}
}
5. 本地使用該gradle插件
根目錄下的build.gradle文件加入如下代碼
buildscript {
repositories {
...
maven {
// ../ 代表當前父目錄的父目錄
url (uri('./repositories'))
}
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
// group : PluginProjectName : version
classpath 'com.ke.plugin:lifecycle_plugin:1.0.0-SNAPSHOT'
}
}
module下的build.gradle添加一行代碼
apply plugin: 'com.ke.lifecycle.plugin'
就可以在工程中調試gradle插件的功能。
針對共享插件, 修改的地方就是將產物上傳到maven, 配置文件如下所示:
apply plugin: 'com.github.dcendents.android-maven'
apply plugin: 'com.jfrog.bintray'
group = GROUP
version = VERSION_NAME
def getPropertyFromLocalProperties(key) {
File file = project.rootProject.file('local.properties');
if (file.exists()) {
Properties properties = new Properties()
properties.load(file.newDataInputStream())
return properties.getProperty(key)
}
}
def getRepositoryUrl() {
return isSnapshot() ? getPropertyFromLocalProperties("SNAPSHOT_REPOSITORY_URL") : getPropertyFromLocalProperties("RELEASE_REPOSITORY_URL")
// return isSnapshot() ? SNAPSHOT_REPOSITORY_URL : RELEASE_REPOSITORY_URL
}
def isSnapshot() {
return version.endsWith("SNAPSHOT");
}
def hasAndroidPlugin() {
return getPlugins().inject(false) { a, b->
def classStr = b.getClass().name
def isAndroid = ("com.android.build.gradle.LibraryPlugin" == classStr) || ("com.android.build.gradle.AppPlugin" == classStr)
a || isAndroid
}
}
task sourcesJar(type: Jar) {
if (hasAndroidPlugin()) {
from android.sourceSets.main.java.srcDirs
classifier = 'sources'
} else {
from sourceSets.main.allSource
classifier = 'sources'
}
}
artifacts {
archives sourcesJar
}
uploadArchives {
// repositories.mavenDeployer {
// repository(url: uri('/Users/xxx/.m2/repository/')) // 本地倉庫的路徑
// pom.groupId = "${project.group}" //groupId ,自行定義,一般是包名
// pom.artifactId = "${project.name}" //artifactId ,自行定義
// pom.version = "${version}" //version 版本號
// }
repositories.mavenDeployer {
repository(url: repositoryUrl) {
authentication(userName: getPropertyFromLocalProperties("USER"), password: getPropertyFromLocalProperties("PASSWORD"))
}
}
}
bintray {
user = getPropertyFromLocalProperties("bintray.user")
key = getPropertyFromLocalProperties("bintray.apikey")
configurations = ['archives']
pkg {
repo = 'maven'
name = "${project.group}:${project.name}"
userOrg = 'xxx'
licenses = ['Apache-2.0']
websiteUrl = 'xxx'
vcsUrl = 'xxx'
publish = true
}
}
classpath 'com.github.dcendents:android-maven-gradle-plugin:1.3'
classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7.3'
一些常量信息都是在local.properties中配置。
Gradle plugin 調試
1. run - > Edit Configurations - > Remote name = debug
2. 命令行輸入: ./gradlew clean build -Dorg.gradle.daemon=false -Dorg.gradle.debug=true