Android Gradle外掛

夢和遠方發表於2021-06-22

Gradle外掛練習地址:https://github.com/peiniwan/ASMLifeCycleTest

什麼是Gradle

Gradle 是一個基於 Apache Ant 和 Apache Maven 概念的專案自動化構建工具。Gradle 就是工程的管理,幫我們做了依賴、打包、部署、釋出、各種渠道的差異管理等工作。
Gradle指令碼是基於Groovy語言來編譯執行的,Java、Groovy、Kotlin等都是基於JVM執行的,所以他們在語法上共性很多,熟悉Java的同學應該對Groovy上手很快

編寫方法

在 Android 下的 gradle 外掛共分為 兩大類:

  • 指令碼外掛:同普通的 gradle 指令碼編寫形式一樣,可以直接寫在build.gradle檔案中,也可以自己新建一個 gradle 指令碼檔案中寫
  • 物件外掛:通過外掛全路徑類名或 id 引用,它主要有 三種編寫形式,如下所示:
    1)在當前構建指令碼下直接編寫。
    2)在 buildSrc 目錄下編寫。
    3)在完全獨立的專案中編寫。

buildSrc

由於buildSrc目錄是gradle預設的目錄之一,該目錄下的程式碼會在構建是自動編譯打包,並被新增到buildScript中的classpath下,所以不需要任何額外的配置,就可以直接被其他模組的構建指令碼所引用。
這就是:buildScript

在buildSrc/src/main目錄下,再分別建立groovy、resources資料夾。

隨便定義的需要自己寫classpath:

優點:

  • 專案構建時,Gradle 會自動編譯專案目錄下的 buildSrc 資料夾下的構建指令碼和原始碼,並將其新增到專案構建指令碼的 classpath 中,因此在使用 buildSrc 中建立的外掛時,無需再手動指定 classpath(依賴的名字)(當然也可以自己建立id)
  • buildSrc 資料夾中構建指令碼和 Gradle 外掛同一專案均可見,因此同一專案中的其他模組也可以使用 buildSrc 中建立的外掛
  • 不需要 uploadArchives task

缺點:
此處建立的外掛對外部專案不可見,無法在其他專案中複用

id引入
引用的方式可以是通過類名引用,也可以通過給外掛對映一個id,然後通過id引用。
通過類名引用外掛的需要使用全限定名,也就是需要帶上包名,或者可以先匯入這個外掛類,如下

// 在app模組下的build.gradle檔案中引用
apply plugin:com.wings.gradle.CustomBuildSrcPlugin
或者

// 在app模組下的build.gradle檔案中引用
import com.wings.gradle.CustomBuildSrcPlugin
apply plugin: CustomBuildSrcPlugin

通過簡單的id的方式,我們可以隱藏類名等細節,使的引用更加容易。對映的方式很簡單,在buildSrc目錄下建立resources/META-INF/gradle-plugins/xxx.properties,這裡的xxx也就是所對映的id,這裡我們假設取名CustomPlugin。具體結構可參考上文buildSrc目錄結構。

基礎概念

Extension

為了能讓 App 傳入相關的版本資訊和生成的版本資訊檔案路徑,我們需要一個用於配置版本資訊的 Extension,其實質就是一個實體類

與建立擴充套件屬性一樣,擴充套件Task也需要在project中建立注入。

project.extensions.create("releaseInfo", ReleaseInfoExtension)

自定義Task

右邊就都是task

  • 使用自定義擴充套件屬性 Extension 僅僅是為了讓使用外掛者有配置外掛的能力。而外掛還得藉助自定義 Task 來實現相應的功能
  • 建立擴充套件屬性一樣,擴充套件Task也需要在project中建立注入
// 建立Task
project.tasks.create("updateReleaseInfo", ReleaseInfoTask)
  • task 的作用就是通過實現自定義的 Extension,可以在 Gradle 指令碼中增加類似 android 這樣名稱空間的配置,Gradle 可以識別這種配置,並讀取裡面的配置內容。。
  • 一個Task表示一個邏輯上較為獨立的執行過程,比如編譯Java原始碼,拷貝檔案,打包Jar檔案,甚至可以是執行一個系統命令或者呼叫Ant。另外,一個Task可以讀取和設定Project的Property以完成特定的操作。
  • 一個Task是由一序列Action組成的,當執行一個Task的時候,這個Task裡的Action序列會按照順序執行

構建生命週期
每次構建的本質其實就是執行一系列的Task,某些Task可能依賴其他Task,那些沒有依賴的Task總會被最先執行,而且每個Task只會被執行一遍,每次構建的依賴關係是在構建的配置階段確定的,在gradle構建中,構建的生命週期主要包括以下三個階段:
初始化(Initialization)
構建工具會根據每個build.gradle檔案建立出一個Project例項,初始化階段會執行專案根目錄下的Settings.gradle檔案,來分析哪些專案參與構建。
include ':app'
配置(Configuration)
執行(Execution)

Plugin

作用

  • 模組化構建指令碼的功能
  • 公共的功能可以抽取出來成為外掛,可以供多個 build.gradle 使用,增加複用性。

和task的關係
如果有個你想要在好幾個專案中重用的Gradle task集合,把這些task提取到一個自定義的plugin中是有意義的。這使得重用你自己的build邏輯和與他人共享該邏輯都是可能的。

Transformer

在 Booster 中,跟位元組碼相關的操作都是通過 Transformer 來完成,它是對位元組碼轉換的簡單抽象,以位元組碼的二進位制做為輸入,經過轉換後,輸出位元組碼二進位制,它與具體使用哪種位元組碼操作框架無關,開發者可以自己選擇跟位元組碼操作框架相關的特定實現, Booster 提供了兩種實現:
基於 ASM 的實現:AsmTransformer
基於 Javassist 的實現:JavassistTransformer

Transform 可以被看作是Gradle 在編譯專案時的一個 task,在 .class 檔案轉換成 .dex 的流程中會執行這些 task,對所有的 .class 檔案(可包括第三方庫的 .class)進行轉換,轉換的邏輯定義在 Transform 的 transform 方法中。實際上平時我們在 build.gradle 中常用的功能都是通過 Transform 實現的,比如混淆(proguard)、分包(multi-dex)、jar 包合併(jarMerge)

class AddCodePlugin implements Plugin<Project> {
    void apply(Project project) {
        project.android.registerTransform(new AddCodeTransform(project))
    }
}

寫法

其實就是:把輸入內容寫入到作為輸出內容

輸出地址不是由你任意指定的。而是根據輸入的內容、作用範圍等由TransformOutputProvider生成,比如,你要獲取輸出路徑:

 String dest = outputProvider.getContentLocation(directoryInput.name,
                        directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
                        
FileUtils.copyDirectory(directoryInput.file, dest)

Transform的inputs有兩種型別,一種是目錄,一種是jar包,要分開遍歷
一旦註冊了transform,就要處理輸入和輸出(預設實現是沒有處理的),否則編譯失敗。

位元組碼操作框架
ASM vs Javassist
https://booster.johnsonlee.io/developer/bytecode-engineering-framework.html#asm-vs-javassist

Transform API 起因
從 Android Gradle Plugin 1.5.0-beta1 開始,為了簡化注入自定義 class 的操作,Android 提供了 Transform API,允許第三方外掛在 class 檔案被轉換成 dex 之前對其進行修改,在此之前,如果要實現同樣的操作,只能通過 Hook Task 的方式才能做到

引數說明
具體看程式碼
解釋說明:Transform 主要作用是檢索專案編譯過程中的所有檔案。通過這幾個方法,我們可以對自定義 Transform 設定一些遍歷規則,具體如下:

getName:
設定我們自定義的 Transform 對應的 Task 名稱。Gradle 在編譯的時候,會將這個名稱顯示在控制檯上。比如:
Task :app:transformClassesWithXXXForDebug。

getInputType:
在專案中會有各種各樣格式的檔案,通過 getInputType 可以設定 LifeCycleTransform 接收的檔案型別,此方法返回的型別是 Set<QualifiedContent.ContentType> 集合。

Gradle用處

gradle外掛修改第三方程式碼

1、我們知道在打包過程中,可以通過動態修改位元組碼,來進行插樁,實現埋點等業務,那麼,在什麼時機插入呢?
2、隨著專案越來越大,編譯專案的時間會越來越長,我們需要統計各個任務的執行時間,來優化我們的打包編譯速度,那麼,如何統計呢?
3、在我們的專案、第三方庫和系統遇到一些bug的時候,我們有沒有什麼比較好的hook方法,對我們的程式碼做到無侵入?

好文章

除錯gradle
https://www.jianshu.com/p/6bbe9352f75d 也可以

gradle外掛釋出

通過自定義Gradle外掛修改編譯後的class檔案

Gradle外掛實戰之編譯期修改程式碼

  • 開源庫和自己寫的插入程式碼注意不要混淆
  • buildSrc中build.gradle的AGP版本要和app模組中一致
  • 插入程式碼引用的類要使用全路徑
  • 插入程式碼中用到的類需要將類路徑新增到classPool中,否則會編譯不過
    buildSrc不要在settings.gradle中配置
  • 不管我們有沒有修改jar的操作,也要拷貝到目標路徑

Android ASM框架詳解

Java學習之 javassist

自定義Gradle外掛+ASM 實戰

常見問題

  • Could not find implementation class 'xxx' 的話
implementation-class=com.lqr.gradle.study.GradleStudyPlugin
// 如果報錯 Could not find implementation class 'xxx' 的話,一般是類全路徑有問題,預設包不需要寫包路徑,修改如下即可:
// implementation-class=GradleStudyPlugin
  • 重新部署外掛時,需要先在 app module 的 build.gradle 中將外掛依賴註釋,否則報錯。
  • 不生效時,可以先註釋,編譯,再開啟試試

相關文章