Android Gradle系列-原理篇

clipboard.png

上週我們在Android Gradle系列-入門篇文章中已經將gradle在項目中的結構過了一遍。對於gradle,我們許多時候都不需要修改類似與*.gradle文件,做的最多的應該是在dependencies中添加第三方依賴,或者說修改sdk版本號,亦或者每次發版本改下versionCode與versionName。即使碰到問題也是直接上google尋找答案,而並沒有真正理解它爲什麼要這麼做,或者它是如何運行的?

今天,我會通過這篇文章一步一步的編寫gradle文件,從項目的創建,到gradle的配置。相信有了這篇文章,你將對gradle的內部運行將有一個全新的認識。

Groovy

在講gradle之前,我們還需明白一點,gradle語法是基於groovy的。所以我們先來了解一些groovy的知識,這有助於我們之後的理解。當然如果你已經有groovy的基礎你可以直接跳過,沒有的也不用慌,因爲只要你懂java就不是什麼難題。

syntax

下面我將通過code的形式,列出幾點

  • 當調用的方法有參數時,可以不用(),看下面的例子
def printAge(String name, int age) {
    print("$name is $age years old")
}
 
def printEmptyLine() {
    println()
}
 
def callClosure(Closure closure) {
    closure()
}
 
printAge "John", 24 //輸出John is 24 years old
printEmptyLine() //輸出空行
callClosure { println("From closure") } //輸出From closure
  • 如果最後的參數是閉包,可以將它寫在括號的外面
def callWithParam(String param, Closure<String> closure) {
    closure(param)
}
 
callWithParam("param", { println it }) //輸出param
callWithParam("param") { println it } //輸出param
callWithParam "param", { println it } //輸出param
  • 調用方法時可以指定參數名進行傳參,有指定的會轉化到Map對象中,沒有的將按正常傳參
def printPersonInfo(Map<String, Object> person) {
    println("${person.name} is ${person.age} years old")
}
 
def printJobInfo(Map<String, Object> job, String employeeName) {
    println("${employeeName} works as ${job.name} at ${job.company}")
}
 
printPersonInfo name: "Jake", age: 29
printJobInfo "Payne", name: "Android Engineer", company: "Google"

你會發現他們的調用都不需要括號,同時printJobInfo的調用參數的順序不受影響。

Closure

在gradle中你會發現許多閉包,所以我們需要對閉包有一定的瞭解。如果你熟悉kotlin,它與Function literals with receiver類似。

在groovy中我們可以將Closures當做成lambdas,所以它可以直接當做代碼塊執行,可以有參數,也可以有返回值。但是不同的是它可以改變其自身的代理。例如:

class DelegateOne {
    def callContent(String content) {
        println "From delegateOne: $content"
    }
}
 
class DelegateTow {
    def callContent(String content) {
        println "From delegateTwo: $content"
    }
}
 
def callClosure = {
    callContent "I am bird"
}
 
callClosure.delegate = new DelegateOne()
callClosure() //輸出From delegateOne: I am bird
callClosure.delegate = new DelegateTow()
callClosure() //輸出From delegateTow: I am bird

通過改變callClosure的delegate,讓其調用不同的callContent。
如果你想了解更多,可以直接閱讀groovy文檔

Gradle

在上篇文章中已經提到有關gradle的腳步相關的知識,這裏就不再累贅。
下面我們來一步一步構建gradle。

搭建項目層級

首先我們新建一個文件夾example,cd進入該文件夾,在該目錄下執行gradle projects,你會發現它已經是一個gradle項目了

$ gradle projects
> Task :projects
 
------------------------------------------------------------
Root project
------------------------------------------------------------
 
Root project 'example'
No sub-projects
 
To see a list of the tasks of a project, run gradle <project-path>:tasks
For example, try running gradle :tasks
 
BUILD SUCCESSFUL in 5s

因爲這裏不是在Android Studio中創建的項目,所以如果你本地沒有安裝與配置gradle環境,將不會有gradle命令。所以這一點要注意一下。

每一個android項目在它的root project下都需要配置一個settings.gradle,它代表着項目的全局配置。同時使用void include(String[] projectPaths)方法來添加子項目,例如我們爲example添加app子項目

$ echo "include ':app'" > settings.gradle
$ gradle projects
> Task :projects
 
------------------------------------------------------------
Root project
------------------------------------------------------------
 
Root project 'example'
\--- Project ':app'
 
To see a list of the tasks of a project, run gradle <project-path>:tasks
For example, try running gradle :app:tasks
 
BUILD SUCCESSFUL in 1s
1 actionable task: 1 executed

:app中的:代表的是路徑的分隔符,同時在settings.gradle中默認root project是該文件的文件夾名稱,也可以通過rootProject.name = name來進行修改。

搭建Android子項目

現在需要做的是將子項目app構建成Android項目,所以我們需要配置app的build.gradle。因爲gradle只是構建工具,它是根據不同的插件來構建不同的項目,所以爲了符合Android的構建,需要申明應用的插件。這裏通過apply方法,它有以下三種類型

void apply(Closure closure)
void apply(Map<String, ?> options)
void apply(Action<? super ObjectConfigurationAction> action)

這裏我們使用的是第二種,它的map參數需要與ObjectConfigurationAction中的方法名相匹配,而它的方法名有以下三種

  • from: 應用一個腳本文件
  • plugin: 應用一個插件,通過id或者class名
  • to: 應用一個目標代理對象

因爲我們要使用android插件,所以需要使用apply(plugin: 'com.android.application'),又由於groovy的語法特性,可以將括號省略,所以最終在build.gradle中的表現可以如下:

$ echo "apply plugin: 'com.android.application'" > app/build.gradle

添加完以後,再來執行一下

$ gradle app:tasks

FAILURE: Build failed with an exception.
 
* Where:
Build file '/Users/idisfkj/example/app/build.gradle' line: 1
 
* What went wrong:
A problem occurred evaluating project ':app'.
> Plugin with id 'com.android.application' not found.
 
* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.
 
* Get more help at https://help.gradle.org
 
BUILD FAILED in 6s

發現報錯了,顯示com.android.application的插件id找不到。這正常,因爲我們還沒有聲明它。所以下面我們要在project下的build.gradle中聲明它。爲什麼不直接到app下的build.gradle聲明呢?是因爲我們是android項目,project可以有多個sub-project,所以爲了防止在子項目中重複聲明,統一到主項目中聲明。

project的build.gradle聲明插件需要在buildscript中,而buildscript會通過ScriptHandler來執行,以至於sub-project也能夠使用。所以最終的申明如下:

buildscript {
    repositories {
        google()
        jcenter()
    }
 
    dependencies {
        classpath 'com.android.tools.build:gradle:3.3.2'
    }
}

上面的buildscript、repositories與dependencies方法都是以Closure作爲參數,然後再通過delegate進行調用

  • buildscript(Closure)在Project中調用,通過ScriptHandler來執行Closure
  • repositories(Closure)在ScriptHandler中調用,通過RepositoryHandler來執行Closure
  • dependencies(Closure)在ScriptHandler中調用,通過DependencyHandler來執行Closure

相應的google()與jcenter()會在RepositoryHandler執行,classpaht(String)會在DependencyHandler(*)執行。

如果你想更詳細的瞭解可以查看文檔

讓我們再一次執行gradle projects

$ gradle projects

FAILURE: Build failed with an exception.
 
* What went wrong:
A problem occurred configuring project ':app'.
> compileSdkVersion is not specified.
 
* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.
 
* Get more help at https://help.gradle.org
 
BUILD FAILED in 1s

發現報沒有指定compileSdkVersion,因爲我們還沒有對app進行相關的配置,只是引用了android插件。所以我們現在來進行基本配置,在app/build.gradle中添加

android {
   buildToolsVersion "28.0.1"
   compileSdkVersion 28
}

我們在android中進行聲明,android方法會加入到project實例中。buildToolsVersion與compileSdkVersion將通過Closure對象進行delegate。

Extensions

android方法會是如何與project進行關聯的?在我們聲明的Android插件中,會註冊一個AppExtension類,這個extension將會與android命名。所以gradle能夠調用android方法,而在AppExtension中已經聲明瞭各種方法屬性,例如buildTypes、defaultConfig與signingConfigs等。這也就是爲什麼我們能夠在android方法中調用它們的原因。下面是extension的創建部分源碼

    @Override
    void apply(Project project) {
        super.apply(project)
        // This is for testing.
        if (pluginHolder != null) {
            pluginHolder.plugin = this;
        }
        def buildTypeContainer = project.container(DefaultBuildType,
                new BuildTypeFactory(instantiator,  project.fileResolver))
        def productFlavorContainer = project.container(GroupableProductFlavorDsl,
                new GroupableProductFlavorFactory(instantiator, project.fileResolver))
        def signingConfigContainer = project.container(SigningConfig,
                new SigningConfigFactory(instantiator))
        extension = project.extensions.create('android', AppExtension,
                this, (ProjectInternal) project, instantiator,
                buildTypeContainer, productFlavorContainer, signingConfigContainer)
        setBaseExtension(extension)
        ...
   }

Dependencies

android方法下面就是dependencies,下面我們再來看dependencies

dependencies {
    implementation 'io.reactivex.rxjava2:rxjava:2.0.4'
    testImplementation 'junit:junit:4.12'
    annotationProcessor 'org.parceler:parceler:1.1.6'
}

有了上面的基礎,應該會容易理解。dependencies是會被delegate給DependencyHandler,不過如果你到DependencyHandler中去查找,會發現找不到上面的implementation、testImplementation等方法。那它們有到底是怎麼來的呢?亦或者如果我們添加了dev flavor,那麼我又可以使用devImplementation。這裏就涉及到了groovy的methodMissing方法。它能夠在runtime(*)中捕獲到沒有定義的方法。

至於(*)是gradle的methodMissing中的一個抽象感念,它申明在MethodMixIn中。

對於DependencyHandler的實現規則是:
在DependencyHandler中如果我們回調了一個沒有定義的方法,且它有相應的參數;同時它的方法名在configuration(*)中;那麼將會根據方法名與參數類型來調用doAdd的相應方法。

對於configuration(*),每一個plugin都有他們自己的配置,例如java插件定義了compile、compileClassPath、testCompile等。而對於Android插件在這基礎上還會定義annotationProcessor,(variant)Implementation、(variant)TestImplementation等。對於variant則是基於你設置的buildTypes與flavors。

另一方面,由於doAdd()是私用的方法,但add()是公用的方法,所以在dependencies中我們可以直接使用add

dependencies {
    add('implementation', 'io.reactivex.rxjava2:rxjava:2.0.4')
    add('testImplementation', 'junit:junit:4.12')
    add('annotationProcessor', 'org.parceler:parceler:1.1.6')
}

注意,這種寫法並不推薦,這裏只是爲了更好的理解它的原理。

gradle的知識點還有很多,這只是對有關Android的一部分進行分析。當我們進行gradle配置的時,不至於對gradle的語法感到魔幻,或者對它的一些操作感到不解。

我在github上建了一個倉庫Android精華錄,收集Android相關的文章,如果有需要的可以去看一下,有好的文章可以加我微信fan331100推薦給我。

clipboard.png

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