鍍金池/ 教程/ Android/ Gradle 編程模型及 API 實例詳解
Groovy 介紹
腳本類、文件 I/O 和 XML 操作
更多
一些前提知識
Gradle 工作流程
基本組件
題外話
總結
Gradle 編程模型及 API 實例詳解
閉包
Gradle 介紹
閑言構建
Groovy 中的數(shù)據(jù)類型

Gradle 編程模型及 API 實例詳解

Gradle 編程模型及 API 實例詳解

希望你在進入此節(jié)之前,一定花時間把前面內(nèi)容看一遍?。?!

https://docs.gradle.org/current/dsl/ <==這個文檔很重要

Gradle 基于 Groovy,Groovy 又基于 Java。所以,Gradle 執(zhí)行的時候和 Groovy 一樣,會把腳本轉換成 Java 對象。Gradle 主要有三種對象,這三種對象和三種不同的腳本文件對應,在 gradle 執(zhí)行的時候,會將腳本轉換成對應的對端:

  • Gradle 對象:當我們執(zhí)行 gradle xxx 或者什么的時候,gradle 會從默認的配置腳本中構造出一個 Gradle 對象。在整個執(zhí)行過程中,只有這么一個對象。Gradle 對象的數(shù)據(jù)類型就是 Gradle。我們一般很少去定制這個默認的配置腳本。
  • Project 對象:每一個 build.gradle 會轉換成一個 Project 對象。
  • Settings 對象:顯然,每一個 settings.gradle 都會轉換成一個 Settings 對象。

注意,對于其他 gradle 文件,除非定義了 class,否則會轉換成一個實現(xiàn)了 Script 接口的對象。這一點和 3.5 節(jié)中 Groovy 的腳本類相似

當我們執(zhí)行 gradle 的時候,gradle 首先是按順序解析各個 gradle 文件。這里邊就有所所謂的生命周期的問題,即先解析誰,后解析誰。圖 27 是 Gradle 文檔中對生命周期的介紹:結合上一節(jié)的內(nèi)容,相信大家都能看明白了。現(xiàn)在只需要看紅框里的內(nèi)容:

http://wiki.jikexueyuan.com/project/deep-android-gradle/images/29.jpg" alt="" />

Gradle 對象

我們先來看 Gradle 對象,它有哪些屬性呢?如圖 28 所示:

http://wiki.jikexueyuan.com/project/deep-android-gradle/images/30.jpg" alt="" />

我在 posdevice build.gradle 中和 settings.gradle 中分別加了如下輸出:

//在 settings.gradle 中,則輸出"In settings,gradle id is"
println "In posdevice, gradle id is " + gradle.hashCode() 
println "Home Dir:" + gradle.gradleHomeDir
println "User Home Dir:" + gradle.gradleUserHomeDir
println "Parent: " + gradle.parent

得到結果如圖 29 所示:

http://wiki.jikexueyuan.com/project/deep-android-gradle/images/31.jpg" alt="" />

  • 你看,在 settings.gradle 和 posdevice build.gradle 中,我們得到的 gradle 實例對象的 hashCode 是一樣的(都是 791279786)。
  • HomeDir 是我在哪個目錄存儲的 gradle 可執(zhí)行程序。
  • User Home Dir:是 gradle 自己設置的目錄,里邊存儲了一些配置文件,以及編譯過程中的緩存文件,生成的類文件,編譯中依賴的插件等等。

Gradle 的函數(shù)接口在文檔中也有。

Project 對象

每一個 build.gradle 文件都會轉換成一個 Project 對象。在 Gradle 術語中,Project 對象對應的是 Build Script。

Project 包含若干 Tasks。另外,由于 Project 對應具體的工程,所以需要為 Project 加載所需要的插件,比如為 Java 工程加載 Java 插件。其實,一個 Project 包含多少 Task 往往是插件決定的。

所以,在 Project 中,我們要:

  • 加載插件。
  • 不同插件有不同的行話,即不同的配置。我們要在 Project 中配置好,這樣插件就知道從哪里讀取源文件等
  • 設置屬性。

1.加載插件

Project 的 API 位于 https://docs.gradle.org/current/javadoc/org/gradle/api/Project.html。加載插件是調(diào)用它的 apply 函數(shù).apply 其實是 Project 實現(xiàn)的 PluginAware 接口定義的:

http://wiki.jikexueyuan.com/project/deep-android-gradle/images/32.jpg" alt="" />

來看代碼:

[apply 函數(shù)的用法] apply 是一個函數(shù),此處調(diào)用的是圖 30 中最后一個 apply 函數(shù)。注意,Groovy 支持

函數(shù)調(diào)用的時候通過 參數(shù)名 1:參數(shù)值 2,參數(shù)名 2:參數(shù)值 2 的方式來傳遞參數(shù)

apply plugin: 'com.android.library' <==如果是編譯 Library,則加載此插件

apply plugin: 'com.android.application' <==如果是編譯 Android APP,則加載此插件

除了加載二進制的插件(上面的插件其實都是下載了對應的 jar 包,這也是通常意義上我們所理解的插件),還可以加載一個 gradle 文件。為什么要加載 gradle 文件呢?

其實這和代碼的模塊劃分有關。一般而言,我會把一些通用的函數(shù)放到一個名叫 utils.gradle 文件里。然后在其他工程的 build.gradle 來加載這個 utils.gradle。這樣,通過一些處理,我就可以調(diào)用 utils.gradle 中定義的函數(shù)了。

加載 utils.gradle 插件的代碼如下:

utils.gradle 是我封裝的一個 gradle 腳本,里邊定義了一些方便函數(shù),比如讀取 AndroidManifest.xml 中

的 versionName,或者是 copy jar 包/APK 包到指定的目錄

apply from: rootProject.getRootDir().getAbsolutePath() + "/utils.gradle"

也是使用 apply 的最后一個函數(shù)。那么,apply 最后一個函數(shù)到底支持哪些參數(shù)呢?還是得看圖 31 中的 API 說明:

http://wiki.jikexueyuan.com/project/deep-android-gradle/images/33.jpg" alt="" />

我這里不遺余力的列出 API 圖片,就是希望大家在寫腳本的時候,碰到不會的,一定要去查看 API 文檔!

2.設置屬性

如果是單個腳本,則不需要考慮屬性的跨腳本傳播,但是 Gradle 往往包含不止一個 build.gradle 文件,比如我設置的 utils.gradle,settings.gradle。如何在多個腳本中設置屬性呢?

Gradle 提供了一種名為 extra property 的方法。extra property 是額外屬性的意思,在第一次定義該屬性的時候需要通過 ext 前綴來標示它是一個額外的屬性。定義好之后,后面的存取就不需要 ext 前綴了。ext 屬性支持 Project 和 Gradle 對象。即 Project 和 Gradle 對象都可以設置 ext 屬性

舉個例子:

我在 settings.gradle 中想為 Gradle 對象設置一些外置屬性,所以在 initMinshengGradleEnvironment 函數(shù)中

def initMinshengGradleEnvironment(){
    //屬性值從 local.properites 中讀取  
    Properties properties = new Properties()
    File propertyFile = new File(rootDir.getAbsolutePath() + "/local.properties")
    properties.load(propertyFile.newDataInputStream())
    //gradle 就是 gradle 對象。它默認是 Settings 和 Project 的成員變量。可直接獲取  

   //ext 前綴,表明操作的是外置屬性。api 是一個新的屬性名。前面說過,只在  
   //第一次定義或者設置它的時候需要 ext 前綴  
    gradle.ext.api = properties.getProperty('sdk.api')

    println gradle.api  //再次存取 api 的時候,就不需要 ext 前綴了  
    ......
    }

再來一個例子強化一下:

我在 utils.gradle 中定義了一些函數(shù),然后想在其他 build.gradle 中調(diào)用這些函數(shù)。那該怎么做呢?

[utils.gradle]
//utils.gradle 中定義了一個獲取 AndroidManifests.xml versionName 的函數(shù)  
def  getVersionNameAdvanced(){
下面這行代碼中的 project 是誰?  

   def xmlFile = project.file("AndroidManifest.xml") 
   def rootManifest = new XmlSlurper().parse(xmlFile)
   return rootManifest['@android:versionName']   
}
//現(xiàn)在,想把這個 API 輸出到各個 Project。由于這個 utils.gradle 會被每一個 Project Apply,所以  

//我可以把 getVersionNameAdvanced 定義成一個 closure,然后賦值到一個外部屬性  
下面的 ext 是誰的 ext?  
ext{ //此段花括號中代碼是閉包  
    //除了 ext.xxx=value 這種定義方法外,還可以使用 ext{}這種書寫方法。  

    //ext{}不是 ext(Closure)對應的函數(shù)調(diào)用。但是 ext{}中的{}確實是閉包。  
    getVersionNameAdvanced = this.&getVersionNameAdvanced
 }

上面代碼中有兩個問題:

project 是誰?

ext 是誰的 ext?

上面兩個問題比較關鍵,我也是花了很長時間才搞清楚。這兩個問題歸結到一起,其實就是:

加載 utils.gradle 的 Project 對象和 utils.gradle 本身所代表的 Script 對象到底有什么關系?

我們在 Groovy 中也講過怎么在一個 Script 中 import 另外一個 Script 中定義的類或者函數(shù)(見 3.5 腳本類、文件 I/O 和 XML 操作一節(jié))。在 Gradle 中,這一塊的處理比 Groovy 要復雜,具體怎么搞我還沒完全弄清楚,但是 Project 和 utils.gradle 對于的 Script 的對象的關系是:

  • 當一個 Project apply 一個 gradle 文件的時候,這個 gradle 文件會轉換成一個 Script 對象。這個,相信大家都已經(jīng)知道了。

  • Script 中有一個 delegate 對象,這個 delegate 默認是加載(即調(diào)用 apply)它的 Project 對象。但是,在 apply 函數(shù)中,有一個 from 參數(shù),還有一個 to 參數(shù)(參考圖 31)。通過 to 參數(shù),你可以把 delegate 對象指定為別的東西。

  • delegate 對象是什么意思?當你在 Script 中操作一些不是 Script 自己定義的變量,或者函數(shù)時候,gradle 會到 Script 的 delegate 對象去找,看看有沒有定義這些變量或函數(shù)。

現(xiàn)在你知道問題 1,2 和答案了:

問題 1:project 就是加載 utils.gradle 的 project。由于 posdevice 有 5 個 project,所以 utils.gradle 會分別加載到 5 個 project 中。所以,getVersionNameAdvanced 才不用區(qū)分到底是哪個 project。反正一個 project 有一個 utils.gradle 對應的 Script。

問題 2:ext:自然就是 Project 對應的 ext 了。此處為 Project 添加了一些 closure。那么,在 Project 中就可以調(diào)用 getVersionNameAdvanced 函數(shù)了

比如:我在 posdevice 每個 build.gradle 中都有如下的代碼:

tasks.getByName("assemble"){
    it.doLast{
        println "$project.name: After assemble, jar libs are copied to local repository"
        copyOutput(true)  //copyOutput 是 utils.gradle 輸出的 closure
     }
}

通過這種方式,我將一些常用的函數(shù)放到 utils.gradle 中,然后為加載它的 Project 設置 ext 屬性。最后,Project 中就可以調(diào)用這種賦值函數(shù)了!

注意:此處我研究的還不是很深,而且我個人感覺:

1 在 Java 和 Groovy 中:我們會把常用的函數(shù)放到一個輔助類和公共類中,然后在別的地方 import 并調(diào)用它們。

2 但是在 Gradle,更正規(guī)的方法是在 xxx.gradle 中定義插件。然后通過添加 Task 的方式來完成工作。gradle 的 user guide 有詳細介紹如何實現(xiàn)自己的插件。

  1. Task 介紹

Task 是 Gradle 中的一種數(shù)據(jù)類型,它代表了一些要執(zhí)行或者要干的工作。不同的插件可以添加不同的 Task。每一個 Task 都需要和一個 Project 關聯(lián)。

Task 的 API 文檔位于 https://docs.gradle.org/current/dsl/org.gradle.api.Task.html。關于 Task,我這里簡單介紹下 build.gradle 中怎么寫它,以及 Task 中一些常見的類型

關于 Task。來看下面的例子:

[build.gradle]
//Task 是和 Project 關聯(lián)的,所以,我們要利用 Project 的 task 函數(shù)來創(chuàng)建一個 Task
task myTask  <==myTask 是新建 Task 的名字  

task myTask { configure closure }
task myType << { task action }  <==注意,<<符號 是 doLast 的縮寫  

task myTask(type: SomeType)
task myTask(type: SomeType) { configure closure }

上述代碼中都用了 Project 的一個函數(shù),名為 task,注意:

  • 一個 Task 包含若干 Action。所以,Task 有 doFirst 和 doLast 兩個函數(shù),用于添加需要最先執(zhí)行的 Action 和需要和需要最后執(zhí)行的 Action。Action 就是一個閉包。
  • Task 創(chuàng)建的時候可以指定 Type,通過 type:名字表達。這是什么意思呢?其實就是告訴 Gradle,這個新建的 Task 對象會從哪個基類 Task 派生。比如,Gradle 本身提供了一些通用的 Task,最常見的有 Copy 任務。Copy 是 Gradle 中的一個類。當我們:task myTask(type:Copy)的時候,創(chuàng)建的 Task 就是一個 Copy Task。
  • 當我們使用 task myTask{ xxx}的時候?;ɡㄌ柺且粋€ closure。這會導致 gradle 在創(chuàng)建這個 Task 之后,返回給用戶之前,會先執(zhí)行 closure 的內(nèi)容。
  • 當我們使用 task myTask << {xxx}的時候,我們創(chuàng)建了一個 Task 對象,同時把 closure 做為一個 action 加到這個 Task 的 action 隊列中,并且告訴它“最后才執(zhí)行這個 closure”(注意,<<符號是 doLast 的代表)。

圖 32 是 Project 中關于 task 函數(shù)說明:

http://wiki.jikexueyuan.com/project/deep-android-gradle/images/34.jpg" alt="" />

陸陸續(xù)續(xù)講了這么些內(nèi)容,我自己感覺都有點煩了。是得,Gradle 用一整本書來講都嫌不夠呢。

anyway,到目前為止,我介紹的都是一些比較基礎的東西,還不是特別多。但是后續(xù)例子該涉及到的知識點都有了。下面我們直接上例子。這里有兩個例子:

  • posdevice 的例子
  • 另外一個是單個 project 的例子

posdevice 實例

現(xiàn)在正是開始通過例子來介紹怎么玩 gradle。這里要特別強調(diào)一點,根據(jù) Gradle 的哲學。gradle 文件中包含一些所謂的 Script Block(姑且這么稱它)。Script Block 作用是讓我們來配置相關的信息。不同的 SB 有不同的需要配置的東西。這也是我最早說的行話。比如,源碼對應的 SB,就需要我們配置源碼在哪個文件夾里。關于 SB,我們后面將見識到!

posdevice 是一個 multi project。下面包含 5 個 Project。對于這種 Project,請大家回想下我們該創(chuàng)建哪些文件?

  • settings.gradle 是必不可少的
  • 根目錄下的 build.gradle。這個我們沒講過,因為 posdevice 的根目錄本身不包含代碼,而是包含其他 5 個子 project。
  • 每個 project 目錄下包含對于的 build.gradle
  • 另外,我把常用的函數(shù)封裝到一個名為 utils.gradle 的腳本里了。

馬上一個一個來看它們。

  1. utils.gradle utils.gradle 是我自己加的,為我們團隊特意加了一些常見函數(shù)。主要代碼如下:
[utils.gradle]
import groovy.util.XmlSlurper  //解析 XML 時候要引入這個 groovy 的 package

def copyFile(String srcFile,dstFile){
     ......//拷貝文件函數(shù),用于將最后的生成物拷貝到指定的目錄  

}
def rmFile(String targetFile){
    .....//刪除指定目錄中的文件  

}
def cleanOutput(boolean bJar = true){
    ....//clean 的時候清理  

}

def copyOutput(boolean bJar = true){
    ....//copyOutput 內(nèi)部會調(diào)用 copyFile 完成一次 build 的產(chǎn)出物拷貝  

}

def getVersionNameAdvanced(){//老朋友  

   def xmlFile = project.file("AndroidManifest.xml")
   def rootManifest = new XmlSlurper().parse(xmlFile)
   return rootManifest['@android:versionName']   
}

//對于 android library 編譯,我會 disable 所有的 debug 編譯任務  

def disableDebugBuild(){
  //project.tasks 包含了所有的 tasks,下面的 findAll 是尋找那些名字中帶 debug 的 Task。  

  //返回值保存到 targetTasks 容器中  

  def targetTasks = project.tasks.findAll{task ->
      task.name.contains("Debug")
  }
  //對滿足條件的 task,設置它為 disable。如此這般,這個 Task 就不會被執(zhí)行  

  targetTasks.each{
     println "disable debug task  : ${it.name}"
     it.setEnabled false
  }
}
//將函數(shù)設置為 extra 屬性中去,這樣,加載 utils.gradle 的 Project 就能調(diào)用此文件中定義的函數(shù)了  

ext{
    copyFile = this.&copyFile
    rmFile = this.&rmFile
    cleanOutput = this.&cleanOutput
    copyOutput = this.&copyOutput
    getVersionNameAdvanced = this.&getVersionNameAdvanced
    disableDebugBuild = this.&disableDebugBuild
}

圖 33 展示了被 disable 的 Debug 任務的部分信息:

http://wiki.jikexueyuan.com/project/deep-android-gradle/images/35.jpg" alt="" />

  1. settings.gradle

這個文件中我們該干什么?調(diào)用 include 把需要包含的子 Project 加進來。代碼如下:

[settings.gradle]
/*我們團隊內(nèi)部建立的編譯環(huán)境初始化函數(shù)  

  這個函數(shù)的目的是  

  1  解析一個名為 local.properties 的文件,讀取 Android SDK 和 NDK 的路徑  

  2  獲取最終產(chǎn)出物目錄的路徑。這樣,編譯完的 apk 或者 jar 包將拷貝到這個最終產(chǎn)出物目錄中  

  3 獲取 Android SDK 指定編譯的版本  

*/
def initMinshengGradleEnvironment(){
    println "initialize Minsheng Gradle Environment ....."
    Properties properties = new Properties()
   //local.properites 也放在 posdevice 目錄下  

    File propertyFile = new File(rootDir.getAbsolutePath() + "/local.properties")
    properties.load(propertyFile.newDataInputStream())
    /*
      根據(jù) Project、Gradle 生命周期的介紹,settings 對象的創(chuàng)建位于具體 Project 創(chuàng)建之前  

      而 Gradle 底對象已經(jīng)創(chuàng)建好了。所以,我們把 local.properties 的信息讀出來后,通過  

      extra 屬性的方式設置到 gradle 對象中  

      而具體 Project 在執(zhí)行的時候,就可以直接從 gradle 對象中得到這些屬性了!  

    */
    gradle.ext.api = properties.getProperty('sdk.api')
    gradle.ext.sdkDir = properties.getProperty('sdk.dir')
     gradle.ext.ndkDir = properties.getProperty('ndk.dir')
     gradle.ext.localDir = properties.getProperty('local.dir')
    //指定 debug keystore 文件的位置,debug 版 apk 簽名的時候會用到  

    gradle.ext.debugKeystore = properties.getProperty('debug.keystore')
     ......
    println "initialize Minsheng Gradle Environment completes..."
}
//初始化  
initMinshengGradleEnvironment()
//添加子 Project 信息  
include 'CPosSystemSdk' , 'CPosDeviceSdk' , 'CPosSdkDemo','CPosDeviceServerApk', 'CPosSystemSdkWizarPosImpl'

注意,對于 Android 來說,local.properties 文件是必須的,它的內(nèi)容如下:

[local.properties]
local.dir=/home/innost/workspace/minsheng-flat-dir/
//注意,根據(jù) Android Gradle 的規(guī)范,只有下面兩個屬性是必須的,其余都是我自己加的  

sdk.dir=/home/innost/workspace/android-aosp-sdk/
ndk.dir=/home/innost/workspace/android-aosp-ndk/
debug.keystore=/home/innost/workspace/tools/mykeystore.jks
sdk.api=android-19

再次強調(diào),sdk.dir 和 ndk.dir 是 Android Gradle 必須要指定的,其他都是我自己加的屬性。當然。不編譯 ndk,就不需要 ndk.dir 屬性了。

3.posdevice build.gradle

作為 multi-project 根目錄,一般情況下,它的 build.gradle 是做一些全局配置。來看我的 build.gradle

[posdevice build.gradle]
//下面這個 subprojects{}就是一個 Script Block
subprojects {
  println "Configure for $project.name"  //遍歷子 Project,project 變量對應每個子 Project
  buildscript {  //這也是一個 SB
    repositories { //repositories 是一個 SB
        ///jcenter 是一個函數(shù),表示編譯過程中依賴的庫,所需的插件可以在 jcenter 倉庫中  

       //下載。 
        jcenter()
    }
    dependencies { //SB
        //dependencies 表示我們編譯的時候,依賴 android 開發(fā)的 gradle 插件。插件對應的  

       //class path 是 com.android.tools.build。版本是 1.2.3
        classpath 'com.android.tools.build:gradle:1.2.3'
    }
   //為每個子 Project 加載 utils.gradle 。當然,這句話可以放到 buildscript 花括號之后  

   apply from: rootProject.getRootDir().getAbsolutePath() + "/utils.gradle"
 }//buildscript 結束  

}

感覺解釋得好蒼白,SB 在 Gradle 的 API 文檔中也是有的。先來看 Gradle 定義了哪些 SB。如圖 34 所示:

http://wiki.jikexueyuan.com/project/deep-android-gradle/images/36.jpg" alt="" />

你看,subprojects、dependencies、repositories 都是 SB。那么 SB 到底是什么?它是怎么完成所謂配置的呢?

仔細研究,你會發(fā)現(xiàn) SB 后面都需要跟一個花括號,而花括號,恩,我們感覺里邊可能一個 Closure。由于圖 34 說,這些 SB 的 Description 都有“Configure xxx for this project”,所以很可能 subprojects 是一個函數(shù),然后其參數(shù)是一個 Closure。是這樣的嗎?

Absolutely right。只是這些函數(shù)你直接到 Project API 里不一定能找全。不過要是你好奇心重,不妨到 https://docs.gradle.org/current/javadoc/,選擇 Index 這一項,然后 ctrl+f,輸入圖 34 中任何一個 Block,你都會找到對應的函數(shù)。比如我替你找了幾個 API,如圖 35 所示:

http://wiki.jikexueyuan.com/project/deep-android-gradle/images/37.jpg" alt="" />

特別提示:當你下次看到一個不認識的 SB 的時候,就去看 API 吧。

下面來解釋代碼中的各個 SB:

  • subprojects:它會遍歷 posdevice 中的每個子 Project。在它的 Closure 中,默認參數(shù)是子 Project 對應的 Project 對象。由于其他 SB 都在 subprojects 花括號中,所以相當于對每個 Project 都配置了一些信息。

  • buildscript:它的 closure 是在一個類型為 ScriptHandler 的對象上執(zhí)行的。主意用來所依賴的 classpath 等信息。通過查看 ScriptHandler API 可知,在 buildscript SB 中,你可以調(diào)用 ScriptHandler 提供的 repositories(Closure )、dependencies(Closure)函數(shù)。這也是為什么 repositories 和 dependencies 兩個 SB 為什么要放在 buildscript 的花括號中的原因。明白了?這就是所謂的行話,得知道規(guī)矩。不知道規(guī)矩你就亂了。記不住規(guī)矩,又不知道查 SDK,那么就徹底抓瞎,只能到網(wǎng)上到處找答案了!

  • 關于 repositories 和 dependencies,大家直接看 API 吧。后面碰到了具體代碼我們再來介紹

4.CPosDeviceSdk build.gradle

CPosDeviceSdk 是一個 Android Library。按 Google 的想法,Android Library 編譯出來的應該是一個 AAR 文件。但是我的項目有些特殊,我需要發(fā)布 CPosDeviceSdk.jar 包給其他人使用。jar 在編譯過程中會生成,但是它不屬于 Android Library 的標準輸出。在這種情況下,我需要在編譯完成后,主動 copy jar 包到我自己設計的產(chǎn)出物目錄中。

//Library 工程必須加載此插件。注意,加載了 Android 插件就不要加載 Java 插件了。因為 Android
//插件本身就是拓展了 Java 插件  
apply plugin: 'com.android.library'  
//android 的編譯,增加了一種新類型的 Script Block-->android
android {
       //你看,我在 local.properties 中設置的 API 版本號,就可以一次設置,多個 Project 使用了  

      //借助我特意設計的 gradle.ext.api 屬性  

       compileSdkVersion = gradle.api  //這兩個紅色的參數(shù)必須設置  

       buildToolsVersion  = "22.0.1"
       sourceSets{ //配置源碼路徑。這個 sourceSets 是 Java 插件引入的  

        main{ //main:Android 也用了  

            manifest.srcFile 'AndroidManifest.xml'  //這是一個函數(shù),設置 manifest.srcFile
            aidl.srcDirs=['src'] //設置 aidl 文件的目錄  

            java.srcDirs=['src'] //設置 java 文件的目錄  

        }
     }
    dependencies {  //配置依賴關系  

       //compile 表示編譯和運行時候需要的 jar 包,fileTree 是一個函數(shù),  

      //dir:'libs',表示搜索目錄的名稱是 libs。include:['*.jar'],表示搜索目錄下滿足*.jar 名字的 jar
     //包都作為依賴 jar 文件  

        compile fileTree(dir: 'libs', include: ['*.jar'])
   }
}  //android SB 配置完了  

//clean 是一個 Task 的名字,這個 Task 好像是 Java 插件(這里是 Android 插件)引入的。  
//dependsOn 是一個函數(shù),下面這句話的意思是 clean 任務依賴 cposCleanTask 任務。所以  
//當你 gradle clean 以執(zhí)行 clean Task 的時候,cposCleanTask 也會執(zhí)行  
clean.dependsOn 'cposCleanTask'
//創(chuàng)建一個 Task,  
task cposCleanTask() <<{
    cleanOutput(true)  //cleanOutput 是 utils.gradle 中通過 extra 屬性設置的 Closure
}
//前面說了,我要把 jar 包拷貝到指定的目錄。對于 Android 編譯,我一般指定 gradle assemble
//它默認編譯 debug 和 release 兩種輸出。所以,下面這個段代碼表示:  
//tasks 代表一個 Projects 中的所有 Task,是一個容器。getByName 表示找到指定名稱的任務。  
//我這里要找的 assemble 任務,然后我通過 doLast 添加了一個 Action。這個 Action 就是 copy
//產(chǎn)出物到我設置的目標目錄中去  
tasks.getByName("assemble"){
    it.doLast{
        println "$project.name: After assemble, jar libs are copied to local repository"
        copyOutput(true)
     }
}
/*
  因為我的項目只提供最終的 release 編譯出來的 Jar 包給其他人,所以不需要編譯 debug 版的東西  

  當 Project 創(chuàng)建完所有任務的有向圖后,我通過 afterEvaluate 函數(shù)設置一個回調(diào) Closure。在這個回調(diào)  

  Closure 里,我 disable 了所有 Debug 的 Task
*/
project.afterEvaluate{
    disableDebugBuild()
}

Android 自己定義了好多 ScriptBlock。Android 定義的 DSL 參考文檔在

https://developer.android.com/tools/building/plugin-for-gradle.html 下載。注意,它居然沒有提供在線文檔。

圖 36 所示為 Android 的 DSL 參考信息。

http://wiki.jikexueyuan.com/project/deep-android-gradle/images/38.jpg" alt="" />

圖 37 為 buildToolsVersion 和 compileSdkVersion 的說明:

http://wiki.jikexueyuan.com/project/deep-android-gradle/images/39.jpg" alt="" />

從圖 37 可知,這兩個變量是必須要設置的.....

5.CPosDeviceServerApk build.gradle

再來看一個 APK 的 build,它包含 NDK 的編譯,并且還要簽名。根據(jù)項目的需求,我們只能簽 debug 版的,而 release 版的簽名得發(fā)布 unsigned 包給領導簽名。另外,CPosDeviceServerAPK 依賴 CPosDeviceSdk。

雖然我可以先編譯 CPosDeviceSdk,得到對應的 jar 包,然后設置 CPosDeviceServerApk 直接依賴這個 jar 包就好。但是我更希望 CPosDeviceServerApk 能直接依賴于 CPosDeviceSdk 這個工程。這樣,整個 posdevice 可以做到這幾個 Project 的依賴關系是最新的。

[build.gradle]
apply plugin: 'com.android.application'  //APK 編譯必須加載這個插件  
android {
       compileSdkVersion gradle.api 
       buildToolsVersion "22.0.1"
       sourceSets{  //差不多的設置  

        main{
            manifest.srcFile 'AndroidManifest.xml'
          //通過設置 jni 目錄為空,我們可不使用 apk 插件的 jni 編譯功能。為什么?因為據(jù)說  

          //APK 插件的 jni 功能好像不是很好使....暈菜  

           jni.srcDirs = []  
            jniLibs.srcDir 'libs'
            aidl.srcDirs=['src']
            java.srcDirs=['src']
            res.srcDirs=['res']
        }
    }//main 結束  

    signingConfigs { //設置簽名信息配置  

        debug {  //如果我們在 local.properties 設置使用特殊的 keystore,則使用它  

           //下面這些設置,無非是函數(shù)調(diào)用....請務必閱讀 API 文檔  

           if(project.gradle.debugKeystore != null){ 
               storeFile file("file://${project.gradle.debugKeystore}")
               storePassword "android"
               keyAlias "androiddebugkey"
               keyPassword "android"
            }
        }
   }//signingConfigs 結束  

     buildTypes {
        debug {
            signingConfig signingConfigs.debug
            jniDebuggable false
        }
    }//buildTypes 結束  

    dependencies {
        //compile:project 函數(shù)可指定依賴 multi-project 中的某個子 project
        compile project(':CPosDeviceSdk')
        compile fileTree(dir: 'libs', include: ['*.jar'])
   } //dependices 結束  

  repositories {
   flatDir { //flatDir:告訴 gradle,編譯中依賴的 jar 包存儲在 dirs 指定的目錄  

            name "minsheng-gradle-local-repository"
            dirs gradle.LOCAL_JAR_OUT //LOCAL_JAR_OUT 是我存放編譯出來的 jar 包的位置  

   }
  }//repositories 結束  

}//android 結束  

/*
   創(chuàng)建一個 Task,類型是 Exec,這表明它會執(zhí)行一個命令。我這里讓他執(zhí)行 ndk 的  

   ndk-build 命令,用于編譯 ndk。關于 Exec 類型的 Task,請自行腦補 Gradle 的 API
*/
//注意此處創(chuàng)建 task 的方法,是直接{}喔,那么它后面的 tasks.withType(JavaCompile)
//設置的依賴關系,還有意義嗎?Think!如果你能想明白,gradle 掌握也就差不多了  

task buildNative(type: Exec, description: 'Compile JNI source via NDK') {
        if(project.gradle.ndkDir == null) //看看有沒有指定 ndk.dir 路徑  

           println "CANNOT Build NDK"
        else{
            commandLine "/${project.gradle.ndkDir}/ndk-build",
                '-C', file('jni').absolutePath,
                '-j', Runtime.runtime.availableProcessors(),
                'all', 'NDK_DEBUG=0'
        }
  }
 tasks.withType(JavaCompile) {
        compileTask -> compileTask.dependsOn buildNative
  }
  ......   
 //對于 APK,除了拷貝 APK 文件到指定目錄外,我還特意為它們加上了自動版本命名的功能  

 tasks.getByName("assemble"){
        it.doLast{
        println "$project.name: After assemble, jar libs are copied to local repository"
        project.ext.versionName = android.defaultConfig.versionName
        println "\t versionName = $versionName"
        copyOutput(false)
     }
}
  1. 結果展示

在 posdevice 下執(zhí)行 gradle assemble 命令,最終的輸出文件都會拷貝到我指定的目錄,結果如圖 38 所示:

http://wiki.jikexueyuan.com/project/deep-android-gradle/images/40.jpg" alt="" />

圖 38 所示為 posdevice gradle assemble 的執(zhí)行結果:

  • library 包都編譯 release 版的,copy 到 xxx/javaLib 目錄下

  • apk 編譯 debug 和 release-unsigned 版的,copy 到 apps 目錄下

  • 所有產(chǎn)出物都自動從 AndroidManifest.xml 中提取 versionName。

實例 2

下面這個實例也是來自一個實際的 APP。這個 APP 對應的是一個單獨的 Project。但是根據(jù)我前面的建議,我會把它改造成支持 Multi-Projects Build 的樣子。即在工程目錄下放一個 settings.build。

另外,這個 app 有一個特點。它有三個版本,分別是 debug、release 和 demo。這三個版本對應的代碼都完全一樣,但是在運行的時候需要從 assets/runtime_config 文件中讀取參數(shù)。參數(shù)不同,則運行的時候會跳轉到 debug、release 或者 demo 的邏輯上。

注意:我知道 assets/runtime_config 這種做法不 decent,但,這是一個既有項目,我們只能做小范圍的適配,而不是傷筋動骨改用更好的方法。另外,從未來的需求來看,暫時也沒有大改的必要。

引入 gradle 后,我們該如何處理呢?

解決方法是:在編譯 build、release 和 demo 版本前,在 build.gradle 中自動設置 runtime_config 的內(nèi)容。代碼如下所示:

[build.gradle]
apply plugin: 'com.android.application'  //加載 APP 插件  

//加載 utils.gradle
apply from: rootProject.getRootDir().getAbsolutePath() + "/utils.gradle"
//buildscript 設置 android app 插件的位置  

buildscript {
    repositories { jcenter() }
    dependencies { classpath 'com.android.tools.build:gradle:1.2.3' }
}
//android ScriptBlock
android {
    compileSdkVersion gradle.api
    buildToolsVersion "22.0.1"
   sourceSets{ //源碼設置 SB
        main{
            manifest.srcFile 'AndroidManifest.xml'
            jni.srcDirs = []
            jniLibs.srcDir 'libs'
            aidl.srcDirs=['src']
            java.srcDirs=['src']
            res.srcDirs=['res']
            assets.srcDirs = ['assets'] //多了一個 assets 目錄  

        }
    }
    signingConfigs {//簽名設置  

        debug {  //debug 對應的 SB。注意  

            if(project.gradle.debugKeystore != null){
                storeFile file("file://${project.gradle.debugKeystore}")
                storePassword "android"
                keyAlias "androiddebugkey"
                keyPassword "android"
            }
        }
    }
    /*
     最關鍵的內(nèi)容來了: buildTypes ScriptBlock.
     buildTypes 和上面的 signingConfigs,當我們在 build.gradle 中通過{}配置它的時候,  

     其背后的所代表的對象是 NamedDomainObjectContainer<BuildType> 和  

     NamedDomainObjectContainer<SigningConfig>
     注意,NamedDomainObjectContainer<BuildType/或者 SigningConfig>是一種容器,  

     容器的元素是 BuildType 或者 SigningConfig。我們在 debug{}要填充 BuildType 或者  

    SigningConfig 所包的元素,比如 storePassword 就是 SigningConfig 類的成員。而 proguardFile 等  

    是 BuildType 的成員。  

    那么,為什么要使用 NamedDomainObjectContainer 這種數(shù)據(jù)結構呢?因為往這種容器里  

    添加元素可以采用這樣的方法: 比如 signingConfig 為例  

    signingConfig{//這是一個 NamedDomainObjectContainer<SigningConfig>
       test1{//新建一個名為 test1 的 SigningConfig 元素,然后添加到容器里  

         //在這個花括號中設置 SigningConfig 的成員變量的值  

       }
      test2{//新建一個名為 test2 的 SigningConfig 元素,然后添加到容器里  

         //在這個花括號中設置 SigningConfig 的成員變量的值  

      }
    }
    在 buildTypes 中,Android 默認為這幾個 NamedDomainObjectContainer 添加了  

    debug 和 release 對應的對象。如果我們再添加別的名字的東西,那么 gradle assemble 的時候  

    也會編譯這個名字的 apk 出來。比如,我添加一個名為 test 的 buildTypes,那么 gradle assemble
    就會編譯一個 xxx-test-yy.apk。在此,test 就好像 debug、release 一樣。  

   */
    buildTypes{
        debug{ //修改 debug 的 signingConfig 為 signingConfig.debug 配置  

            signingConfig signingConfigs.debug
        }
        demo{ //demo 版需要混淆  

            proguardFile 'proguard-project.txt'
            signingConfig signingConfigs.debug
        }
       //release 版沒有設置,所以默認沒有簽名,沒有混淆  

    }
      ......//其他和 posdevice 類似的處理。來看如何動態(tài)生成 runtime_config 文件  

   def  runtime_config_file = 'assets/runtime_config'
   /*
   我們在 gradle 解析完整個任務之后,找到對應的 Task,然后在里邊添加一個 doFirst Action
   這樣能確保編譯開始的時候,我們就把 runtime_config 文件準備好了。  

   注意,必須在 afterEvaluate 里邊才能做,否則 gradle 沒有建立完任務有向圖,你是找不到  

   什么 preDebugBuild 之類的任務的  

   */
    project.afterEvaluate{
      //找到 preDebugBuild 任務,然后添加一個 Action  
      tasks.getByName("preDebugBuild"){
            it.doFirst{
                println "generate debug configuration for ${project.name}"
                def configFile = new File(runtime_config_file)
                configFile.withOutputStream{os->
                    os << I am Debug\n'  //往配置文件里寫 I am Debug
                 }
            }
        }
       //找到 preReleaseBuild 任務  

        tasks.getByName("preReleaseBuild"){
            it.doFirst{
                println "generate release configuration for ${project.name}"
                def configFile = new File(runtime_config_file)
                configFile.withOutputStream{os->
                    os << I am release\n'
                }
            }
        }
       //找到 preDemoBuild。這個任務明顯是因為我們在 buildType 里添加了一個 demo 的元素  

      //所以 Android APP 插件自動為我們生成的  

        tasks.getByName("preDemoBuild"){
            it.doFirst{
                println "generate offlinedemo configuration for ${project.name}"
                def configFile = new File(runtime_config_file)
                configFile.withOutputStream{os->
                    os << I am Demo\n'
                }
            }
        }
    }
}
 .....//copyOutput

最終的結果如圖 39 所示:

http://wiki.jikexueyuan.com/project/deep-android-gradle/images/41.jpg" alt="" />

幾個問題,為什么我知道有 preXXXBuild 這樣的任務?

答案:gradle tasks --all 查看所有任務。然后,多嘗試幾次,直到成功

上一篇:一些前提知識下一篇:Gradle 介紹