Gradle 完整指南(Android)

本文謝絕轉載,非要轉載,請註明出處http://www.jianshu.com/p/9df3c3b6067a

前言

爲什麼需要學Gradle?

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

但是對於一些比較複雜的,特別是多人團隊合作的項目我們會需要一些個性化的配置來提高我們的開發效率。比如我們要自定義編譯出的apk包的名字、對於一些特殊產品我們可能會要用同一個項目編譯出免費版付費版的apk。這些高級的功能都需要我們對配置代碼進行自定義地修改。

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

爲什麼要了解命令行編譯?

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

瞭解 Gradle 之後我們可以做什麼?

we can do everything what we want.

  • 自定義編譯輸出文件格式。
  • hook Android 編譯過程。
  • 配置和改善 Gradle 編譯速度

Gralde Overview

History

我們知道,Android 的編譯過程非常複雜:

2897814-6dad3516eb020009.png

我們需要一種工具幫我們更快更方便更簡潔地完成 Android 程序的編譯。現在結合Android Studio 我們一般使用的工具都是Gradle, 在 Gradle 出現以前Android 也有對應的編譯工具叫 Ant,在Gradle 出現之後,也有新的編譯工具出現,就是FaceBook 的Buck工具。這些編譯工具在出現的時候幾乎都比 Gradle 要快,Gradle 之所以慢是跟它的編譯週期有很大關係。

Gradle 的編譯週期

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

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

在編譯過程中, Gradle 會根據 build 相關文件,聚合所有的projecttask,執行task 中的 action。因爲 build.gradle文件中的task非常多,先執行哪個後執行那個需要一種邏輯來保證。這種邏輯就是依賴邏輯,幾乎所有的Task 都需要依賴其他 task 來執行,沒有被依賴的task 會首先被執行。所以到最後所有的 Task 會構成一個 有向無環圖(DAG Directed Acyclic Graph)的數據結構。

編譯過程分爲三個階段:

  • 初始化階段:創建 Project 對象,如果有多個build.gradle,也會創建多個project.
  • 配置階段:在這個階段,會執行所有的編譯腳本,同時還會創建project的所有的task,爲後一個階段做準備。
  • 執行階段:在這個階段,gradle 會根據傳入的參數決定如何執行這些task,真正action的執行代碼就在這裏.

謝絕轉載,非要轉載,請註明出處http://www.jianshu.com/p/9df3c3b6067a

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

Gradle Files

對於一個gradle 項目,最基礎的文件配置如下:

2897814-cf10a6dfd2ff930a.png

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

  • setting.gradle:這個 setting 文件定義了哪些module 應該被加入到編譯過程,對於單個module 的項目可以不用需要這個文件,但是對於 multimodule 的項目我們就需要這個文件,否則gradle 不知道要加載哪些項目。這個文件的代碼在初始化階段就會被執行。
  • 頂層的build.gradle:頂層的build.gradle文件的配置最終會被應用到所有項目中。它典型的配置如下:
buildscript {
    repositories {
        jcenter()
    }

    dependencies {
        classpath 'com.android.tools.build:gradle:1.2.3'
    }
}

allprojects{
    repositories{
        jcenter()
    }
}
  • buildscript:定義了 Android 編譯工具的類路徑。repositories中,jCenter是一個著名的 Maven 倉庫。

  • allprojects:中定義的屬性會被應用到所有 module 中,但是爲了保證每個項目的獨立性,我們一般不會在這裏面操作太多共有的東西。

  • 每個項目單獨的 build.gradle:針對每個module 的配置,如果這裏的定義的選項和頂層build.gradle定義的相同,後者會被覆蓋。典型的 配置內容如下:

2897814-81f6400f4632357c.png
2897814-ff9adc07edc7ee72.png
  • apply plugin:第一行代碼應用了Android 程序的gradle插件,作爲Android 的應用程序,這一步是必須的,因爲plugin中提供了Android 編譯、測試、打包等等的所有task。
  • android:這是編譯文件中最大的代碼塊,關於android 的所有特殊配置都在這裏,這就是又我們前面的聲明的 plugin 提供的。
    • defaultConfig就是程序的默認配置,注意,如果在AndroidMainfest.xml裏面定義了與這裏相同的屬性,會以這裏的爲主。
    • 這裏最有必要要說明的是applicationId的選項:在我們曾經定義的AndroidManifest.xml中,那裏定義的包名有兩個用途:一個是作爲程序的唯一識別ID,防止在同一手機裝兩個一樣的程序;另一個就是作爲我們R資源類的包名。在以前我們修改這個ID會導致所有用引用R資源類的地方都要修改。但是現在我們如果修改applicationId只會修改當前程序的ID,而不會去修改源碼中資源文件的引用。
  • buildTypes:定義了編譯類型,針對每個類型我們可以有不同的編譯配置,不同的編譯配置對應的有不同的編譯命令。默認的有debug、release 的類型。
  • dependencies:是屬於gradle 的依賴配置。它定義了當前項目需要依賴的其他庫。

Gradle Wrapper

Gradle 不斷的在發展,新的版本難免會對以往的項目有一些向後兼容性的問題,這個時候,gradle wrapper就應運而生了。

gradlw wrapper 包含一些腳本文件和針對不同系統下面的運行文件。wrapper 有版本區分,但是並不需要你手動去下載,當你運行腳本的時候,如果本地沒有會自動下載對應版本文件。

在不同操作系統下面執行的腳本不同,在 Mac 系統下執行./gradlew ...,在windows 下執行gradle.bat進行編譯。

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

gradle wrapper --gradle-version 2.4

它會創建如下目錄結構:

2897814-dd93da461a165be3.png

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

Gradle basics

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

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

Android tasks

有四個基本的 task, Android 繼承他們分別進行了自己的實現:

  • assemble:對所有的 buildType 生成 apk 包。
  • clean:移除所有的編譯輸出文件,比如apk
  • check:執行lint檢測編譯。
  • build:同時執行assemblecheck命令

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

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

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

Configuration

BuildConfig

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

這個功能非常強大,我們可以通過在這裏設置一些key-value對,這些key-value 對在不同編譯類型的 apk 下的值不同,比如我們可以爲debug 和release 兩種環境定義不同的服務器。比如:

2897814-9e801701cf75bbaa.png

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

2897814-d24d796859ea6710.png

Repositories

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

gradle 支持多種 Maven 倉庫,一般我們就是用共有的jCenter就可以了。
有一些項目,可能是一些公司私有的倉庫中的,這時候我們需要手動加入倉庫連接:

2897814-ab5eff88646d3b1c.png

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

2897814-95afad6c6bdc7f4d.png

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

2897814-195c8955d4add085.png

Dependencies

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

2897814-7fedf7fd5c8c2ee6.png

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

2897814-14fc6f6b001b4ce6.png

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

Local dependencies

File dependencies

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

2897814-faba5c7c1e9eefa6.png

Native libraries

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

2897814-7933ea9f6c29abe0.png

文件結構如下:

2897814-78c4ad17028dfd4c.png

Library projects

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

apply plugin: 'com.android.library'

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

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

2897814-d04de5931b3d9cee.png

當需要引用裏面的某個項目時,通過如下方式引用:

2897814-073185b041380072.png

Build Variants

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

  • 我們需要在debug 和 release 兩種情況下配置不同的服務器地址;
  • 當打市場渠道包的時候,我們可能需要打免費版、收費版,或者內部版、外部版的程序。
  • 渠道首發包通常需要要求在歡迎頁添加渠道的logo。等等
  • 爲了讓市場版和debug版同時存在與一個手機,我們需要編譯的時候自動給debug版本不一樣的包名。

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

Build Type

android 默認的帶有Debug和Release兩種編譯類型。比如我們現在有一個新的statging的編譯類型

2897814-762bcbd41e57e91f.png

Source sets

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

2897814-9fd4affd4dd89052.png

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

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

2897814-b008946b8159c9ab.png

Product flavors

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

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

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

更復雜的情況下,我們可能需要多個product 的維度進行組合,比如我想要 color 和 price 兩個維度去構建程序。這時候我們就需要使用flavorDimensions

2897814-78679075ac33b2f1.png
2897814-55823134a39af482.png

根據我們的配置,再次查看我們的task,發現多了這些task:

2897814-3954856a8616066c.png

Resource merge priority

2897814-ba7ca2908af1ba50.png

在Build Type中定義的資源優先級最大,在Library 中定義的資源優先級最低。

Signing configurations

如果我們打包市場版的時候,我們需要輸入我們的keystore數據。如果是debug 版本,系統默認會幫我們配置這些信息。這些信息在gradle 中都配置在signingConfigs中。

2897814-78c4c7ed3934d4a8.png

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

2897814-fdd29c1debdc9d0a.png

Optimize

Speeding up multimodule builds

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

  • 開啓並行編譯:在項目根目錄下面的 gradle.properties中設置
org.gradle.parallel=true
  • 開啓編譯守護進程:該進程在第一次啓動後回一直存在,當你進行二次編譯的時候,可以重用該進程。同樣是在gradle.properties中設置。
org.gradle.daemon=true
  • 加大可用編譯內存:
org.gradle.jvmargs=-Xms256m -Xmx1024m

Reducing apk file

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

2897814-372cba87ab5ef33a.png

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

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

2897814-e17cd84293ab1fea.png

Manual shrinking

對一些特殊的文件或者文件夾,比如 國際化的資源文件、屏幕適配資源,如果我們已經確定了某種型號,而不需要重新適配,我們可以直接去掉不可能會被適配的資源。這在爲廠商適配機型定製app的時候是很用的。做法如下:
比如我們可能有非常多的國際化的資源,如果我們應用場景只用到了English,Danish,Dutch的資源,我們可以直接指定我們的resConfig:

2897814-a69f416cdfeb0a92.png

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

2897814-c28a41426f664fc1.png

Profiling

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

2897814-deab70514ebf85a5.png

我們可以通過這份報告看出哪個項目耗費的時間最多,哪個環節耗費的時間最多。

Practice

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

Groovy

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

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

變量

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

def name = 'Andy'

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

def name = 'Andy'
def greeting = "Hello, $name!"

方法

類似 python 一樣,通過def關鍵字定義一個方法。方法如果不指定返回值,默認返回最後一行代碼的值。

def square(def num) {
    num * num
}
square 4

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

class MyGroovyClass {
    String greeting
    String getGreeting() {
        return 'Hello!'
    }
}
  • 在Groovy 中,默認所有的類和方法都是pulic的,所有類的字段都是private的;
  • 和java一樣,我們通過new關鍵字得到類的實例,使用def接受對象的引用:def instance = new MyGroovyClass()
  • 而且在類中聲明的字段都默認會生成對應的setter,getter方法。所以上面的代碼我們可以直接調用instance.setGreeting 'Hello, Groovy!'注意,groovy 的方法調用是可以沒有括號的,而且也不需要分號結尾。除此之外,我們甚至也可以直接調用;
  • 我們可以直接通過instance.greeting這樣的方式拿到字段值,但其實這也會通過其get方法,而且不是直接拿到這個值。

map、collections

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

List list = [1, 2, 3, 4, 5]

遍歷一個列表是這樣的:

list.each() { element ->

println element

}

定義一個 map 是這樣的:

Map pizzaPrices = [margherita:10, pepperoni:12]

獲取一個map 值是這樣的:

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

閉包

在Groovy 中有一個閉包的概念。閉包可以理解爲就是 Java 中的匿名內部類。閉包支持類似lamda形式的語法調用。如下:

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

如果只有一個參數,我們甚至可以省略這個參數,默認使用it作爲參數,最後代碼是這樣的:

Closure square = {
    it * it
}
square 16

理解閉包的語法後,我們會發現,其實在我們之前的配置文件裏,android,dependencies這些後面緊跟的代碼塊,都是一個閉包而已。

Groovy in Gradle

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

  • apply
apply plugin: 'com.android.application'

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

project.apply([plugin: 'com.android.application'])
  • dependencies
    我們看到的是這樣:
2897814-87a05c0d5613d1e3.png

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

2897814-adef01f8eda6e041.png

Task

  • 創建一個task
2897814-db3ddfdfc28632af.png

運行該 task

./gradlew hello

注意:我們前面說過,gradle的生命週期分三步,初始化,配置和執行。上面的代碼在配置過程就已經執行了,所以,打印出的字符串發生在該任務執行之前,如果要在執行階段才執行任務中的代碼應該如下設置:

2897814-dd229fc1bcc0bbab.png
  • 添加Action:前面我們說過task 包含系列的action,當task 被執行的時候,所有的action 都會被依次執行。如果我們要加入自己的action,我們可以通過複寫doFirst()doLast()方法。
2897814-6c3acf7d93c9e3c9.png

打印出來是這樣的:

2897814-524d97849e6ad82d.png
  • Task 依賴:前面我們也說過,task 之間的關係就是依賴關係,關於Task 的依賴有兩種,mustRunAfterdependsOn。比如:
task task1 <<{
    printfln 'task1'
}

task task2 <<{
    printfln 'task2'
}
task2.mustRunAfter task1

task task1 <<{
    printfln 'task1'
}

task task2 <<{
    printfln 'task2'
}
task2.dependsOn task1

他們的區別是,運行的的時候前者必須要都按順序加入gradlew task2 task1執行纔可以順利執行,否則只會單獨執行每個任務。而後者只需要執行gradlew task2即可同時執行兩個任務。

Practice

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

keystore 保護

2897814-f8eae0b3849b9aa0.png

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

2897814-135d62a6c7a3b730.png

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

2897814-d2199772db725a14.png

最後設置最終值

然後設置release 任務依賴於我們剛剛設置的任務

2897814-69ee80842e9df17a.png

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

2897814-16722d582308d396.png
2897814-ae3a0ca73beaf9e9.png

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

參考文獻

  • Gadle For Android

如果你喜歡,歡迎閱讀我的其他文章:完美搭建 shadowsocks 和 VPN 服務器,親測有效!

---------------------2017-10-14----update--------------------------

FAQ

下面會列出大家在評論裏熱點的問題以及解決方案,我會經常更新

  • Flavor 和 Main 相同類編譯報錯的問題


    2897814-7dedb2d140a1cd24.png
    image.png
  • 顯示 task 依賴

2897814-7a33c880ba621eee.png
image.png
發佈了35 篇原創文章 · 獲贊 2 · 訪問量 4374
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章