Android Gradle插件
- 2021 年 6 月 22 日
- 筆記
- Android學習筆記
Gradle插件練習地址://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
//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
//www.jianshu.com/p/6bbe9352f75d 也可以
- 開源庫和自己寫的插入代碼注意不要混淆
- buildSrc中build.gradle的AGP版本要和app模塊中一致
- 插入代碼引用的類要使用全路徑
- 插入代碼中用到的類需要將類路徑添加到classPool中,否則會編譯不過
buildSrc不要在settings.gradle中配置 - 不管我們有沒有修改jar的操作,也要拷貝到目標路徑
常見問題
- 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 中將插件依賴注釋,否則報錯。
- 不生效時,可以先注釋,編譯,再打開試試