一起入門gradle自定義外掛編寫(二) - 深入理解build.gradle
這篇部落格我們來通過groovy的語法去深入理解build.gradle的底層實現。
通過分析build.gradle裡面的實現原理,我們在寫自己的自定義gradle外掛的時候就能使用同樣的配置方法了。
在上一篇部落格裡面提到,在gradle檔案裡面預設使用的都是project這個物件的方法或者屬性,並且分析了apply方法的完整形式:
project.apply(['plugin': 'com.android.application'])
其實android,和dependencies程式碼塊也是一樣的,省略了project物件,新增上之後變成這樣:
project.android { compileSdkVersion 28 defaultConfig { applicationId "me.linjw.demo" minSdkVersion 24 targetSdkVersion 28 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } project.dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'com.android.support:appcompat-v7:28.0.0' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' }
我們先講dependencies,按住ctrl鍵用滑鼠點選它可以跳轉到到Project介面的void dependencies(Closure configureClosure)方法
也就是說它其實是project的一個方法,傳入一個Closure物件作為引數.然後這裡是省略了方法的括號,它的完整形式如下:
project.dependencies({ implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'com.android.support:appcompat-v7:28.0.0' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' })
groovy閉包
這個Closure就是閉包的意思,閉包作為指令碼語言裡面比較常見的東西我就不過多介紹了,感興趣的同學可以自行搜尋.
groovy裡的閉包就是用話括號來定義的,可以看看下面閉包的例子:
//定義閉包並且把它賦值給closure變數 def closure = { println('hello world!') } //呼叫閉包 closure()
這裡的closure()會呼叫閉包的方法,打印出”hello world!”
這裡的閉包也是一個省略的寫法,它的完整寫法如下:
def closure = {-> println('hello world!') }
“->”左邊是閉包的輸入引數,由於這裡不需要輸入引數,所以它左邊沒有東西.我們可以看看下面的例子,這個閉包接收兩個引數:
def closure = { str1, str2 -> println(str1 + ' ' + str2) } closure('hello', 'world')
特殊的,如果閉包只接收一個引數,也可以省略引數名和”->”,它會預設包含一個隱式的引數it:
def closure = { println(it) } closure('hello world!') // 列印hello world! closure() // 列印null
可以看到,如果只有一個引數的話在呼叫閉包的時候可以不傳引數,它會預設傳入null.
delegate
dependencies方法傳入的閉包裡面的implementation其實也是呼叫的方法,我們補全它們的括號
project.dependencies({ implementation(fileTree(dir: 'libs', include: ['*.jar'])) implementation('com.android.support:appcompat-v7:28.0.0') testImplementation('junit:junit:4.12') androidTestImplementation('com.android.support.test:runner:1.0.2') androidTestImplementation('com.android.support.test.espresso:espresso-core:3.0.2') })
那這個implementation方法又是哪裡來的呢?是groovy閉包自帶的方法?還是全域性的方法?
其實都不是,這裡我們先從閉包的delegate說起,顧名思義它是閉包的一個委託物件,閉包中沒有的方法都會調到它那裡去.
我們來看下面的例子,在閉包中呼叫foo()方法,呼叫的時候會報錯,因為找不到foo()方法:
def closure = { foo() } closure() // 報錯,找不到foo()方法
如果我們定義一個類,裡面實現foo方法,然後將這個類設定成閉包的delegate,則在閉包中找不到foo()方法的時候就會去它的代理中找:
class TestClass { def foo() { println('foo') } } def closure = { foo() println(delegate) } closure.delegate = new TestClass() closure() // 先在TestClass.foo方法中列印'foo',然後列印閉包的delegate物件'TestClass@755e1c30'
這個時候讓我們看看dependencies閉包的delegate:
project.dependencies({ println(delegate) implementation(fileTree(dir: 'libs', include: ['*.jar'])) implementation('com.android.support:appcompat-v7:28.0.0') testImplementation('junit:junit:4.12') androidTestImplementation('com.android.support.test:runner:1.0.2') androidTestImplementation('com.android.support.test.espresso:espresso-core:3.0.2') })
輸出為
org.gradle.api.internal.artifacts.dsl.dependencies.DefaultDependencyHandler_Decorated@ee11179
這個DefaultDependencyHandler_Decorated東西我們不用細究,只要知道它是DefaultDependencyHandler的子類就行
dependencies的原理
我們都知道當我們在配置了productFlavors的時候,可以為每個product單獨配置依賴庫
android { ... productFlavors { demo { } } } dependencies { ... demoImplementation 'com.google.code.gson:gson:2.6.2' }
但是這個demoImplementation方法又是怎麼生成的呢?
其實最後它們都是呼叫到了DefaultDependencyHandler.add方法,也就是說我們的dependencies其實實際的形式是這樣的:
project.dependencies({ add('implementation', fileTree(dir: 'libs', include: ['*.jar'])) add('implementation', 'com.android.support:appcompat-v7:28.0.0') add('testImplementation', 'junit:junit:4.12') add('androidTestImplementation', 'com.android.support.test:runner:1.0.2') add('androidTestImplementation', 'com.android.support.test.espresso:espresso-core:3.0.2') add('demoImplementation', 'com.google.code.gson:gson:2.6.2') })
這個add方法是怎麼呼叫到的呢?groovy裡面可以有幾種方法做到,這裡就講一種:
class Delegate { def invokeMethod(String name, args) { println('method : ' + name) println('args : ' + args) } } def closure = { demoImplementation 'com.google.code.gson:gson:2.6.2' } closure.delegate = new Delegate() closure()
上面的例子,我們在閉包中呼叫了delegate中也沒有的方法demoImplementation,這個時候會呼叫delegate的invokeMethod,列印如下:
method : demoImplementation args : [com.google.code.gson:gson:2.6.2]
所以這個時候我們就可以在這個invokeMethod方法裡面給每個product配置依賴了。
Extension
與project.dependencies不同project.android,project裡面並沒有一個方法叫做android。
那這個project.android方法是怎麼呼叫的呢?它是通過project的一個Extension,也就是project的一個拓展。
這個拓展是怎麼來的呢?可以看看下面的程式碼:
class MyAndroid { def compileSdkVersion; def compileSdkVersion(compileSdkVersion) { this.compileSdkVersion = compileSdkVersion } } project.extensions.add('myAndroid', new MyAndroid()) project.myAndroid { compileSdkVersion 28 }
我們只需要使用project.extensions.add方法加入一個名字叫做myAndroid的Extension,gradle就會為我們在project裡面新增一個名字叫做myAndroid的方法,接收一個閉包,然後在這個方法裡面會將傳入的閉包的delegate設定成我們new出來的MyAndroid物件。
metaClass
這個Extension又是怎麼實現的呢?
其實指令碼語言一般都支援動態新增方法和屬性,groovy同樣也支援。
我們在groovy中可以使用metaClass進行執行是超程式設計,動態建立類、方法等
例如,下面程式碼中我們給Demo類動態添加了hello屬性和sayHello方法:
class Demo { } Demo.metaClass."hello" = "hello world" Demo.metaClass."sayHello" = { println("hello world") } Demo demo = new Demo() demo.sayHello() println(demo.hello)
甚至當重名的時候它還會根據我們設定的是值還是閉包幫我們分別建立屬性和方法:
class Demo { } Demo.metaClass."hello" = "hello world" Demo.metaClass."hello" = { println("hello world") } Demo demo = new Demo() demo.hello() println(demo.hello)
有了這個超程式設計的技術,要實現Extension就簡單了:
def addExtensions(String name, Object handler) { project.metaClass."$name" = { it -> it.delegate = handler it() } project.metaClass."$name" = handler } class MyAndroid { def compileSdkVersion; def compileSdkVersion(compileSdkVersion) { this.compileSdkVersion = compileSdkVersion } } addExtensions('myAndroid', new MyAndroid()) project.myAndroid { compileSdkVersion 28 } println(project.myAndroid.compileSdkVersion)