自定義Gradle插件

可使用任何您喜歡的語言來實現Gradle插件,前提是該實現源碼最終被編譯爲JVM字節碼html

Gradle 腦圖

  • 通過前面文章,是時候來個腦圖總結一下

Gradle知識體系

自定義插件

  • 自定義插件有三種方式,分別爲build.gradle中編寫、buildSrc工程項目中編寫(統一依賴管理)和插件以獨立項目編譯提供jar包形式引入項目

build.gradle 自定義插件

  • 要建立 Gradle 插件,您須要編寫一個實現 Plugin 接口。將插件應用於項目時,Gradle 將建立插件類的實例,並調用該實例的 Plugin.apply()方法。項目對象做爲參數傳遞,插件可使用它來配置項目。下面的示例包含一個Greeting插件,該插件將一個 hello 任務(task)添加到項目中。
  • 直接在 build.gradle 添加以下代碼
class GreetingPlugin implements Plugin<Project>{

    @Override
    void apply(Project target) {

        target.task("hello"){
            doLast {
                println("Hello from the GreetingPlugin")
            }
        }
    }
}

apply plugin: GreetingPlugin
複製代碼
  • 運行 gradle 命令(注意本地gradle版本與AS 版本想匹配,不然會編譯失敗)
gradle -q hello

輸出 Hello from the GreetingPlugin
複製代碼

build.gradle 自定義插件配置擴展

  • 爲構建插件提供了一些配置選項,插件使用擴展對象執行此操做。Gradle 項目具備一個關聯的ExtensionContainer對象,該對象包含已應用於該項目的插件的全部設置和屬性。您能夠經過向該容器添加擴展對象來爲您的插件提供配置。擴展對象只是具備表示配置的Java Bean屬性的對象
//build.gradle   自定義插件
class GreetingPlugin2 implements Plugin<Project> {
    void apply(Project project) {
        //獲取配置
        def extension = project.extensions.create('greeting', GreetingPluginExtension)

        project.task('hello2') { //名字爲 hello 的task
            doLast {
                //獲取 extension 配置信息
                println "${extension.message} from ${extension.greeter}"
            }
        }
    }
}

//引入插件
apply plugin: GreetingPlugin2

// 配置 extension
greeting{
    greeter = 'Gradle'
    message = "Hi"
}
複製代碼
  • 運行 gradle 命令
gradle -q hello2

輸出 > Task :app:hello2
Hi from Gradle
複製代碼
  • 以上分別建立了兩個名爲包含 hello 和 hello2 任務(task)的自定義插件,在 Tasks列表的 other 中也可找到

hello和hello2task

buildSrc工程項目自定義插件

  • 新建 buildSrc module,刪除沒必要要文件以下所示,並新建 groovy目錄添加自定義插件 TestPlugin,同時也簡單設置一個叫TestPlugin的task打印日誌

注意:記得刪除 settings.gradle buildSrc配置,不然會報'buildSrc' cannot be used as a project name as it is a reserved name 錯誤java

buildSrc自定義插件

import org.gradle.api.Plugin
import org.gradle.api.Project

class TestPlugin implements Plugin<Project>{

    @Override
    void apply(Project project) {

        println("====== buildSrc TestPlugin Plugin加載===========")
        //執行自定義的  task
        project.task("TestPlugin"){
            doLast {
                println("buildSrc TestPlugin task 任務執行")
            }
        }
    }
}
複製代碼
  • 而後在 app 項目下 build.gradle 引入buildSrc 剛剛建立好的插件
apply plugin: TestPlugin
複製代碼
  • 一樣能夠在 Task other 下找到 TestPlugin 執行

Taskother下找到TestPlugin

自定義插件編譯成 jar 包

  • AS 中新建 module,刪除其餘文件,只保留 src 目和 build.gradle 腳本文件,以下圖所示

自定義插件新建項目

  • 刪除原有 build.gradle 腳本文件內容,修改成以下
## 須要引入的插件
apply plugin: 'groovy'
apply plugin: 'maven'

//gradle 開發 sdk 依賴
dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])

    implementation gradleApi()
    implementation localGroovy()

    implementation 'com.android.tools.build:gradle:4.0.0'

}

//設置插件 group 和版本號 在項目中使用的時候會用到
group='com.maoasm.plugin'
version='1.0.0'

uploadArchives {
    repositories {
        mavenDeployer {
            //本地的Maven地址設置
            repository(url: uri('../asm_test_repo'))
        }
    }
}
複製代碼
  • 而後 在 mian 目錄下新建 groovy 目錄,由於gradle 是groovy寫的,因此該目錄用來存放插件相關的.groovy類,而後咱們建立 MainPlugin.groovy 文件,並實現插件接口,project 則表明引入插件的項目
package com.maoasm.plugin

import org.gradle.api.Plugin
import org.gradle.api.Project

class MainPlugin implements Plugin<Project>{

    @Override
    void apply(Project project) {

        println("======自定義MainPlugin加載===========")
        //執行自定義的  task
        project.task("TestPluginTask"){
            doLast {
                println("自定義插件task 任務執行")
            }
        }
    }
}
複製代碼
  • 最後建立 properties 文件,maven 項目都須要這個配置,properties 文件名標識項目名稱
## 本文件名稱就是插件 apply 名稱
implementation-class=com.maoasm.plugin.MainPlugin
複製代碼

建立項目文件

注意,上圖中的目錄層級須要一一對應建立目錄,保證父子目錄,不然 apply 插件會出現如下錯誤android

Plugin with id 'XXXXX' not found
複製代碼

插件上傳到本地 maven 倉庫

  • 前面在插件 build.gradle 腳本文件中咱們配置了上傳 jar 的 uploadArchives task 任務,找到 gradle task 執行上傳 jar 包,以下圖爲執行任務和上傳 jar 成功

執行uploadArchives任務

自定義插件 jar 上傳本地倉庫成功

測試自定義插件

  • 在app項目的 build.gradle 引入咱們剛剛寫好的插件
//引入自定義插件
apply plugin: 'com.mao.asmtest'
buildscript {
    repositories {
        google()
        jcenter()
        //自定義插件maven地址,這裏以本地目錄做爲倉庫地址目錄
        maven { url '../asm_test_repo' }
    }
    dependencies {
        //加載自定義插件 group + module + version
        classpath 'com.maoasm.plugin:asm_test_plugin:1.0.0'
    }
}
複製代碼

引入自定義插件

  • 插件引入編譯成功,說明此時自定義插件加載成功

插件引入編譯成功

  • 在項目Task 選項中執行這個自定義 TestPluginTask

TestPluginTask執行成功

實踐:自定義插件實現Activity生命週期方法插樁打印log

  • 前面例子都是使用 grrovy 語法來編寫自定義插件,開頭已經說過,只有編寫插件語言能編譯成字節碼就行,因此接下來使用 kotlin 來實現一個自定義插件,實現Activity生命週期方法插樁打印log。
  • 首先要改寫插件的 build.gradle 引入依賴讓插件可以編譯 kotlin,以下所示加入kotlin依賴
apply plugin: 'groovy'
apply plugin: 'maven'
apply plugin: 'kotlin'

dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])

    implementation gradleApi()
    implementation localGroovy()

    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'com.android.tools.build:gradle:4.0.0'
}


group='com.mao.asm'
version='1.0.1'

uploadArchives {
    repositories {
        mavenDeployer {
            //本地的Maven地址設置
            repository(url: uri('../asm_test_repo'))
        }
    }
}
複製代碼

Transform是什麼?

  • 前面咱們寫的代碼在編譯的時候可使用 project.task 來指定編譯過程要作什麼操做,要完成自動插樁,首先就要找到項目中對應的.class文件修改,編譯過程當中 compileJava 這個task 將Java文件變成 .class ,若是編寫一個 Transform 註冊後 gradle 會將其看作是一個Task,並在 compileJava task 以後執行,Transform 接收這些 class 文件在執行插樁這就是這個自定義插件實現思路。git

  • 詳情請看個人另外一篇文章深刻了解 Gradlegithub

使用 Transform Task 處理字節碼文件

  • 在前面自定義插件編譯基礎上新建一個 kotlin 目錄,這樣插件纔會編譯 kotlin 目錄代碼,新建 MainPlugin.kt 類繼承 Plugin 接口,泛型定義爲加載插件的 Project
/**
 * @Description:
 * @author maoqitian
 * @date 2020/11/13 0013 17:01
 */
class MainPlugin :Plugin<Project> {
    override fun apply(project: Project) {
        println("======自定義MainPlugin加載===========")
    }
}
複製代碼
  • 而後定義 ASMLifecycleTransform.kt 類 處理字節碼文件,分別獲取輸入輸出文件集合,遍歷獲得.class 文件,其中涉及 ASM 框架的 ClassReader和 ClassWriter下面會介紹。
/**
 * @Description: Transform 能夠被看做是 Gradle 在編譯項目時的一個 task
 * @author maoqitian
 * @date 2020/11/13 0013 17:03
 */
class ASMLifecycleTransform :Transform() {

    /**
     * 設置咱們自定義的 Transform 對應的 Task 名稱。Gradle 在編譯的時候,會將這個名稱顯示在控制檯上
     * @return String
     */
    override fun getName(): String = "ASMLifecycleTransform111"

    /**
     * 在項目中會有各類各樣格式的文件,該方法能夠設置 Transform 接收的文件類型
     * 具體取值範圍
     * CONTENT_CLASS  .class 文件
     * CONTENT_JARS  jar 包
     * CONTENT_RESOURCES  資源 包含 java 文件
     * CONTENT_NATIVE_LIBS native lib
     * CONTENT_DEX dex 文件
     * CONTENT_DEX_WITH_RESOURCES  dex 文件
     * @return
     */
    override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> = TransformManager.CONTENT_CLASS


    /**
     * 定義 Transform 檢索的範圍
     * PROJECT 只檢索項目內容
     * SUB_PROJECTS 只檢索子項目內容
     * EXTERNAL_LIBRARIES 只有外部庫
     * TESTED_CODE 由當前變量測試的代碼,包括依賴項
     * PROVIDED_ONLY 僅提供的本地或遠程依賴項
     * @return
     */
    //只檢索項目內容
    override fun getScopes(): MutableSet<in QualifiedContent.Scope> = TransformManager.PROJECT_ONLY

    /**
     * 表示當前 Transform 是否支持增量編譯 返回 true 標識支持 目前測試插件不須要
     * @return Boolean
     */
    override fun isIncremental(): Boolean = false
    //對項目 class 檢索操做
    override fun transform(transformInvocation: TransformInvocation) {
        println("transform 方法調用")

        //獲取全部 輸入 文件集合
        val transformInputs = transformInvocation.inputs
        val transformOutputProvider = transformInvocation.outputProvider

        transformOutputProvider?.deleteAll()

        transformInputs.forEach { transformInput ->
            // Caused by: java.lang.ClassNotFoundException: Didn't find class "androidx.appcompat.R$drawable" on path 問題
            // gradle 3.6.0以上R類不會轉爲.class文件而會轉成jar,所以在Transform實現中須要單獨拷貝,TransformInvocation.inputs.jarInputs
            // jar 文件處理
            transformInput.jarInputs.forEach { jarInput ->
                val file = jarInput.file
                println("find jar input:$file.name")
                val dest = transformOutputProvider.getContentLocation(jarInput.name, jarInput.contentTypes, jarInput.scopes, Format.JAR)
                FileUtils.copyFile(file, dest)
            }
            //源碼文件處理
            //directoryInputs表明着以源碼方式參與項目編譯的全部目錄結構及其目錄下的源碼文件
            transformInput.directoryInputs.forEach { directoryInput ->
                //遍歷全部文件和文件夾 找到 class 結尾文件
                directoryInput.file.walkTopDown()
                    .filter { it.isFile }
                    .filter { it.extension == "class" }
                    .forEach { file ->
                        println("find class file:${file.name}")
                        val classReader = ClassReader(file.readBytes())
                        val classWriter = ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                        //字節碼插樁處理
                        //2.class 讀取傳入 ASM visitor
                        val asmLifecycleClassVisitor = ASMLifecycleClassVisitor(classWriter)
                        //3.經過ClassVisitor api 處理
                        classReader.accept(asmLifecycleClassVisitor,ClassReader.EXPAND_FRAMES)
                        //4.處理修改爲功的字節碼
                        val bytes = classWriter.toByteArray()
                        //寫回文件中
                        val fos =  FileOutputStream(file.path)
                        fos.write(bytes)
                        fos.close()
                }
                //複製到對應目錄
                val dest = transformOutputProvider.getContentLocation(directoryInput.name,directoryInput.contentTypes,directoryInput.scopes, Format.DIRECTORY)
                FileUtils.copyDirectory(directoryInput.file,dest)
            }
        }
    }
}
複製代碼

ASM 字節碼操做

  • 字節碼操做已有現成的框架ASM,官方文檔後端

  • 在插件 build.gradle 中依賴引入api

implementation 'org.ow2.asm:asm:9.0'
implementation 'org.ow2.asm:asm-commons:9.0'
複製代碼

ASM 幾個關鍵類

  • ClassReader:讀取字節碼文件的字節數組,並將字節碼傳遞給ClassWriter
  • ClassWriter:它的父類是ClassVisitor,做用是生成修改後的字節碼,並輸出字節數組;字節碼文件由無符號數和表組成,最終其實爲十六進制數,在 ASM 修改了字節碼文件以後,確定會影響到常量池的大小,此外包括本地變量表和操做數棧等變化,不過放心,只要在實例化 ClassWriter 操做類的時候設置 COMPUTE_MAXS 後,ASM 就會自動計算本地變量表和操做數棧。
val classWriter = ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
複製代碼
  • ClassVisitor:用來解析字節碼文件結構的,當解析到某些特定結構時(好比類、方法、變量、),則調用內部相應的 FieldVisitor 或者 MethodVisitor 的方法,進一步解析或者能夠修改 字節碼文件的內容幫助完成字節碼插樁功能數組

  • 更多字節碼結果組成內容可看個人另外一篇文章 從新認識Java字節碼markdown

其餘插樁框架

  • AspectJ,作事後端開發應該對其不陌生。它在 Android 中使用比較難搞,Android 可使用 AspectJX
  • facebook 的 redex , 它有提供在全部方法或者指定方法前面插入一段跟蹤代碼,具體我也沒研究過,能夠自行查看項目例子InstrumentTest.config

ASM 插樁實現

  • 使用 ClassVisitor 讀取目標 Activity 的 .class 文件,並過濾對應生命週期方法
/**
 * @Description: class Visitor
 * @author maoqitian
 * @date 2020/11/13 0013 11:47
 */
class ASMLifecycleClassVisitor(classVisitor: ClassVisitor?) : ClassVisitor(Opcodes.ASM5, classVisitor) {

     private var className:String? = null
     private var superName:String? = null

    override fun visit(version: Int, access: Int, name: String?, signature: String?, superName: String?, interfaces: Array<out String>?) {
        super.visit(version, access, name, signature, superName, interfaces)
        this.className = name
        this.superName = superName
    }


    override fun visitMethod(access: Int, name: String, descriptor: String?, signature: String?, exceptions: Array<out String>?): MethodVisitor {
        val methodVisitor = cv.visitMethod(access,name,descriptor,signature,exceptions)
        //找到 androidX 包下的 Activity 類
        if (superName == "androidx/appcompat/app/AppCompatActivity"){
            //對 onCreate 方法處理 加入日誌打印
            if (name.startsWith("onCreate")){
                println("do ASM ClassVisitor visitMethod onCreate")
                return ASMLifecycleMethodVisitor(methodVisitor, className!!, name)
            }
            if (name.startsWith("onStart")){
                println("do ASM ClassVisitor visitMethod onStart")
                return ASMLifecycleMethodVisitor(methodVisitor, className!!, name)
            }
            if (name.startsWith("onResume")){
                println("do ASM ClassVisitor visitMethod onResume")
                return ASMLifecycleMethodVisitor(methodVisitor, className!!, name)
            }
            if (name.startsWith("onRestart")){
                println("do ASM ClassVisitor visitMethod onRestart")
                return ASMLifecycleMethodVisitor(methodVisitor, className!!, name)
            }
            if (name.startsWith("onPause")){
                println("do ASM ClassVisitor visitMethod onPause")
                return ASMLifecycleMethodVisitor(methodVisitor, className!!, name)
            }
            if (name.startsWith("onStop")){
                println("do ASM ClassVisitor visitMethod onStop")
                return ASMLifecycleMethodVisitor(methodVisitor, className!!, name)
            }
            if (name.startsWith("onDestroy")){
                println("do ASM ClassVisitor visitMethod onDestroy")
                return ASMLifecycleMethodVisitor(methodVisitor, className!!, name)
            }
        }
        return methodVisitor
    }

    override fun visitEnd() {
        super.visitEnd()
    }
}
複製代碼
  • 使用 MethodVisitor 執行字節碼插樁
/**
 * @Description: 方法 Method Visitor 爲每一個方法加入日誌打印
 * @author maoqitian
 * @date 2020/11/13 0013 11:47
 */
class ASMLifecycleMethodVisitor(private val methodVisitor:MethodVisitor, private val className:String,private val methodName:String) : MethodVisitor(Opcodes.ASM5, methodVisitor) {


    //在方法執行前插入日誌字節碼
    override fun visitCode() {
        super.visitCode()
        println("do ASMLifecycleMethodVisitor visitCode method......")

        methodVisitor.visitLdcInsn("毛麒添")

        methodVisitor.visitLdcInsn("$className -> $methodName")
        //字節碼 插入方法 日誌
        methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false)
        methodVisitor.visitInsn(Opcodes.POP)
    }

    override fun visitEnd() {
        super.visitEnd()
    }
}
複製代碼
  • 在Demo 項目中寫了兩個Activity,兩個Activity都實現了一些生命週期方法,具體代碼就不貼了,可自行查看demo源碼編譯運行項目

項目編譯自動插樁完成

  • 插件效果展現,從 MainActivity 跳轉 SecondActivity再返回以下,能夠看到只有Activity實現了生命週期方法就會自動插入日誌打印代碼

自動插樁效果

  • 最後以一張圖來講明自定義插件介入修改字節碼的過程

自定義插件介入插樁示意圖

自定義插件調試

  • 插件寫好了不免有 bug,這時就須要用到插件調試來解決問題。
  • 首先和平時調試同樣,在插件代碼須要打斷點的地方點上斷點

插件代碼打斷點

  • gradle 命令 daemon 進程執行編譯等待 debug
gradlew assembleDebug -Dorg.gradle.daemon=false -Dorg.gradle.debug=true
複製代碼

gradle命令daemon進程執行編譯等待 debug

  • 添加 remote 編譯配置,保持默認配置就行

remote編譯配置

  • 點擊 debug 進入斷點調試成功

點擊 debug 進入斷點調試

控制檯等待編譯等待調試下一步

gradle系列文章

參考

相關文章
相關標籤/搜索