寫給 Android 開發者的 Gradle 系列(三)撰寫 plugin

揪克發表於2018-05-21

歡迎關注本人公眾號,掃描下方二維碼或搜尋公眾號 id: mxszgg

寫給 Android 開發者的 Gradle 系列(三)撰寫 plugin

本文基於 Android Gradle plugin 3.0.1

前言

前文中筆者闡述道 task 就相當於函式,那麼這篇文章所要介紹的 plugin 就相當於函式庫了。畢竟在 build.gradle 檔案中撰寫大量的 task 是肯定不好維護的,所以可以將 tasks 做成 plugin 然後直接 apply 就好了。

就像在 app/build.gradleapply plugin: 'com.android.application' 這樣 appProject 就可以使用該 plugin 中的 task 了。

準備工作

  1. 新建一個 Android 專案。
  2. 新建一個 java library module,該 module 必須命名為 buildSrc
  3. src/main/java 改成 src/main/groovy

基本實現

  1. 新建一個 xxxPlugin.groovy 並實現 Plugin 介面,例如:

    import org.gradle.api.Plugin
    import org.gradle.api.Project
    
    class TestPlugin implements Plugin<Project> {
      @Override
      void apply(Project project) {
        project.task('pluginTest') {
          doLast {
            println 'Hello World'
          }
        }
      }
    }
    複製程式碼

可以看到,上述 plugin 僅是在 apply() 方法內部建立了一個名為 pluginTest 的 task。

由於 Kotlin/Java 與 groovy 的相容,所以並非一定要建立 groovy 檔案,也可以是 xxxPlugin.java/xxxPlugin.kotlin。

  1. 既然 plugin 已經就這麼簡單的實現了,那麼如何應用到實際專案中呢?在 build.gradle 檔案中新增如下資訊:

apply plugin: TestPlugin

至此之後,不妨在命令列呼叫 pluginTest task 看看是否有效果——

./gradlew pluginTest

> Task :app:testPlugin

Hello from the TestPlugin

擴充套件

隨著專案的急速發展,有朝一日發現有時候不想輸出 Hello World 而是希望這個 pluginTest task 可以根據開發者的需求進行配置。

  1. 建立一個 xxxExtension.groovy 檔案(當然,也可以用 Java/Kotlin 來寫),實際上就是和 JavaBean 差不多的類,類似如下:

    class TestPluginExtension {
      String message = 'Hello World'
    }
    複製程式碼
  2. 在 Plugin 類中獲取閉包資訊,並輸出:

    class TestPlugin implements Plugin<Project> {
        void apply(Project project) {
            // Add the 'testExtension' extension object
            def extension = project.extensions.create('testExtension', TestPluginExtension)
            project.task('pluginTest') {
                doLast {
                    println extension.message
                }
            }
        }
    }
    複製程式碼

    第四行通過 project.extensions.create(String name, Class<T> type, Object... constructionArguments) 來獲取 testExtension 閉包中的內容並通過反射將閉包的內容轉換成一個 TestPluginExtension 物件。

  3. build.gradle 中新增一個 testExtension 閉包:

    testExtension {
     message 'Hello Gradle'
    }
    複製程式碼
  4. 在命令列鍵入以下資訊:

./gradlew pluginTest

將會看到輸出結果——

> Task :app:pluginTest

Hello Gradle

專案化

到目前為止談及到的東西都還是一個普通的、不可以釋出到倉庫的外掛,如果想要將外掛釋出出去供他人和自己在專案中 apply,需要進行以下步驟將外掛變成一個 Project——

  1. 更改 build.gradle 檔案內容:

    apply plugin: 'groovy'
    
    dependencies {
        compile gradleApi()
        compile localGroovy()
    }
    複製程式碼

    此時可以觀察到 External Libraries 中多出了 gradle-api/gradle-installation-beacon/groovy 庫。其中,gradle 的版本是基於專案下 gradle wrapper 中配置的版本——

    這裡寫圖片描述
    這裡寫圖片描述

  2. 建立 src/main/resources/META-INF/gradle-plugins/外掛名.properties,例如 src/main/resources/META-INF/gradle-plugins/com.sample.test.properties,然後將 properities 檔案內容改為 implementation-class=Plugin 路徑,例如 implementation-class=com.sample.test.TestPlugin

  3. build.gradle 檔案中通過 apply plugin: '外掛名' 引入外掛 —— apply plugin: 'com.sample.test'

  4. 在命令列鍵入以下資訊:

./gradlew pluginTest

將會看到輸出結果——

> Task :app:pluginTest

Hello Gradle

當然,以上僅是告訴各位讀者如何將 plugin 專案化,並未涉及到如何將 plugin 提交到倉庫中,關於 jcenter 倉庫提交方式可借鑑手摸手教你如何把專案提交到 jcenter,其他倉庫提交方式讀者可自行搜尋。

實戰

Android 打包過程中,一個 task 接著一個 task 的執行,每個 task 都會執行一段特定的事情(例如第一篇文章中提到的幾個 task),所以在 Gradle 外掛的開發中,如果是針對打包流程的更改,實際上大部分都是 hook 某一個 task 來達到目的——例如我司的 mess 通過 hook transformClassesAndResourcesWithProguardForDebug task (Gradle v2.0+ task)來實現對四大元件以及 View 的混淆的;美麗說的 ThinRPlugin 是通過 hook transformClassesWithDexForDebug(Gradle v2.0+ task)來實現精簡 R.class/R2.class 的。

因為 Android 現有的 task 已經很完善了,所以如果想要達到目的,只需要瞭解相應的 task 並在其之前或之後做一些操作即可。

為了示例而示例的簡單例子實在不多,筆者只能拿起上篇文章中的示例——在 app 目錄下建立 pic 資料夾,並新增一個名為 test 的 png 圖片,hook apk 打包流程將該圖片新增入 apk 的 assets 資料夾。

儘管這看起來真的很沒有卵用。

這次為了符合實際開發要求,不妨提升一定的難度——僅在 release 包中向 assets 新增圖片,而 debug 包不向 assets 中新增圖片。在實際開發中有很多這樣的需求,例如前文提到的 mess 是對 apk 原始碼進行混淆的,那麼日常開發者執行的 debug 包有必要執行該 task 麼?顯然並不需要,應該僅在釋出的時候打 release 包的時候執行該 task 就好了。

那麼如何知道當前 task 是為 release 服務的呢?簡單的尋找到 name 為 packageRelease 的 task 是肯定不行的,日常開發中專案時常有很多種變體,例如在 app/build.gradle 中輸入以下程式碼:

android {
	...
	flavorDimensions "api", "mode"
	
	productFlavors {
   		demo {
      		dimension "mode"
    	}

    	full {
      		dimension "mode"
    	}

    	minApi23 {
      		dimension "api"
      		minSdkVersion '23'
    	}

    	minApi21 {
      		dimension "api"
      		minSdkVersion '21'
    	}
  }
複製程式碼

此時的變種共有 3 (debug、release、androidTest) * 2(demo、full) * 2(minApi23、minApi21)共計12種,截圖如下:

這裡寫圖片描述

那麼如何為以上所有的 release 變種包的 assets 中都填入圖片呢?

根據官方文件可以知道開發者可以通過 android.applicationVariants.all 獲取到當前所有的 apk 變體,該變體的型別為 ApplicationVariant,其父類 BaseVariantOutput 中含 name 欄位,該欄位實際上就是當前變體的名字,那麼其實只需要判斷該 name 欄位是否包含 release 關鍵字即可。

建立 plugin 的基本流程已經在前文中闡述過了,直接進行核心 plugin 的撰寫,HookAssetsPlugin 原始碼如下:

import com.android.build.gradle.api.ApkVariantOutput
import com.android.build.gradle.api.ApplicationVariant
import com.android.build.gradle.tasks.PackageApplication
import org.gradle.api.Plugin
import org.gradle.api.Project

class HookAssetsPlugin implements Plugin<Project> {
  @Override
  void apply(Project project) {
    project.afterEvaluate {
      project.plugins.withId('com.android.application') {
        project.android.applicationVariants.all { ApplicationVariant variant ->
          variant.outputs.each { ApkVariantOutput variantOutput ->
            if (variantOutput.name.equalsIgnoreCase("release")) {
              variantOutput.packageApplication.doFirst { PackageApplication task ->
                project.copy {
                  from "${project.projectDir.absolutePath}/pic/test.png"
                  into "${task.assets.asPath}"
                }
              }
            }
          }
        }
      }
    }
  }
}
複製程式碼
  1. 第一篇文中就闡述過,只能在 project.afterEvaluate 閉包中才能獲取到當前 project 中的所有 task 。

  2. 通過 project.plugins.withId('com.android.application') 確保當前 project 是 Android app project 而不是 Android library project,以此來避免無效操作,畢竟 package task 是 com.android.application 中的 task。

  3. 通過 project.android.applicationVariants.all 獲取所有變體資訊。

  4. 通過觀察 ApplicationVariant 類的父類 BaseVariant 中 outputs 欄位可知道該欄位代表著當前變體的輸出資訊(DomainObjectCollection 型別),BaseVariantOutput 的子類 ApkVariantOutput 中的 packageApplication 即為上一篇文章中所說的 PackageAndroidArtifact task 了。

  5. 判斷當前變體是否是 release 的變體。(通過 variantOutput.name.equalsIgnoreCase("release")/variant.name.equalsIgnoreCase("release") 都是可以的。)

  6. hook 步驟4中所說的 PackageAndroidArtifact task,將圖片複製到 assets 中。

實際上,在日常開發中尋找 task 的方式可能更多的是使用 project.tasks.findByName(name)/project.tasks.getByName(name),這樣也更加方便,筆者在 demo 中附帶了此種寫法,原始碼戳我

後續

除了上面提到的 messmess 原始碼解析) 和 ThinRPlugin (筆者將會在後續的文章中對 ThinRPlugin 的原始碼進行解析)以外,筆者瞭解到的還有一些以下知名的 Gradle 外掛可供讀者學習:

當然,前面提到的幾個 plugin 有些重量級,輕量級的筆者沒有了解多少,只能推薦mess 原始碼解析作者的一個快速生成R2.java中fields的外掛一個快速將指定class打入maindex的外掛,對新手瞭解 Gradle plugin 還是很友好的。

本文實戰模組的原始碼連結:請戳我

筆者新建了微信群,如果讀者有問題或者對筆者感興趣,歡迎加入,由於滿了100人,需要先加筆者的微信,微信備註:入群。

寫給 Android 開發者的 Gradle 系列(三)撰寫 plugin

相關文章