Gradle 完整指南(Android)

前言

為什么需要學(xué)Gradle?

Gradle 是 Android 現(xiàn)在主流的編譯工具,雖然在Gradle 出現(xiàn)之前和之后都有對應(yīng)更快的編譯工具出現(xiàn),但是 Gradle 的優(yōu)勢就在于它是親兒子,Gradle 確實比較慢,這和它的編譯過程有關(guān),但是現(xiàn)在的Gradle 編譯速度已經(jīng)有了成倍提高。除此之外,相對其他編譯工具,最重要的,他和 Android Studio 的關(guān)系非常緊密,可以說對于一些簡單的程序我們幾乎不需要任何代碼上的配置只使用 Android Studio 就可以完成編譯和運(yùn)行。

但是對于一些比較復(fù)雜的,特別是多人團(tuán)隊合作的項目我們會需要一些個性化的配置來提高我們的開發(fā)效率。比如我們要自定義編譯出的apk包的名字、對于一些特殊產(chǎn)品我們可能會要用同一個項目編譯出免費(fèi)版和付費(fèi)版的apk。這些高級的功能都需要我們對配置代碼進(jìn)行自定義地修改。

最近伴隨著 Android Studio2.0的發(fā)布, Gradle 也進(jìn)行了一次非常大的升級,叫Instant Run.它的編譯速度網(wǎng)上有人用逆天兩個字來形容。當(dāng)我們第一次點擊run、debug按鈕的時候,它運(yùn)行時間和我們往常一樣。但是接下去的時間里,你每次修改代碼后點擊run、debug按鈕,對應(yīng)的改變將迅速的部署到你正在運(yùn)行的程序上,傳說速度快到你都來不及把注意力集中到手機(jī)屏幕上,它就已經(jīng)做好相應(yīng)的更改。但是剛出來的似乎對一些項目的兼容性不太好,現(xiàn)在升級后不知道怎么樣。

為什么要了解命令行編譯?

在很多情況下我們都是使用的 Android Studio 來build、debug項目。Android Studio 能滿足我們開發(fā)的大多數(shù)需求,但是某些情況下命令行能夠讓我們編譯的效率更高,過程更明朗,一些高級的配置也需要熟悉命令行才能夠使用,比如在服務(wù)器編譯,某些項目初始化的時候如果直接交給Android Studio ,它會一直Loading,你都不知道它在干嘛,但是用命令行你就知道它卡在了哪個環(huán)節(jié),你只需要修改某些代碼,馬上就能夠編譯過了。

了解 Gradle 之后我們可以做什么?

we can do everything what we want.

自定義編譯輸出文件格式。

hook Android 編譯過程。

配置和改善 Gradle 編譯速度

Gralde Overview

History

我們知道,Android 的編譯過程非常復(fù)雜:

我們需要一種工具幫我們更快更方便更簡潔地完成 Android 程序的編譯?,F(xiàn)在結(jié)合Android Studio 我們一般使用的工具都是Gradle, 在 Gradle 出現(xiàn)以前Android 也有對應(yīng)的編譯工具叫Ant,在Gradle 出現(xiàn)之后,也有新的編譯工具出現(xiàn),就是FaceBook 的Buck工具。這些編譯工具在出現(xiàn)的時候幾乎都比 Gradle 要快,Gradle 之所以慢是跟它的編譯周期有很大關(guān)系。

Gradle 的編譯周期

在解析 Gradle 的編譯過程之前我們需要理解在 Gradle 中非常重要的兩個對象。ProjectTask。

每個項目的編譯至少有一個 Project,一個build.gradle就代表一個project,每個project里面包含了多個task,task 里面又包含很多action,action是一個代碼塊,里面包含了需要被執(zhí)行的代碼。

在編譯過程中, Gradle 會根據(jù)build 相關(guān)文件,聚合所有的project和task,執(zhí)行task 中的 action。因為build.gradle文件中的task非常多,先執(zhí)行哪個后執(zhí)行那個需要一種邏輯來保證。這種邏輯就是依賴邏輯,幾乎所有的Task 都需要依賴其他 task 來執(zhí)行,沒有被依賴的task 會首先被執(zhí)行。所以到最后所有的 Task 會構(gòu)成一個有向無環(huán)圖(DAG Directed Acyclic Graph)的數(shù)據(jù)結(jié)構(gòu)。

編譯過程分為三個階段:

初始化階段:創(chuàng)建 Project 對象,如果有多個build.gradle,也會創(chuàng)建多個project.

配置階段:在這個階段,會執(zhí)行所有的編譯腳本,同時還會創(chuàng)建project的所有的task,為后一個階段做準(zhǔn)備。

執(zhí)行階段:在這個階段,gradle 會根據(jù)傳入的參數(shù)決定如何執(zhí)行這些task,真正action的執(zhí)行代碼就在這里.

剛剛我們提到Gradle 編譯的時候的一些相關(guān)文件,下面我們挨個解析一下這些文件。

Gradle Files

對于一個gradle 項目,最基礎(chǔ)的文件配置如下:

一個項目有一個setting.gradle、包括一個頂層的build.gradle文件、每個Module 都有自己的一個build.gradle文件。

setting.gradle:這個 setting 文件定義了哪些module 應(yīng)該被加入到編譯過程,對于單個module 的項目可以不用需要這個文件,但是對于 multimodule 的項目我們就需要這個文件,否則gradle 不知道要加載哪些項目。這個文件的代碼在初始化階段就會被執(zhí)行。

頂層的build.gradle:頂層的build.gradle文件的配置最終會被應(yīng)用到所有項目中。它典型的配置如下:

buildscript {? ? repositories {? ? ? ? jcenter()? ? }? ? dependencies {? ? ? ? classpath'com.android.tools.build:gradle:1.2.3'}}allprojects{? ? repositories{? ? ? ? jcenter()? ? }}

buildscript:定義了 Android 編譯工具的類路徑。repositories中,jCenter是一個著名的 Maven 倉庫。

allprojects:中定義的屬性會被應(yīng)用到所有 moudle 中,但是為了保證每個項目的獨(dú)立性,我們一般不會在這里面操作太多共有的東西。

每個項目單獨(dú)的 build.gradle:針對每個moudle 的配置,如果這里的定義的選項和頂層build.gradle定義的相同,后者會被覆蓋。典型的 配置內(nèi)容如下:

apply plugin:第一行代碼應(yīng)用了Android 程序的gradle插件,作為Android 的應(yīng)用程序,這一步是必須的,因為plugin中提供了Android 編譯、測試、打包等等的所有task。

android:這是編譯文件中最大的代碼塊,關(guān)于android 的所有特殊配置都在這里,這就是又我們前面的聲明的 plugin 提供的。

defaultConfig就是程序的默認(rèn)配置,注意,如果在AndroidMainfest.xml里面定義了與這里相同的屬性,會以這里的為主。

這里最有必要要說明的是applicationId的選項:在我們曾經(jīng)定義的AndroidManifest.xml中,那里定義的包名有兩個用途:一個是作為程序的唯一識別ID,防止在同一手機(jī)裝兩個一樣的程序;另一個就是作為我們R資源類的包名。在以前我們修改這個ID會導(dǎo)致所有用引用R資源類的地方都要修改。但是現(xiàn)在我們?nèi)绻薷腶pplicationId只會修改當(dāng)前程序的ID,而不會去修改源碼中資源文件的引用。

buildTypes:定義了編譯類型,針對每個類型我們可以有不同的編譯配置,不同的編譯配置對應(yīng)的有不同的編譯命令。默認(rèn)的有debug、release 的類型。

dependencies:是屬于gradle 的依賴配置。它定義了當(dāng)前項目需要依賴的其他庫。

Gradle Wrapper

Gradle 不斷的在發(fā)展,新的版本難免會對以往的項目有一些向后兼容性的問題,這個時候,gradle wrapper就應(yīng)運(yùn)而生了。

gradlw wrapper 包含一些腳本文件和針對不同系統(tǒng)下面的運(yùn)行文件。wrapper 有版本區(qū)分,但是并不需要你手動去下載,當(dāng)你運(yùn)行腳本的時候,如果本地沒有會自動下載對應(yīng)版本文件。

在不同操作系統(tǒng)下面執(zhí)行的腳本不同,在 Mac 系統(tǒng)下執(zhí)行./gradlew ...,在windows 下執(zhí)行g(shù)radle.bat進(jìn)行編譯。

如果你是直接從eclipse 中的項目轉(zhuǎn)換過來的,程序并不會自動創(chuàng)建wrapper腳本,我們需要手動創(chuàng)建。在命令行輸入以下命令即可

gradlewrapper--gradle-version2.4

它會創(chuàng)建如下目錄結(jié)構(gòu):

wrapper 就是我們使用命令行編譯的開始。下面我們看看 wrapper 有什么樣的作用。

Gradle basics

Gradle 會根據(jù)build 文件的配置生成不同的task,我們可以直接單獨(dú)執(zhí)行每一個task。通過./gradlew tasks列出所有task。如果通過同時還想列出每個task 對應(yīng)依賴的其他task,可以使用./gradlew tasks -all。

其實每當(dāng)我們在Android Studio點擊 build,rebuild,clean菜單的時候,執(zhí)行的就是一些gradle task.

Android tasks

有四個基本的 task, Android 繼承他們分別進(jìn)行了自己的實現(xiàn):

assemble:對所有的 buildType 生成 apk 包。

clean:移除所有的編譯輸出文件,比如apk

check:執(zhí)行l(wèi)int檢測編譯。

build:同時執(zhí)行assemble和check命令

這些都是基本的命令,在實際項目中會根據(jù)不同的配置,會對這些task 設(shè)置不同的依賴。比如 默認(rèn)的 assmeble 會依賴 assembleDebug 和assembleRelease,如果直接執(zhí)行assmeble,最后會編譯debug,和release 的所有版本出來。如果我們只需要編譯debug 版本,我們可以運(yùn)行assembleDebug。

除此之外還有一些常用的新增的其他命令,比如 install命令,會將編譯后的apk 安裝到連接的設(shè)備。

我們運(yùn)行的許多命令除了會輸出到命令行,還會在build文件夾下生產(chǎn)一份運(yùn)行報告。比如check命令會生成lint-results.html.在build/outputs中。

Configuration

BuildConfig

這個類相信大家都不會陌生,我們最常用的用法就是通過BuildConfig.DEBUG來判斷當(dāng)前的版本是否是debug版本,如果是就會輸出一些只有在 debug 環(huán)境下才會執(zhí)行的操作。 這個類就是由gradle 根據(jù) 配置文件生成的。為什么gradle 可以直接生成一個Java 字節(jié)碼類,這就得益于我們的 gradle 的編寫語言是Groovy, Groovy 是一種 JVM 語言,JVM 語言的特征就是,雖然編寫的語法不一樣,但是他們最終都會編程 JVM 字節(jié)碼文件。同是JVM 語言的還有 Scala,Kotlin 等等。

這個功能非常強(qiáng)大,我們可以通過在這里設(shè)置一些key-value對,這些key-value 對在不同編譯類型的 apk 下的值不同,比如我們可以為debug 和release 兩種環(huán)境定義不同的服務(wù)器。比如:

除此之外,我們還可以為不同的編譯類型的設(shè)置不同的資源文件,比如:

Repositories

Repositories 就是代碼倉庫,這個相信大家都知道,我們平時的添加的一些 dependency 就是從這里下載的,Gradle 支持三種類型的倉庫:Maven,Ivy和一些靜態(tài)文件或者文件夾。在編譯的執(zhí)行階段,gradle 將會從倉庫中取出對應(yīng)需要的依賴文件,當(dāng)然,gradle 本地也會有自己的緩存,不會每次都去取這些依賴。

gradle 支持多種 Maven 倉庫,一般我們就是用共有的jCenter就可以了。

有一些項目,可能是一些公司私有的倉庫中的,這時候我們需要手動加入倉庫連接:

如果倉庫有密碼,也可以同時傳入用戶名和密碼

我們也可以使用相對路徑配置本地倉庫,我們可以通過配置項目中存在的靜態(tài)文件夾作為本地倉庫:

Dependencies

我們在引用庫的時候,每個庫名稱包含三個元素:組名:庫名稱:版本號,如下:

如果我們要保證我們依賴的庫始終處于最新狀態(tài),我們可以通過添加通配符的方式,比如:

但是我們一般不要這么做,這樣做除了每次編譯都要去做網(wǎng)絡(luò)請求查看是否有新版本導(dǎo)致編譯過慢外,最大的弊病在于我們使用過的版本很很困難是測試版,性能得不到保證,所以,在我們引用庫的時候一定要指名依賴版本。

Local dependencies

File dependencies

通過files()方法可以添加文件依賴,如果有很多jar文件,我們也可以通過fileTree()方法添加一個文件夾,除此之外,我們還可以通過通配符的方式添加,如下:

Native libraries

配置本地.so庫。在配置文件中做如下配置,然后在對應(yīng)位置建立文件夾,加入對應(yīng)平臺的.so文件。

文件結(jié)構(gòu)如下:

Library projects

如果我們要寫一個library項目讓其他的項目引用,我們的bubild.gradle的plugin 就不能是andrid plugin了,需要引用如下plugin

applyplugin:'com.android.library'

引用的時候在setting文件中include即可。

如果我們不方便直接引用項目,需要通過文件的形式引用,我們也可以將項目打包成aar文件,注意,這種情況下,我們在項目下面新建arrs文件夾,并在build.gradle 文件中配置 倉庫:

當(dāng)需要引用里面的某個項目時,通過如下方式引用:

Build Variants

在開發(fā)中我們可能會有這樣的需求:

我們需要在debug 和 release 兩種情況下配置不同的服務(wù)器地址;

當(dāng)打市場渠道包的時候,我們可能需要打免費(fèi)版、收費(fèi)版,或者內(nèi)部版、外部版的程序。

渠道首發(fā)包通常需要要求在歡迎頁添加渠道的logo。等等

為了讓市場版和debug版同時存在與一個手機(jī),我們需要編譯的時候自動給debug版本不一樣的包名。

這些需求都需要在編譯的時候動態(tài)根據(jù)當(dāng)前的編譯類型輸出不同樣式的apk文件。這時候就是我們的buildType大展身手的時候了。

Build Type

android 默認(rèn)的帶有Debug和Release兩種編譯類型。比如我們現(xiàn)在有一個新的statging的編譯類型

Source sets

每當(dāng)創(chuàng)建一個新的build type 的時候,gradle 默認(rèn)都會創(chuàng)建一個新的source set。我們可以建立與main文件夾同級的文件夾,根據(jù)編譯類型的不同我們可以選擇對某些源碼直接進(jìn)行替換。

除了代碼可以替換,我們的資源文件也可以替換

除此之外,不同編譯類型的項目,我們的依賴都可以不同,比如,如果我需要在staging和debug兩個版本中使用不同的log框架,我們這樣配置:

Product flavors

前面我們都是針對同一份源碼編譯同一個程序的不同類型,如果我們需要針對同一份源碼編譯不同的程序(包名也不同),比如 免費(fèi)版和收費(fèi)版。我們就需要Product flavors。

注意,Product flavorsBuild Type是不一樣的,而且他們的屬性也不一樣。所有的 product flavor 版本和defaultConfig 共享所有屬性!

像Build type 一樣,product flavor 也可以有自己的source set文件夾。除此之外,product flavor 和 build type 可以結(jié)合,他們的文件夾里面的文件優(yōu)先級甚至高于 單獨(dú)的built type 和product flavor 文件夾的優(yōu)先級。如果你想對于 blue類型的release 版本有不同的圖標(biāo),我們可以建立一個文件夾叫blueRelease,注意,這個順序不能錯,一定是 flavor+buildType 的形式。

更復(fù)雜的情況下,我們可能需要多個product 的維度進(jìn)行組合,比如我想要 color 和 price 兩個維度去構(gòu)建程序。這時候我們就需要使用flavorDimensions:

根據(jù)我們的配置,再次查看我們的task,發(fā)現(xiàn)多了這些task:

Resource merge priority

在Build Type中定義的資源優(yōu)先級最大,在Library 中定義的資源優(yōu)先級最低。

Signing configurations

如果我們打包市場版的時候,我們需要輸入我們的keystore數(shù)據(jù)。如果是debug 版本,系統(tǒng)默認(rèn)會幫我們配置這些信息。這些信息在gradle 中都配置在signingConfigs中。

配置之后我們需要在build type中直接使用

Optimize

Speeding up multimodule builds

可以通過以下方式加快gradle 的編譯:

開啟并行編譯:在項目根目錄下面的gradle.properties中設(shè)置

org.gradle.parallel=true

開啟編譯守護(hù)進(jìn)程:該進(jìn)程在第一次啟動后回一直存在,當(dāng)你進(jìn)行二次編譯的時候,可以重用該進(jìn)程。同樣是在gradle.properties中設(shè)置。

org.gradle.daemon=true

org.gradle.jvmargs=-Xms256m -Xmx1024m

Reducing apk file

在編譯的時候,我們可能會有很多資源并沒有用到,此時就可以通過shrinkResources來優(yōu)化我們的資源文件,除去那些不必要的資源。

如果我們需要查看該命令幫我們減少了多少無用的資源,我們也可以通過運(yùn)行shrinkReleaseResources命令來查看log.

某些情況下,一些資源是需要通過動態(tài)加載的方式載入的,這時候我也需要像 Progard 一樣對我們的資源進(jìn)行keep操作。方法就是在res/raw/下建立一個keep.xml文件,通過如下方式 keep 資源:

Manual shrinking

對一些特殊的文件或者文件夾,比如 國際化的資源文件、屏幕適配資源,如果我們已經(jīng)確定了某種型號,而不需要重新適配,我們可以直接去掉不可能會被適配的資源。這在為廠商適配機(jī)型定制app的時候是很用的。做法如下:

比如我們可能有非常多的國際化的資源,如果我們應(yīng)用場景只用到了English,Danish,Dutch的資源,我們可以直接指定我們的resConfig:

對于尺寸文件我們也可以這樣做

Profiling

當(dāng)我們執(zhí)行所有task的時候我們都可以通過添加--profile參數(shù)生成一份執(zhí)行報告在reports/profile中。示例如下:

我們可以通過這份報告看出哪個項目耗費(fèi)的時間最多,哪個環(huán)節(jié)耗費(fèi)的時間最多。

Practice

在開發(fā)的過程中,我們可能會遇到很多情況需要我們能夠自己定義task,在自定義task 之前,我們先簡單看看groovy 的語法。

Groovy

我們前面看到的那些build.gradle 配置文件,和xml 等的配置文件不同,這些文件可以說就是可以執(zhí)行的代碼,只是他們的結(jié)構(gòu)看起來通俗易懂,和配置文件沒什么兩樣,這也是Google 之所以選擇Groovy 的原因。除此之外,Groovy 是一門JVM 語言,也就是,Groovy 的代碼最終也會被編譯成JVM 字節(jié)碼,交給虛擬機(jī)去執(zhí)行,我們也可以直接反編譯這些字節(jié)碼文件。

我們這里簡單地說一下 groovy 一些語法。

變量

在groovy 中,沒有固定的類型,變量可以通過def關(guān)鍵字引用,比如:

defname='Andy'

我們通過單引號引用一串字符串的時候這個字符串只是單純的字符串,但是如果使用雙引號引用,在字符串里面還支持插值操作,

defname='Andy'defgreeting="Hello, $name!"

方法

類似 python 一樣,通過def關(guān)鍵字定義一個方法。方法如果不指定返回值,默認(rèn)返回最后一行代碼的值。

defsquare(defnum){? ? num * num}square4

Groovy 也是通過Groovy 定義一個類:

classMyGroovyClass{StringgreetingStringgetGreeting() {return'Hello!'}}

在Groovy 中,默認(rèn)所有的類和方法都是pulic的,所有類的字段都是private的;

和java一樣,我們通過new關(guān)鍵字得到類的實例,使用def接受對象的引用:def instance = new MyGroovyClass()

而且在類中聲明的字段都默認(rèn)會生成對應(yīng)的setter,getter方法。所以上面的代碼我們可以直接調(diào)用instance.setGreeting 'Hello, Groovy!',注意,groovy 的方法調(diào)用是可以沒有括號的,而且也不需要分號結(jié)尾。除此之外,我們甚至也可以直接調(diào)用;

我們可以直接通過instance.greeting這樣的方式拿到字段值,但其實這也會通過其get方法,而且不是直接拿到這個值。

map、collections

在 Groovy 中,定義一個列表是這樣的:

Listlist= [1,2,3,4,5]

遍歷一個列表是這樣的:

list.each() { element ->printlnelement}

定義一個 map 是這樣的:

MappizzaPrices = [margherita:10, pepperoni:12]

獲取一個map 值是這樣的:

pizzaPrices.get('pepperoni')pizzaPrices['pepperoni']

閉包

在Groovy 中有一個閉包的概念。閉包可以理解為就是 Java 中的匿名內(nèi)部類。閉包支持類似lamda形式的語法調(diào)用。如下:

def square = {num->num*num}square8

如果只有一個參數(shù),我們甚至可以省略這個參數(shù),默認(rèn)使用it作為參數(shù),最后代碼是這樣的:

Closuresquare = {it* it}square16

理解閉包的語法后,我們會發(fā)現(xiàn),其實在我們之前的配置文件里,android,dependencies這些后面緊跟的代碼塊,都是一個閉包而已。

Groovy in Gradle

了解完 groovy 的基本語法后,我們來看看 gradle 里面的代碼就好理解多了。

apply

applyplugin:'com.android.application'

這段代碼其實就是調(diào)用了project對象的apply方法,傳入了一個以plugin為key的map。完整寫出來就是這樣的:

project.apply([plugin:'com.android.application'])

實際調(diào)用的時候會傳入一個DependencyHandler的閉包,代碼如下:

Task

運(yùn)行該 task

./gradlew hello

注意:我們前面說過,gradle的生命周期分三步,初始化,配置和執(zhí)行。上面的代碼在配置過程就已經(jīng)執(zhí)行了,所以,打印出的字符串發(fā)生在該任務(wù)執(zhí)行之前,如果要在執(zhí)行階段才執(zhí)行任務(wù)中的代碼應(yīng)該如下設(shè)置:

添加Action:前面我們說過task 包含系列的action,當(dāng)task 被執(zhí)行的時候,所有的action 都會被依次執(zhí)行。如果我們要加入自己的action,我們可以通過復(fù)寫doFirst()和doLast()方法。

打印出來是這樣的:

Task 依賴:前面我們也說過,task 之間的關(guān)系就是依賴關(guān)系,關(guān)于Task 的依賴有兩種,must RunAfter和dependsOn。比如:

task task1 <<{ printfln=""'task1'=""}=""task=""task2=""<<{=""'task2'=""task2.mustRunAfter=""task1<=""code=""/>

task task1 <<{ printfln=""'task1'=""}=""task=""task2=""<<{=""'task2'=""task2.dependsOn=""task1<=""code=""/>

他們的區(qū)別是,運(yùn)行的的時候前者必須要都按順序加入gradlew task2 task1執(zhí)行才可以順利執(zhí)行,否則單獨(dú)執(zhí)行每個任務(wù),后者只需要執(zhí)行g(shù)radlew task2即可同時執(zhí)行兩個任務(wù)。

Practice

我們可以通過兩個例子來實踐task。

keystore 保護(hù)

這里直接將 store 的密碼明文寫在這里對于產(chǎn)品的安全性來說不太好,特別是如果該源碼開源,別人就可以用你的 id 去發(fā)布app。對于這種情況,我們需要構(gòu)建一個動態(tài)加載任務(wù),在編譯release 源碼的時候從本地文件(未加入git)獲取keystore 信息,如下:

你還可以設(shè)置一個保險措施,萬一我們的沒有找到對應(yīng)的文件需要用戶從控制臺輸入密碼

最后設(shè)置最終值

然后設(shè)置release 任務(wù)依賴于我們剛剛設(shè)置的任務(wù)

通過 hook Android 編譯插件 重命名 apk

最后編譯出來的apk 名字類似app-debug-1.0.apk。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容