深度探索 Gradle 自動化構建技術(三、Gradle 核心解密)

前言

成爲一名優秀的Android開發,需要一份完備的知識體系,在這裏,讓我們一起成長爲自己所想的那樣~。

從明面上看,Gradle 是一款強大的構建工具,而且許多文章也僅僅都把 Gradle 當做一款工具對待。但是,Gradle 不僅僅是一款強大的構建工具,它看起來更像是一個編程框架。Gradle 的組成可以細分爲如下三個方面

  • 1)、groovy 核心語法包括 groovy 基本語法、閉包、數據結構、面向對象等等
  • 2)、Android DSL(build scrpit block)Android 插件在 Gradle 所特有的東西,我們可以在不同的 build scrpit block 中去做不同的事情
  • 3)、Gradle API包含 Project、Task、Setting 等等(本文重點)

可以看到,Gradle 的語法是以 groovy 爲基礎的,而且,它還有自己獨有的 API,所以我們可以把 Gradle 認作是一款編程框架,利用 Gradle 我們可以在編程中去實現項目構建過程中的所有需求。需要注意的是,想要隨心所欲地使用 Gradle,我們必須提前掌握好 groovy,如果對 groovy 還不是很熟悉的建議看看 《深入探索Gradle自動化構建技術(二、Groovy 築基篇)》 一文。

需要注意的是,Groovy 是一門語言,而 DSL 一種特定領域的配置文件,Gradle 是基於 Groovy 的一種框架工具,而 gradlew 則是 gradle 的一個兼容包裝工具。

一、Gradle 優勢

1、更好的靈活性

在靈活性上,Gradle 相對於 Maven、Ant 等構建工具, 其 提供了一系列的 API 讓我們有能力去修改或定製項目的構建過程。例如我們可以 利用 Gradle 去動態修改生成的 APK 包名,但是如果是使用的 Maven、Ant 等工具,我們就必須等生成 APK 後,再手動去修改 APK 的名稱。

2、更細的粒度

在粒度性上,使用 Maven、Ant 等構建工具時,我們的源代碼和構建腳本是獨立的,而且我們也不知道其內部的處理是怎樣的。但是,我們的 Gradle 則不同,它 從源代碼的編譯、資源的編譯、再到生成 APK 的過程中都是一個接一個來執行的

此外,Gradle 構建的粒度細化到了每一個 task 之中。並且它所有的 Task 源碼都是開源的,在我們掌握了這一整套打包流程後,我們就可以通過去修改它的 Task 去動態改變其執行流程。例如 Tinker 框架的實現過程中,它通過動態地修改 Gradle 的打包過程生成 APK 的同時,也生成了各種補丁文件。

3、更好的擴展性

在擴展性上,Gradle 支持插件機制,所以我們可以複用這些插件,就如同複用庫一樣簡單方便

4、更強的兼容性

Gradle 不僅自身功能強大,而且它還能 兼容所有的 Maven、Ant 功能,也就是說,Gradle 吸取了所有構建工具的長處

可以看到,Gradle 相比於其它構建工具,其好處不言而喻,而其 最核心的原因就是因爲 Gradle 是一套編程框架

二、Gradle 構建生命週期

Gradle 的構建過程分爲 三部分:初始化階段、配置階段和執行階段。其構建流程如下圖所示:

下面分別來詳細瞭解下它們。

1、初始化階段

首先,在這個階段中,會讀取根工程中的 setting.gradle 中的 include 信息,確定有多少工程加入構建,然後,會爲每一個項目(build.gradle 腳本文件)創建一個個與之對應的 Project 實例,最終形成一個項目的層次結構。 與初始化階段相關的腳本文件是 settings.gradle,而一個 settings.gradle 腳本對應一個 Settings 對象,我們最常用來聲明項目的層次結構的 include 就是 Settings 對象下的一個方法,在 Gradle 初始化的時候會構造一個 Settings 實例對象,以執行各個 Project 的初始化配置

settings.gradle

settings.gradle 文件中,我們可以 在 Gradle 的構建過程中添加各個生命週期節點監聽,其代碼如下所示:

include ':app'
gradle.addBuildListener(new BuildListener() {
    void buildStarted(Gradle var1) {
        println '開始構建'
    }
    void settingsEvaluated(Settings var1) {
        // var1.gradle.rootProject 這裏訪問 Project 對象時會報錯,
        // 因爲還未完成 Project 的初始化。
        println 'settings 評估完成(settings.gradle 中代碼執行完畢)'
    }
    void projectsLoaded(Gradle var1) {
        println '項目結構加載完成(初始化階段結束)'
        println '初始化結束,可訪問根項目:' + var1.gradle.rootProject
    }
    void projectsEvaluated(Gradle var1) {
        println '所有項目評估完成(配置階段結束)'
    }
    void buildFinished(BuildResult var1) {
        println '構建結束 '
    }
})
複製代碼

編寫完相應的 Gradle 生命週期監聽代碼之後,我們就可以在 Build 輸出界面看到如下信息:

Executing tasks: [clean, :app:assembleSpeedDebug] in project
/Users/quchao/Documents/main-open-project/Awesome-WanAndroid
settings評估完成(settins.gradle中代碼執行完畢)
項目結構加載完成(初始化階段結束)
初始化結束,可訪問根項目:root project 'Awesome-WanAndroid'
Configuration on demand is an incubating feature.
> Configure project :app
gradlew version > 4.0
WARNING: API 'variant.getJavaCompiler()' is obsolete and has been
replaced with 'variant.getJavaCompileProvider()'.
It will be removed at the end of 2019.
For more information, see
https://d.android.com/r/tools/task-configuration-avoidance.
To determine what is calling variant.getJavaCompiler(), use
-Pandroid.debug.obsoleteApi=true on the command line to display more
information.
skip tinyPicPlugin Task!!!!!!
skip tinyPicPlugin Task!!!!!!
所有項目評估完成(配置階段結束)
Task :clean UP-TO-DATE
:clean spend 1ms
...
Task :app:clean
:app:clean spend 2ms
Task :app:packageSpeedDebug
:app:packageSpeedDebug spend 825ms
Task :app:assembleSpeedDebug
:app:assembleSpeedDebug spend 1ms
構建結束 
Tasks spend time > 50ms:
    ...
複製代碼

此外,在 settings.gradle 文件中,我們可以指定其它 project 的位置,這樣就可以將其它外部工程中的 moudle 導入到當前的工程之中了。示例代碼如下所示:

if (useSpeechMoudle) {
    // 導入其它 App 的 speech 語音模塊
    include "speech"
    project(":speech").projectDir = new     File("../OtherApp/speech")
}
複製代碼

2、配置階段

配置階段的任務是 執行各項目下的 build.gradle 腳本,完成 Project 的配置,與此同時,會構造 Task 任務依賴關係圖以便在執行階段按照依賴關係執行 Task。而在配置階段執行的代碼通常來說都會包括以下三個部分的內容,如下所示:

  • 1)、build.gralde 中的各種語句
  • 2)、閉包
  • 3)、Task 中的配置段語句

需要注意的是,執行任何 Gradle 命令,在初始化階段和配置階段的代碼都會被執行

3、執行階段

在配置階段結束後,Gradle 會根據各個任務 Task 的依賴關係來創建一個有向無環圖,我們可以通過 Gradle 對象的 getTaskGraph 方法來得到該有向無環圖 => TaskExecutionGraph,並且,當有向無環圖構建完成之後,所有 Task 執行之前,我們可以通過 whenReady(groovy.lang.Closure) 或者 addTaskExecutionGraphListener(TaskExecutionGraphListener) 來接收相應的通知,其代碼如下所示:

gradle.getTaskGraph().addTaskExecutionGraphListener(new
TaskExecutionGraphListener() {
    @Override
    void graphPopulated(TaskExecutionGraph graph) {
    }
})
複製代碼

然後,Gradle 構建系統會通過調用 gradle <任務名> 來執行相應的各個任務。

4、Hook Gradle 各個生命週期節點

這裏借用 Goe_H 的 Gradle 生命週期時序圖來講解一下 Gradle 生命週期的整個流程,如下圖所示:

可以看到,整個 Gradle 生命週期的流程包含如下 四個部分

  • 1)、首先,解析 settings.gradle 來獲取模塊信息,這是初始化階段
  • 2)、然後,配置每個模塊,配置的時候並不會執行 task
  • 3)、接着,配置完了以後,有一個重要的回調 project.afterEvaluate,它表示所有的模塊都已經配置完了,可以準備執行 task 了
  • 4)、最後,執行指定的 task 及其依賴的 task

在 Gradle 構建命令中,最爲複雜的命令可以說是 gradle build 這個命令了,因爲項目的構建過程中需要依賴很多其它的 task。這裏,我們以 Java 項目的構建過程看看它所依賴的 tasks 及其組成的有向無環圖,如下所示:

注意事項

  • 1)、每一個 Hook 點對應的監聽器一定要在回調的生命週期之前添加
  • 2)、如果註冊了多個 project.afterEvaluate 回調,那麼執行順序將與註冊順序保持一致

5、獲取構建各個階段、任務的耗時情況

瞭解了 Gradle 生命週期中的各個 Hook 方法之後,我們就可以 利用它們來獲取項目構建各個階段、任務的耗時情況,在 settings.gradle 中加入如下代碼即可:

long beginOfSetting = System.currentTimeMillis()
def beginOfConfig
def configHasBegin = false
def beginOfProjectConfig = new HashMap()
def beginOfProjectExcute
gradle.projectsLoaded {
    println '初始化階段,耗時:' + (System.currentTimeMillis() -
beginOfSetting) + 'ms'
}
gradle.beforeProject { project ->
    if (!configHasBegin) {
        configHasBegin = true
        beginOfConfig = System.currentTimeMillis()
    }
    beginOfProjectConfig.put(project, System.currentTimeMillis())
}
gradle.afterProject { project ->
    def begin = beginOfProjectConfig.get(project)
    println '配置階段,' + project + '耗時:' +
(System.currentTimeMillis() - begin) + 'ms'
}
gradle.taskGraph.whenReady {
    println '配置階段,總共耗時:' + (System.currentTimeMillis() -
beginOfConfig) + 'ms'
    beginOfProjectExcute = System.currentTimeMillis()
}
gradle.taskGraph.beforeTask { task ->
    task.doFirst {
        task.ext.beginOfTask = System.currentTimeMillis()
    }
    task.doLast {
        println '執行階段,' + task + '耗時:' +
(System.currentTimeMillis() - task.beginOfTask) + 'ms'
    }
}
gradle.buildFinished {
    println '執行階段,耗時:' + (System.currentTimeMillis() -
beginOfProjectExcute) + 'ms'
}
複製代碼

在 Gradle 中,執行每一種類型的配置腳本就會創建與之對應的實例,而在 Gradle 中如 三種類型的配置腳本,如下所示:

  • 1)、Build Scrpit對應一個 Project 實例,即每個 build.gradle 都會轉換成一個 Project 實例
  • 2)、Init Scrpit對應一個 Gradle 實例,它在構建初始化時創建,整個構建執行過程中以單例形式存在
  • 3)、Settings Scrpit對應一個 Settings 實例,即每個 settings.gradle 都會轉換成一個 Settings 實例

可以看到,一個 Gradle 構建流程中會由一至多個 project 實例構成,而每一個 project 實例又是由一至多個 task 構成。下面,我們就來認識下 Project。

三、Project

Project 是 Gradle 構建整個應用程序的入口,所以它非常重要,我們必須對其有深刻地瞭解。不幸的是,網上幾乎沒有關於 project 講解的比較好的文章,不過沒關係,下面,我們將會一起來深入學習 project api 這部分。

由前可知,每一個 build.gradle 都有一個與之對應的 Project 實例,而在 build.gradle 中,我們通常都會配置一系列的項目依賴,如下面這個依賴:

implementation 'com.github.bumptech.glide:glide:4.8.0'
複製代碼

類似於 implementation、api 這種依賴關鍵字,在本質上它就是一個方法調用,在上面,我們使用 implementation() 方法傳入了一個 map 參數,參數裏面有三對 key-value,完整寫法如下所示:

implementation group: 'com.github.bumptech.glide' name:'glide' version:'4.8.0'
複製代碼

當我們使用 implementation、api 依賴對應的 aar 文件時,Gradle 會在 repository 倉庫 裏面找到與之對應的依賴文件,你的倉庫中可能包含 jcenter、maven 等一系列倉庫,而每一個倉庫其實就是很多依賴文件的集合服務器, 而他們就是通過上述的 group、name、version 來進行歸類存儲的

1、Project 核心 API 分解

在 Project 中有很多的 API,但是根據它們的 屬性和用途 我們可以將其分解爲 六大部分,如下圖所示:

對於 Project 中各個部分的作用,我們可以先來大致瞭解下,以便爲 Project 的 API 體系建立一個整體的感知能力,如下所示:

  • 1)、Project API讓當前的 Project 擁有了操作它的父 Project 以及管理它的子 Project 的能力
  • 2)、Task 相關 API爲當前 Project 提供了新增 Task 以及管理已有 Task 的能力。由於 task 非常重要,我們將放到第四章來進行講解
  • 3)、Project 屬性相關的 ApiGradle 會預先爲我們提供一些 Project 屬性,而屬性相關的 api 讓我們擁有了爲 Project 添加額外屬性的能力
  • 4)、File 相關 ApiProject File 相關的 API 主要用來操作我們當前 Project 下的一些文件處理
  • 5)、Gradle 生命週期 API即我們在第二章講解過的生命週期 API
  • 6)、其它 API添加依賴、添加配置、引入外部文件等等零散 API 的聚合

2、Project API

每一個 Groovy 腳本都會被編譯器編譯成 Script 字節碼,而每一個 build.gradle 腳本都會被編譯器編譯成 Project 字節碼,所以我們在 build.gradle 中所寫的一切邏輯都是在 Project 類內進行書寫的。下面,我們將按照由易到難的套路來介紹 Project 的一系列重要的 API。

需要提前說明的是,默認情況下我們選定根工程的 build.gradle 這個腳本文件中來學習 Project 的一系列用法,關於 getAllProject 的用法如下所示:

1、getAllprojects

getAllprojects 表示 獲取所有 project 的實例,示例代碼如下所示:

/**
 * getAllProjects 使用示例
 */

this.getProjects()

def getProjects() {
    println "<================>"
    println " Root Project Start "
    println "<================>"
    // 1、getAllprojects 方法返回一個包含根 project 與其子 project 的 Set 集合
    // eachWithIndex 方法用於遍歷集合、數組等可迭代的容器,
    // 並同時返回下標,不同於 each 方法僅返回 project
    this.getAllprojects().eachWithIndex { Project project, int index ->
        // 2、下標爲 0,表明當前遍歷的是 rootProject
        if (index == 0) {
            println "Root Project is $project"
        } else {
            println "child Project is $project"
        }
    }
}
複製代碼

首先,我們使用了 def 關鍵字定義了一個 getProjects 方法。然後,在註釋1處,我們調用了 getAllprojects 方法返回一個包含根 project 與其子 project 的 Set 集合,並鏈式調用了 eachWithIndex 遍歷 Set 集合。接着,在註釋2處,我們會判斷當前的下標 index 是否是0,如果是,則表明當前遍歷的是 rootProject,則輸出 rootProject 的名字,否則,輸出 child project 的名字。

下面,我們在命令行執行 ./gradlew clean,其運行結果如下所示:

quchao@quchaodeMacBook-Pro Awesome-WanAndroid % ./gradlew clean
settings 評估完成(settings.gradle 中代碼執行完畢)
項目結構加載完成(初始化階段結束)
初始化結束,可訪問根項目:root project 'Awesome-WanAndroid'
初始化階段,耗時:5ms
Configuration on demand is an incubating feature.
> Configure project :
<================>
 Root Project Start 
<================>
Root Project is root project 'Awesome-WanAndroid'
child Project is project ':app'
配置階段,root project 'Awesome-WanAndroid'耗時:284ms
> Configure project :app
...
配置階段,總共耗時:428ms
> Task :app:clean
執行階段,task ':app:clean'耗時:1ms
:app:clean spend 2ms
構建結束 
Tasks spend time > 50ms:
執行階段,耗時:9ms
複製代碼

可以看到,執行了初始化之後,就會先配置我們的 rootProject,並輸出了對應的工程信息。接着,便會執行子工程 app 的配置。最後,執行了 clean 這個 task。

需要注意的是,rootProject 與其旗下的各個子工程組成了一個樹形結構,但是這顆樹的高度也僅僅被限定爲了兩層

2、getSubprojects

getSubprojects 表示獲取當前工程下所有子工程的實例,示例代碼如下所示:

/**
 * getAllsubproject 使用示例
 */

this.getSubProjects()

def getSubProjects() {
    println "<================>"
    println " Sub Project Start "
    println "<================>"
    // getSubprojects 方法返回一個包含子 project 的 Set 集合
    this.getSubprojects().each { Project project ->
        println "child Project is $project"
    }
}
複製代碼

同 getAllprojects 的用法一樣,getSubprojects 方法返回了一個包含子 project 的 Set 集合,這裏我們直接使用 each 方法將各個子 project 的名字打印出來。其運行結果如下所示:

quchao@quchaodeMacBook-Pro Awesome-WanAndroid % ./gradlew clean
settings 評估完成(settings.gradle 中代碼執行完畢)
...
> Configure project :
<================>
 Sub Project Start 
<================>
child Project is project ':app'
配置階段,root project 'Awesome-WanAndroid'耗時:289ms
> Configure project :app
...
所有項目評估完成(配置階段結束)
配置階段,總共耗時:425ms
> Task :app:clean
執行階段,task ':app:clean'耗時:1ms
:app:clean spend 2ms
構建結束 
Tasks spend time > 50ms:
執行階段,耗時:9ms
複製代碼

可以看到,同樣在 Gradle 的配置階段輸出了子工程的名字。

3、getParent

getParent 表示 獲取當前 project 的父類,需要注意的是,如果我們在根工程中使用它,獲取的父類會爲 null,因爲根工程沒有父類,所以這裏我們直接在 app 的 build.gradle 下編寫下面的示例代碼:

...
> Configure project :
配置階段,root project 'Awesome-WanAndroid'耗時:104ms
> Configure project :app
gradlew version > 4.0
my parent project is Awesome-WanAndroid
配置階段,project ':app'耗時:282ms
...
所有項目評估完成(配置階段結束)
配置階段,總共耗時:443ms
...
複製代碼

可以看到,這裏輸出了 app project 當前的父類,即 Awesome-WanAndroid project。

4、getRootProject

如果我們想在根工程僅僅獲取當前的 project 實例該怎麼辦呢?直接使用 getRootProject 即可在任意 build.gradle 文件獲取當前根工程的 project 實例,示例代碼如下所示:

/**
 * 4、getRootProject 使用示例
 */

this.getRootPro()

def getRootPro() {
    def rootProjectName = this.getRootProject().name
    println "root project is $rootProjectName"
}
複製代碼

5、project

project 表示的是 指定工程的實例,然後在閉包中對其進行操作。在使用之前,我們有必要看看 project 方法的源碼,如下所示:

    /**
     * <p>Locates a project by path and configures it using the given closure. If the path is relative, it is
     * interpreted relative to this project. The target project is passed to the closure as the closure's delegate.</p>
     *
     * @param path The path.
     * @param configureClosure The closure to use to configure the project.
     * @return The project with the given path. Never returns null.
     * @throws UnknownProjectException If no project with the given path exists.
     */

    Project project(String path, Closure configureClosure);
複製代碼

可以看到,在 project 方法中兩個參數,一個是指定工程的路徑,另一個是用來配置該工程的閉包。下面我們看看如何靈活地使用 project,示例代碼如下所示:

/**
 * 5、project 使用示例
 */


// 1、閉包參數可以放在括號外面
project("app") { Project project ->
    apply plugin: 'com.android.application'
}

// 2、更簡潔的寫法是這樣的:省略參數
project("app") {
    apply plugin: 'com.android.application'
}
複製代碼

使用熟練之後,我們通常會採用註釋2處的寫法。

6、allprojects

allprojects 表示 用於配置當前 project 及其旗下的每一個子 project,如下所示:

/**
 * 6、allprojects 使用示例
 */


// 同 project 一樣的更簡潔寫法
allprojects {
    repositories {
        google()
        jcenter()
        mavenCentral()
        maven {
            url "https://jitpack.io"
        }
        maven { url "https://plugins.gradle.org/m2/" }
    }
}
複製代碼

在 allprojects 中我們一般用來配置一些通用的配置,比如上面最常見的全局倉庫配置。

7、subprojects

subprojects 可以 統一配置當前 project 下的所有子 project,示例代碼如下所示:

/**
 * 7、subprojects 使用示例:
 *    給所有的子工程引入 將 aar 文件上傳置 Maven 服務器的配置腳本
 */

subprojects {
    if (project.plugins.hasPlugin("com.android.library")) {
        apply from: '../publishToMaven.gradle'
    }
}
複製代碼

在上述示例代碼中,我們會先判斷當前 project 旗下的子 project 是不是庫,如果是庫纔有必要引入 publishToMaven 腳本。

3、project 屬性

目前,在 project 接口裏,僅僅預先定義了 七個 屬性,其源碼如下所示:

public interface Project extends Comparable<Project>, ExtensionAwarePluginAware {
    /**
     * 默認的工程構建文件名稱
     */

    String DEFAULT_BUILD_FILE = "build.gradle";

    /**
     * 區分開 project 名字與 task 名字的符號
     */

    String PATH_SEPARATOR = ":";

    /**
     * 默認的構建目錄名稱
     */

    String DEFAULT_BUILD_DIR_NAME = "build";

    String GRADLE_PROPERTIES = "gradle.properties";

    String SYSTEM_PROP_PREFIX = "systemProp";

    String DEFAULT_VERSION = "unspecified";

    String DEFAULT_STATUS = "release";
    
    ...
}
複製代碼

幸運的是,Gradle 提供了 ext 關鍵字讓我們有能力去定義自身所需要的擴展屬性。有了它便可以對我們工程中的依賴進行全局配置。下面,我們先從配置的遠古時代講起,以便讓我們對 gradle 的 全局依賴配置有更深入的理解。

ext 擴展屬性

1、遠古時代

在 AS 剛出現的時候,我們的依賴配置代碼是這樣的:

android {
    compileSdkVersion 27
    buildToolsVersion "28.0.3"
    ...
}
複製代碼

2、刀耕火種

但是這種直接寫值的方式顯示是不規範的,因此,後面我們使用了這種方式:

def mCompileSdkVersion = 27
def mBuildToolsVersion = "28.0.3"

android {
    compileSdkVersion mCompileSdkVersion
    buildToolsVersion mBuildToolsVersion
    ...
}
複製代碼

3、鐵犁牛耕

如果每一個子 project 都需要配置相同的 Version,我們就需要多寫很多的重複代碼,因此,我們可以利用上面我們學過的 subproject 和 ext 來進行簡化:

// 在根目錄下的 build.gradle 中
subprojects {
    ext {
        compileSdkVersion = 27
        buildToolsVersion = "28.0.3"
    }
}

// 在 app moudle 下的 build.gradle 中
android {
    compileSdkVersion this.compileSdkVersion
    buildToolsVersion this.buildToolsVersion
    ...
}
複製代碼

4、工業時代

使用 subprojects 方法來定義通用的擴展屬性還是存在着很嚴重的問題,它跟之前的方式一樣,還是會在每一個子 project 去定義這些被擴展的屬性,此時,我們可以將 subprojects 去除,直接使用 ext 進行全局定義即可:

// 在根目錄下的 build.gradle 中
ext {
    compileSdkVersion = 27
    buildToolsVersion = "28.0.3"
}
複製代碼

5、電器時代

當項目越來越大的時候,在根項目下定義的 ext 擴展屬性越來越多,因此,我們可以將這一套全局屬性配置在另一個 gradle 腳本中進行定義,這裏我們通常會將其命名爲 config.gradle,通用的模板如下所示:

ext {

    android = [
            compileSdkVersion       : 27,
            buildToolsVersion       : "28.0.3",
            ...
            ]
            
    version = [
            supportLibraryVersion   : "28.0.0",
            ...
            ]
            
    dependencies = [
            // base
            "appcompat-v7"                      : "com.android.support:appcompat-v7:${version["supportLibraryVersion"]}",
            ...
            ]
            
    annotationProcessor = [
            "glide_compiler"                    : "com.github.bumptech.glide:compiler:${version["glideVersion"]}",
            ...
            ]
            
    apiFileDependencies = [
            "launchstarter"                                   : "libs/launchstarter-release-1.0.0.aar",
            ...
            ]
            
    debugImplementationDependencies = [
            "MethodTraceMan"                                  : "com.github.zhengcx:MethodTraceMan:1.0.7"
    ]

    releaseImplementationDependencies = [
            "MethodTraceMan"                                  : "com.github.zhengcx:MethodTraceMan:1.0.5-noop"
    ]
    
    ...
}
複製代碼

6、更加智能化的現在

儘管有了很全面的全局依賴配置文件,但是,在我們的各個模塊之中,還是不得不寫一大長串的依賴代碼,因此,我們可以 使用遍歷的方式去進行依賴,其模板代碼如下所示:


// 在各個 moulde 下的 build.gradle 腳本下
def implementationDependencies = rootProject.ext.dependencies
def processors = rootProject.ext.annotationProcessor
def apiFileDependencies = rootProject.ext.apiFileDependencies

// 在各個 moulde 下的 build.gradle 腳本的 dependencies 閉包中
// 處理所有的 aar 依賴
apiFileDependencies.each { k, v -> api files(v)}

// 處理所有的 xxximplementation 依賴
implementationDependencies.each 
{ k, v -> implementation v }
debugImplementationDependencies.each { k, v -> debugImplementation v } 
...

// 處理 annotationProcessor 依賴
processors.each { k, v -> annotationProcessor v }

// 處理所有包含 exclude 的依賴
debugImplementationExcludes.each { entry ->
    debugImplementation(entry.key) {
        entry.value.each { childEntry ->
            exclude(group: childEntry.key, module: childEntry.value)
        }
    }
}
複製代碼

也許未來隨着 Gradle 的不斷優化會有更加簡潔的方式,如果你有更好地方式,我們可以來探討一番。

在 gradle.properties 下定義擴展屬性

除了使用 ext 擴展屬性定義額外的屬性之外,我們也可以在 gradle.properties 下定義擴展屬性,其示例代碼如下所示:

// 在 gradle.properties 中
mCompileVersion = 27

// 在 app moudle 下的 build.gradle 中
compileSdkVersion mCompileVersion.toInteger()
複製代碼

4、文件相關 API

在 gradle 中,文件相關的 API 可以總結爲如下 兩大類

  • 1)、路徑獲取 API
    • getRootDir()
    • getProjectDir()
    • getBuildDir()
  • 2)、文件操作相關 API
    • 文件定位
    • 文件拷貝
    • 文件樹遍歷

1)、路徑獲取 API

關於路徑獲取的 API 常用的有 三種,其示例代碼如下所示:

/**
 * 1、路徑獲取 API
 */

println "the root file path is:" + getRootDir().absolutePath
println "this build file path is:" + getBuildDir().absolutePath
println "this Project file path is:" + getProjectDir().absolutePath
複製代碼

然後,我們執行 ./gradlew clean,輸出結果如下所示:

> Configure project :
the root file path is:/Users/quchao/Documents/main-open-project/Awesome-WanAndroid
this build file path is:/Users/quchao/Documents/main-open-project/Awesome-WanAndroid/build
this Project file path is:/Users/quchao/Documents/main-open-project/Awesome-WanAndroid
配置階段,root project 'Awesome-WanAndroid'耗時:538ms
複製代碼

2)、文件操作相關 API

1、文件定位

常用的文件定位 API 有 file/files,其示例代碼如下所示:

// 在 rootProject 下的 build.gradle 中

/**
 * 1、文件定位之 file
 */

this.getContent("config.gradle")

def getContent(String path) {
    try {
        // 不同與 new file 的需要傳入 絕對路徑 的方式,
        // file 從相對於當前的 project 工程開始查找
        def mFile = file(path)
        println mFile.text 
    } catch (GradleException e) {
        println e.toString()
        return null
    }
}

/**
 * 1、文件定位之 files
 */

this.getContent("config.gradle""build.gradle")

def getContent(String path1, String path2) {
    try {
        // 不同與 new file 的需要傳入 絕對路徑 的方式,
        // file 從相對於當前的 project 工程開始查找
        def mFiles = files(path1, path2)
        println mFiles[0].text + mFiles[1].text
    } catch (GradleException e) {
        println e.toString()
        return null
    }
}
複製代碼

2、文件拷貝

常用的文件拷貝 API 爲 copy,其示例代碼如下所示:

/**
 * 2、文件拷貝
 */

copy {
    // 既可以拷貝文件,也可以拷貝文件夾
    // 這裏是將 app moudle 下生成的 apk 目錄拷貝到
    // 根工程下的 build 目錄
    from file("build/outputs/apk")
    into getRootProject().getBuildDir().path + "/apk/"
    exclude 
{
        // 排除不需要拷貝的文件
    }
    rename {
        // 對拷貝過來的文件進行重命名
    }
}
複製代碼

3、文件樹遍歷

我們可以 使用 fileTree 將當前目錄轉換爲文件數的形式,然後便可以獲取到每一個樹元素(節點)進行相應的操作,其示例代碼如下所示:

/**
 * 3、文件樹遍歷
 */

fileTree("build/outputs/apk") { FileTree fileTree ->
    fileTree.visit { FileTreeElement fileTreeElement ->
        println "The file is $fileTreeElement.file.name"
        copy {
            from fileTreeElement.file
            into getRootProject().getBuildDir().path + "/apkTree/"
        }
    }
}
複製代碼

5、其它 API

1、依賴相關 API

根項目下的 buildscript

buildscript 中 用於配置項目核心的依賴。其原始的使用示例與簡化後的使用示例分別如下所示:

原始的使用示例
buildscript { ScriptHandler scriptHandler ->
    // 配置我們工程的倉庫地址
    scriptHandler.repositories { RepositoryHandler repositoryHandler ->
        repositoryHandler.google()
        repositoryHandler.jcenter()
        repositoryHandler.mavenCentral()
        repositoryHandler.maven { url 'https://maven.google.com' }
        repositoryHandler.maven { url "https://plugins.gradle.org/m2/" }
        repositoryHandler.maven {
            url uri('../PAGradlePlugin/repo')
        }
        // 訪問本地私有 Maven 服務器
        repositoryHandler.maven 
{
            name "personal"
            url "http://localhost:8081:/JsonChao/repositories"
            credentials {
                username = "JsonChao"
                password = "123456"
            }
        }
    }
    
      // 配置我們工程的插件依賴
    dependencies { DependencyHandler dependencyHandler ->
        dependencyHandler.classpath 'com.android.tools.build:gradle:3.1.4'
       
        ...
    }
複製代碼
簡化後的使用示例
buildscript {
    // 配置我們工程的倉庫地址
    repositories {
        google()
        jcenter()
        mavenCentral()
        maven { url 'https://maven.google.com' }
        maven { url "https://plugins.gradle.org/m2/" }
        maven {
            url uri('../PAGradlePlugin/repo')
        }
    }
    
    // 配置我們工程的插件依賴
    dependencies 
{
        classpath 'com.android.tools.build:gradle:3.1.4'
        
        ...
    }
複製代碼

app moudle 下的 dependencies

不同於 根項目 buildscript 中的 dependencies 是用來配置我們 Gradle 工程的插件依賴的,而 app moudle 下的 dependencies 是用來爲應用程序添加第三方依賴的。關於 app moudle 下的依賴使用這裏我們 需要注意下 exclude 與 transitive 的使用 即可,示例代碼如下所示:

implementation(rootProject.ext.dependencies.glide) {
        // 排除依賴:一般用於解決資源、代碼衝突相關的問題
        exclude module'support-v4' 
        // 傳遞依賴:A => B => C ,B 中使用到了 C 中的依賴,
        // 且 A 依賴於 B,如果打開傳遞依賴,則 A 能使用到 B 
        // 中所使用的 C 中的依賴,默認都是不打開,即 false
        transitive false 
}
複製代碼

2、外部命令執行

我們一般是 使用 Gradle 提供的 exec 來執行外部命令,下面我們就使用 exec 命令來 將當前工程下新生產的 APK 文件拷貝到 電腦下的 Downloads 目錄中,示例代碼如下所示:

/**
 * 使用 exec 執行外部命令
 */

task apkMove() {
    doLast {
        // 在 gradle 的執行階段去執行
        def sourcePath = this.buildDir.path + "/outputs/apk/speed/release/"
        def destinationPath = "/Users/quchao/Downloads/"
        def command = "mv -f $sourcePath $destinationPath"
        exec {
            try {
                executable "bash"
                args "-c", command
                println "The command execute is success"
            } catch (GradleException e) {
                println "The command execute is failed"
            }
        }
    }
}
複製代碼

四、Task

只有 Task 纔可以在 Gradle 的執行階段去執行(其實質是執行的 Task 中的一系列 Action),所以 Task 的重要性不言而喻。

1、從一個例子 🌰 出發

首先,我們可以在任意一個 build.gradle 文件中可以去定義一個 Task,下面是一個完整的示例代碼:

// 1、聲明一個名爲 JsonChao 的 gradle task
task JsonChao
JsonChao {
    // 2、在 JsonChao task 閉包內輸出 hello~,
    // 執行在 gradle 生命週期的第二個階段,即配置階段。
    println("hello~")
    // 3、給 task 附帶一些 執行動作(Action),執行在
    // gradle 生命週期的第三個階段,即執行階段。
    doFirst {
        println("start")
    }
    doLast {
        println("end")
    }
}
// 4、除了上述這種將聲明與配置、Action 分別定義
// 的方式之外,也可以直接將它們結合起來。
// 這裏我們又定義了一個 Android task,它依賴於 JsonChao
// task,也就是說,必須先執行完 JsonChao task,才能
// 去執行 Android task,由此,它們之間便組成了一個
// 有向無環圖:JsonChao task => Android task
task Andorid(dependsOn:"JsonChao") {
    doLast {
        println("end?")
    }
}
複製代碼

首先,在註釋1處,我們聲明瞭一個名爲 JsonChao 的 gradle task。接着,在註釋2處,在 JsonChao task 閉包內輸出了 hello~,這裏的代碼將會執行在 gradle 生命週期的第二個階段,即配置階段。然後,在註釋3處,這裏 給 task 附帶一些了一些執行動作(Action),即 doFirst 與 doLast,它們閉包內的代碼將執行在 gradle 生命週期的第三個階段,即執行階段

對於 doFirst 與 doLast 這兩個 Action,它們的作用分別如下所示:

  • doFirst表示 task 執行最開始的時候被調用的 Action
  • doLast表示 task 將執行完的時候被調用的 Action

需要注意的是,doFirst 和 doLast 是可以被執行多次的

最後,註釋4處,我們可以看到,除了註釋1、2、3處這種將聲明與配置、Action 分別定義的方式之外,也可以直接將它們結合起來。在這裏我們又定義了一個 Android task,它依賴於 JsonChao task,也就是說,必須先執行完 JsonChao task,才能 去執行 Android task,由此,它們之間便組成了一個 有向無環圖:JsonChao task => Android task

執行 Android 這個 gradle task 可以看到如下輸出結果:

> Task :JsonChao
start
end
執行階段,task ':JsonChao'耗時:1ms
:JsonChao spend 4ms
> Task :Andorid
end?
執行階段,task ':Andorid'耗時:1ms
:Andorid spend 2ms
構建結束 
Tasks spend time > 50ms:
執行階段,耗時:15ms
複製代碼

2、Task 的定義及配置

Task 常見的定義方式有 兩種,示例代碼如下所示:

// Task 定義方式1:直接通過 task 函數去創建(在 "()" 可以不指定 group 與 description 屬性)
task myTask1(group: "MyTask", description: "task1") {
    println "This is myTask1"
}

// Task 定義方式2:通過 TaskContainer 去創建 task
this.tasks.create(name: "myTask2") {
    setGroup("MyTask")
    setDescription("task2")
    println "This is myTask2"
}
複製代碼

定義完上述 Task 之後再同步項目,即可看到對應的 Task Group 及其旗下的 Tasks,如下圖所示:

Task 的屬性

需要注意的是,不管是哪一種 task 的定義方式,在 "()" 內我們都可以配置它的一系列屬性,如下:

project.task('JsonChao3'group: "JsonChao"description: "my tasks",
dependsOn: ["JsonChao1""JsonChao2"] ).doLast {
    println "execute JsonChao3 Task"
}
複製代碼

目前 官方所支持的屬性 可以總結爲如下表格:

選型 描述 默認值
"name" task 名字 無,必須指定
"type" 需要創建的 task Class DefaultTask
"action" 當 task 執行的時候,需要執行的閉包 closure 或 行爲 Action null
"overwrite" 替換一個已存在的 task false
"dependsOn" 該 task 所依賴的 task 集合 []
"group" 該 task 所屬組 null
"description" task 的描述信息 null
"constructorArgs" 傳遞到 task Class 構造器中的參數 null

使用 "$" 來引用另一個 task 的屬性

在這裏,我們可以 在當前 task 中使用 "$" 來引用另一個 task 的屬性,示例代碼如下所示:

task Gradle_First() {

}

task Gradle_Last() {
    doLast {
        println "I am not $Gradle_First.name"
    }
}
複製代碼

使用 ext 給 task 自定義需要的屬性

當然,除了使用已有的屬性之外,我們也可以 使用 ext 給 task 自定義需要的屬性,代碼如下所示:

task Gradle_First() {
    ext.good = true
}

task Gradle_Last() {
    doFirst {
        println Gradle_First.good
    }
    doLast {
        println "I am not $Gradle_First.name"
    }
}
複製代碼

使用 defaultTasks 關鍵字標識默認執行任務

此外,我們也可以 使用 defaultTasks 關鍵字 來將一些任務標識爲默認的執行任務,代碼如下所示:

defaultTasks "Gradle_First""Gradle_Last"

task Gradle_First() {
    ext.good = true
}

task Gradle_Last() {
    doFirst {
        println Gradle_First.goodg
    }
    doLast {
        println "I am not $Gradle_First.name"
    }
}
複製代碼

注意事項

每個 task 都會經歷 初始化、配置、執行 這一套完整的生命週期流程

3、Task 的執行詳解

Task 通常使用 doFirst 與 doLast 兩個方式用於在執行期間進行操作。其示例代碼如下所示:

// 使用 Task 在執行階段進行操作
task myTask3(group: "MyTask", description: "task3") {
    println "This is myTask3"
    doFirst {
        // 老二
        println "This group is 2"
    }

    doLast {
        // 老三
        println "This description is 3"
    }
}

// 也可以使用 taskName.doxxx 的方式添加執行任務
myTask3.doFirst {
    // 這種方式的最先執行 => 老大
    println "This group is 1"
}
複製代碼

Task 執行實戰

接下來,我們就使用 doFirst 與 doLast 來進行一下實戰,來實現 計算 build 執行期間的耗時,其完整代碼如下所示:

// Task 執行實戰:計算 build 執行期間的耗時
def startBuildTime, endBuildTime
// 1、在 Gradle 配置階段完成之後進行操作,
// 以此保證要執行的 task 配置完畢
this.afterEvaluate { Project project ->
    // 2、找到當前 project 下第一個執行的 task,即 preBuild task
    def preBuildTask = project.tasks.getByName("preBuild")
    preBuildTask.doFirst {
        // 3、獲取第一個 task 開始執行時刻的時間戳
        startBuildTime = System.currentTimeMillis()
    }
    // 4、找到當前 project 下最後一個執行的 task,即 build task
    def buildTask = project.tasks.getByName("build")
    buildTask.doLast {
        // 5、獲取最後一個 task 執行完成前一瞬間的時間戳
        endBuildTime = System.currentTimeMillis()
        // 6、輸出 build 執行期間的耗時
        println "Current project execute time is ${endBuildTime - startBuildTime}"
    }
}
複製代碼

4、Task 的依賴和執行順序

指定 Task 的執行順序有 三種 方式,如下圖所示:

1)、dependsOn 強依賴方式

dependsOn 強依賴的方式可以細分爲 靜態依賴和動態依賴,示例代碼如下所示:

靜態依賴

task task1 {
    doLast {
        println "This is task1"
    }
}

task task2 {
    doLast {
        println "This is task2"
    }
}

// Task 靜態依賴方式1 (常用)
task task3(dependsOn: [task1, task2]) {
    doLast {
        println "This is task3"
    }
}

// Task 靜態依賴方式2
task3.dependsOn(task1, task2)
複製代碼

動態依賴

// Task 動態依賴方式
task dytask4 {
    dependsOn this.tasks.findAll { task ->
        return task.name.startsWith("task")
    }
    doLast {
        println "This is task4"
    }
}
複製代碼

2)、通過 Task 指定輸入輸出

我們也可以通過 Task 來指定輸入輸出,使用這種方式我們可以 高效地實現一個 自動維護版本發佈文檔的 gradle 腳本,其中輸入輸出相關的代碼如下所示:

task writeTask {
  inputs.property('versionCode'this.versionCode)
  inputs.property('versionName'this.versionName)
  inputs.property('versionInfo'this.versionInfo)
  // 1、指定輸出文件爲 destFile
  outputs.file this.destFile
  doLast {
    //將輸入的內容寫入到輸出文件中去
    def data = inputs.getProperties()
    File file = outputs.getFiles().getSingleFile()
    
    // 寫入版本信息到 XML 文件
    ...
    
}

task readTask {
  // 2、指定輸入文件爲上一個 task(writeTask) 的輸出文件 destFile
  inputs.file this.destFile
  doLast {
    //讀取輸入文件的內容並顯示
    def file = inputs.files.singleFile
    println file.text
  }
}

task outputwithinputTask {
  // 3、先執行寫入,再執行讀取
  dependsOn writeTask, readTask
  doLast {
    println '輸入輸出任務結束'
  }
}
複製代碼

首先,我們定義了一個 WirteTask,然後,在註釋1處,指定了輸出文件爲 destFile, 並寫入版本信息到 XML 文件。接着,定義了一個 readTask,並在註釋2處,指定輸入文件爲上一個 task(即 writeTask) 的輸出文件。最後,在註釋3處,使用 dependsOn 將這兩個 task 關聯起來,此時輸入與輸出的順序是會先執行寫入,再執行讀取。這樣,一個輸入輸出的實際案例就實現了。如果想要查看完整的實現代碼,請查看 Awesome-WanAndroid 的 releaseinfo.gradle 腳本

此外,在 McImage 中就利用了 dependsOn 的方式將自身的 task 插入到了 Gradle 的構建流程之中,關鍵代碼如下所示:

// inject task
(project.tasks.findByName(chmodTask.name) as Task).dependsOn(mergeResourcesTask.taskDependencies.getDependencies(mergeResourcesTask))
(project.tasks.findByName(mcPicTask.name) as Task).dependsOn(project.tasks.findByName(chmodTask.name) as Task)
mergeResourcesTask.dependsOn(project.tasks.findByName(mcPicTask.name))
複製代碼

通過 API 指定依賴順序

除了 dependsOn 的方式,我們還可以在 task 閉包中通過 mustRunAfter 方法指定 task 的依賴順序,需要注意的是,在最新的 gradle api 中,mustRunAfter 必須結合 dependsOn 強依賴進行配套使用,其示例代碼如下所示:

// 通過 API 指定依賴順序
task taskX {
    mustRunAfter "taskY"

    doFirst {
        println "this is taskX"
    }
}

task taskY {
    // 使用 mustRunAfter 指定依賴的(一至多個)前置 task
    // 也可以使用 shouldRunAfter 的方式,但是是非強制的依賴
//    shouldRunAfter taskA
    doFirst {
        println "this is taskY"
    }
}

task taskZ(dependsOn: [taskX, taskY]) {
    mustRunAfter "taskY"
    doFirst {
        println "this is taskZ"
    }
}
複製代碼

5、Task 類型

除了定義一個新的 task 之外,我們也可以使用 type 屬性來直接使用一個已有的 task 類型(很多文章都說的是繼承一個已有的類,不是很準確),比如 Gradle 自帶的 Copy、Delete、Sync task 等等。示例代碼如下所示:

// 1、刪除根目錄下的 build 文件
task clean(type: Delete) {
    delete rootProject.buildDir
}
// 2、將 doc 複製到 build/target 目錄下
task copyDocs(type: Copy) {
    from 'src/main/doc'
    into 'build/target/doc'
}
// 3、執行時會複製源文件到目標目錄,然後從目標目錄刪除所有非複製文件
task syncFile(type:Sync) {
    from 'src/main/doc'
    into 'build/target/doc'
}
複製代碼

6、掛接到構建生命週期

我們可以使用 gradle 提供的一系列生命週期 API 去掛接我們自己的 task 到構建生命週期之中,比如使用 afterEvaluate 方法 將我們第三小節定義的 writeTask 掛接到 gradle 配置完所有的 task 之後的時刻,示例代碼如下所示:

// 在配置階段執行完之後執行 writeTask
this.project.afterEvaluate { project ->
  def buildTask = project.tasks.findByName("build")
  doLast {
    buildTask.doLast {
      writeTask.execute()
    }
  }
}
複製代碼

需要注意的是,配置完成之後,我們需要在 app moudle 下引入我們定義的 releaseinfo 腳本,引入方式如下:

apply from: this.project.file("releaseinfo.gradle")
複製代碼

五、SourceSet

SourceSet 主要是 用來設置我們項目中源碼或資源的位置的,目前它最常見的兩個使用案例就是如下 兩類

  • 1)、修改 so 庫存放位置
  • 2)、資源文件分包存放

1、修改 so 庫存放位置

我們僅需在 app moudle 下的 android 閉包下配置如下代碼即可修改 so 庫存放位置:

android {
    ...
    sourceSets {
        main {
            // 修改 so 庫存放位置
            jniLibs.srcDirs = ["libs"]
        }
    }
}
複製代碼

2、資源文件分包存放

同樣,在 app moudle 下的 android 閉包下配置如下代碼即可將資源文件進行分包存放:

android {
    sourceSets {
        main {
            res.srcDirs = ["src/main/res",
                           "src/main/res-play",
                           "src/main/res-shop"
                            ... 
                           ]
        }
    }
}
複製代碼

此外,我們也可以使用如下代碼 將 sourceSets 在 android 閉包的外部進行定義

this.android.sourceSets {
    ...
}
複製代碼

六、Gradle 命令

Gradle 的命令有很多,但是我們通常只會使用如下兩種類型的命令:

  • 1)、獲取構建信息的命令
  • 2)、執行 task 的命令

1、獲取構建信息的命令

// 1、按自頂向下的結構列出子項目的名稱列表
./gradlew projects
// 2、分類列出項目中所有的任務
./gradlew tasks
// 3、列出項目的依賴列表
./gradlew dependencies
複製代碼

2、執行 task 的命令

常規的用於執行 task 的命令有 四種,如下所示:

// 1、用於執行多個 task 任務
./gradlew JsonChao Gradle_Last
// 2、使用 -x 排除單個 task 任務
./gradlew -x JsonChao
// 3、使用 -continue 可以在構建失敗後繼續執行下面的構建命令
./gradlew -continue JsonChao
// 4、建議使用簡化的 task name 去執行 task,下面的命令用於執行 
// Gradle_Last 這個 task
./gradlew G_Last
複製代碼

而對於子目錄下定義的 task,我們通常會使用如下的命令來執行它:

// 1、使用 -b 執行 app 目錄下定義的 task
./gradlew -b app/build.gradle MyTask
// 2、在大型項目中我們一般使用更加智能的 -p 來替代 -b
./gradlew -p app MyTask
複製代碼

七、總結

至此,我們就將 Gradle 的核心 API 部分講解完畢了,這裏我們再來回顧一下本文的要點,如下所示:

  • 一、Gradle 優勢
    • 1、更好的靈活性
    • 2、更細的粒度
    • 3、更好的擴展性
    • 4、更強的兼容性
  • 二、Gradle 構建生命週期
    • 1、初始化階段
    • 2、配置階段
    • 3、執行階段
    • 4、Hook Gradle 各個生命週期節點
    • 5、獲取構建各個階段、任務的耗時情況
  • 三、Project
    • 1、Project 核心 API 分解
    • 2、Project API
    • 3、project 屬性
    • 4、文件相關 API
    • 5、其它 API
  • 四、Task
    • 1、從一個例子 🌰 出發
    • 2、Task 的定義及配置
    • 3、Task 的執行詳解
    • 4、Task 的依賴和執行順序
    • 5、Task 類型
    • 6、掛接到構建生命週期
  • 五、SourceSet
    • 1、修改 so 庫存放位置
    • 2、資源文件分包存放
  • 六、Gradle 命令
    • 1、獲取構建信息的命令
    • 2、執行 task 的命令

Gradle 的核心 API 非常重要,這對我們高效實現一個 Gradle 插件無疑是必不可少的。因爲 只有紮實基礎才能走的更遠,願我們能一同前行

公衆號

我的公衆號 JsonChao 開通啦,如果您想第一時間獲取最新文章和最新動態,歡迎掃描關注~

參考鏈接:


Contanct Me

● 微信:

歡迎關注我的微信:bcce5360

● 微信羣:

由於微信羣已超過 200 人,麻煩大家想進微信羣的朋友們,加我微信拉你進羣。

● QQ羣:

2千人QQ羣,Awesome-Android學習交流羣,QQ羣號:959936182, 歡迎大家加入~

About me

很感謝您閱讀這篇文章,希望您能將它分享給您的朋友或技術羣,這對我意義重大。

希望我們能成爲朋友,在 Github掘金上一起分享知識。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章