Gradle插件開發(fā)

gradle 生命周期

任務(wù)圖(Task Graph)

首先要明白Gradle 核心是基于依賴的編程。具體來說是當(dāng)你定義了任務(wù)和任務(wù)之間的依賴,gradle得保證這些任務(wù)按照他們的依賴順序執(zhí)行,所以gradle在執(zhí)行任務(wù)之前,會構(gòu)建一個任務(wù)圖(Task Graph)。通過配置,Gradle 會跳過不屬于當(dāng)前構(gòu)建的任務(wù)的配置。

在每個項(xiàng)目中,任務(wù)圖最終會形成一個有向無環(huán)圖(DAG)。

gradle-task-graph.png

構(gòu)建階段

Gradle 構(gòu)建具有三個不同的階段。Gradle 按順序運(yùn)行這些階段:首先是初始化,然后是配置,最后是執(zhí)行。

  • 初始化

    • 檢測settting.gradle文件。
    • 評估settting.gradle文件以確定哪些項(xiàng)目和包含的構(gòu)建參與構(gòu)建。
    • 為每個項(xiàng)目創(chuàng)建一個Project實(shí)例。
  • 配置

    • 評估參與構(gòu)建的每個項(xiàng)目的構(gòu)建腳本。
    • 為請求的任務(wù)創(chuàng)建任務(wù)圖。
  • 執(zhí)行

    • 按照依賴關(guān)系的順序安排和執(zhí)行每個選定的任務(wù)。
// setting.gradle
rootProject.name = 'basic'
println 'This is executed during the initialization phase.'
// build.gradle
println 'This is executed during the configuration phase.'

tasks.register('configured') {
    println 'This is also executed during the configuration phase, because :configured is used in the build.'
}

tasks.register('test') {
    doLast {
        println 'This is executed during the execution phase.'
    }
}

tasks.register('testBoth') {
    doFirst {
      println 'This is executed first during the execution phase.'
    }
    doLast {
      println 'This is executed last during the execution phase.'
    }
    println 'This is executed during the configuration phase as well, because :testBoth is used in the build.'
}

具體來說,當(dāng)以上gradle文件執(zhí)行任務(wù)時(shí),會先運(yùn)行setting.gradle,比如Android項(xiàng)目中會有的include ':app',也會在這時(shí)注冊app項(xiàng)目,創(chuàng)建其project實(shí)例。

然后在怕配置階段會執(zhí)行build.gradle,在這里會創(chuàng)建三個任務(wù),但不會立即執(zhí)行。

所以當(dāng)執(zhí)行以下命令時(shí)顯示如下:

> gradle test testBoth
This is executed during the initialization phase.

> Configure project :
This is executed during the configuration phase.
This is executed during the configuration phase as well, because :testBoth is used in the build.

> Task :test
This is executed during the execution phase.

> Task :testBoth
This is executed first during the execution phase.
This is executed last during the execution phase.

BUILD SUCCESSFUL in 0s
2 actionable tasks: 2 executed

而gradle也為我們準(zhǔn)備了一些鉤子函數(shù)對其生命周期的各個階段進(jìn)行監(jiān)聽:

// 項(xiàng)目評估監(jiān)聽
gradle.beforeProject { project ->
    project.ext.set("hasTests", false)
}

gradle.afterProject { project ->
    // ...
}

// 任務(wù)監(jiān)聽
gradle.taskGraph.beforeTask { Task task ->
    println "executing $task ..."
}

gradle.taskGraph.afterTask { Task task, TaskState state ->
    if (state.failure) {
        println "FAILED"
    }
    else {
        println "done"
    }
}

gradle插件

gradle的核心是上述的任務(wù)自動化,但其所有的功能(如編譯 Java 代碼的能力)都是由插件提供的。gradle插件有點(diǎn)類似于代碼中封裝的方法,可以通過特定的配置,封裝并且擴(kuò)展項(xiàng)目的功能,減少多個項(xiàng)目維護(hù)相似的邏輯的開銷。

插件類型分為兩種:二進(jìn)制插件和腳本插件。因?yàn)槎M(jìn)制插件可以以外部jar包的形式提供,所以通常運(yùn)用的更普遍,這里以二進(jìn)制插件為例。

聲明插件

// build.gradle
plugins {
    id 'java' // 核心插件
    id 'com.jfrog.bintray' version '1.8.5' // 第三方社區(qū)插件
    // id ?plugin id? version ?plugin version? [apply ?false?]
}

plugins塊有如下限制:

  • 該plugins {}塊還必須是構(gòu)建腳本中的頂級語句。它不能嵌套在另一個構(gòu)造中(例如 if 語句或 for 循環(huán))。
  • 該plugins {}塊目前只能在項(xiàng)目的構(gòu)建腳本和 settings.gradle 文件中使用。它不能用于腳本插件或初始化腳本。

管理插件

可以在setting.gradle中配置pluginManagement {}塊管理插件。它必須是文件中的第一個塊

// setting.gradle
pluginManagement {
    plugins {
        id 'com.example.hello' version "${helloPluginVersion}"
    }
    repositories {
         maven {
            url './maven-repo'
        }
        gradlePluginPortal()
    }
}
// build.gradle
plugins {
    id 'com.example.hello'
}

約定插件

如果想定義一個自己的插件,可以新建一個module,或者在項(xiàng)目的buildSrc目錄中新建build.gradle,并配置如下:

plugins {
    id 'java-gradle-plugin'
}

// 
gradlePlugin {
    plugins {
        myPlugins {
            id = 'my-plugin'
            implementationClass = 'my.MyPlugin'
        }
    }
}

這其實(shí)是 Java Gradle Plugin 提供的一個簡化 API,其背后會自動幫我們創(chuàng)建一個 [插件ID].properties 配置文件,Gradle 就是通過這個文件類進(jìn)行匹配的。如果你不使用 gradlePlugin API,直接手動創(chuàng)建 [插件ID].properties 文件,作用是完全一樣的。

// my-plugin.properties
implementation-class=my.MyPlugin

然后在module中新建java/groovy/kotlin文件MyPlugin,繼承自Plugin<Project>,并重寫apply方法實(shí)現(xiàn)插件的邏輯。

class CyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        // apply
    }
}

發(fā)布插件

通過 maven-publish 或 ivy-publish 發(fā)布:

// build.gradle
plugins {
    id 'java-gradle-plugin'
    id 'maven-publish'
    id 'ivy-publish'
}

group 'com.example'
version '1.0.0'

gradlePlugin {
    plugins {
        hello {
            id = 'com.example.hello'
            implementationClass = 'com.example.hello.HelloPlugin'
        }
        goodbye {
            id = 'com.example.goodbye'
            implementationClass = 'com.example.goodbye.GoodbyePlugin'
        }
    }
}

publishing {
    repositories {
        maven {
            url layout.buildDirectory.dir("maven-repo")
        }
        ivy {
            url layout.buildDirectory.dir("ivy-repo")
        }
    }
}

應(yīng)用插件

在Gradle構(gòu)建工具中,可以用buildScript{} 塊來定義構(gòu)建腳本自身的依賴關(guān)系,將已作為外部 jar 文件發(fā)布的二進(jìn)制插件添加到項(xiàng)目中。

buildscript {
    repositories {
        gradlePluginPortal()
    }
    dependencies {
        classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.5'
    }
}

apply plugin: 'com.jfrog.bintray'

限制:buildscript {}塊必須放在plugin {}塊之前。

TASK Transform ASM

TASK

通用task

// android項(xiàng)目的clean task
task clean(type: Delete) {
    delete rootProject.buildDir
}

// 自定義task
tasks.register('hello')
// 自定義Copy類型的task
tasks.register('copy', Copy)

自定義task

首先繼承defaultTask:

abstract class MyTask : DefaultTask() {
    @get:OutputFile
    abstract val outputFile: RegularFileProperty

    @get:Input
    abstract val inputCount: Property<Int>

    @TaskAction
    fun action() {
        // task執(zhí)行的代碼
        val outputFile = outputFile.get().asFile
        outputFile.delete()
        outputFile.parentFile.mkdirs()
        Files.write(outputFile.toPath(), ("Count is: " + inputCount.get()).toByteArray())
        println("MyTask Output file is: " + outputFile.toPath())
    }
}

注冊:

project.tasks.register("myTask", MyTask::class.java) {
                it.inputCount.set(10)
                it.outputFile.set(File("build/myTask/output/file.txt"))
            }

上述任務(wù)執(zhí)行的結(jié)果是生成app/build/myTask/output/file.txt,內(nèi)容是

Count is: 10

Action

其實(shí)Task的本質(zhì)是一組被順序的Action對象構(gòu)成??梢园袮ction理解為一段代碼塊??赏ㄟ^在Task中添加doFirst{}和doLast{}來為Task執(zhí)行Action的開始和結(jié)束添加Action。

task clean(type: Delete) {
   delete rootProject.buildDir
   doLast {
       println(prefix + "Android Studio auto add clean task do last")
   }
   doFirst {
       println(prefix + "Android Studio auto add clean task do first")
   }
}

task執(zhí)行順序

  1. B.dependsOn A:先執(zhí)行完ATask,在執(zhí)行BTask;
  2. B.mushRunAfter A:先執(zhí)行完ATask,在執(zhí)行BTask
  3. B.mushRunAfter A C.mushRunAfter A:按照ATask、BTask、CTask順序執(zhí)行
  4. B.shouldRunAfter A:先執(zhí)行完ATask,在執(zhí)行BTask

Transform

Transform API 是 AGP1.5 就引入的特性,主要用于在 Android 構(gòu)建過程中,在 Class轉(zhuǎn)Dex的過程中修改 Class 字節(jié)碼。

Android 打包.png

自定義Transform流程:

public class DemoTransform extends Transform {
    Project project;

    public DemoTransform(Project project) {
        this.project = project;
    }

    // transform任務(wù)名字(用于尾部拼接)
    // 最終會生成 transformClassesWithDemoTransformForDebug 的Task
    @Override
    public String getName() {
        return "DemoTransform";
    }

    // Transform需要處理的類型
    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS;
    }

    // transform作用域,要處理所有class字節(jié)碼,Scope我們一般使用TransformManager.SCOPE_FULL_PROJECT
    @Override
    public Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT;
    }

    // 增量編譯開關(guān),true只有增量編譯時(shí)才回生效
    @Override
    public boolean isIncremental() {
        return true;
    }

    @Override
    public void transform(TransformInvocation transformInvocation) throws IOException {
        System.out.println("============ DemoTransform 開始執(zhí)行============");
        //消費(fèi)型輸入,可以從中獲取jar包和class文件夾路徑。需要輸出給下一個任務(wù)
        final Collection<TransformInput> inputs = transformInvocation.getInputs();
        //引用型輸入,無需輸出。
        final Collection<TransformInput> referencedInputs = transformInvocation.getReferencedInputs();
        //OutputProvider管理輸出路徑
        final TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();

        for (TransformInput input : inputs) {
            // 處理jar文件
            for (JarInput jarInput : input.getJarInputs()) {
                System.out.println("jar= " + jarInput.getName());
                File dest = outputProvider.getContentLocation(
                        jarInput.getFile().getAbsolutePath(),
                        jarInput.getContentTypes(),
                        jarInput.getScopes(),
                        Format.JAR);
                // 將修改過的字節(jié)碼copy到dest,就可以實(shí)現(xiàn)編譯期間干預(yù)字節(jié)碼的目的了
                FileUtils.copyFile(jarInput.getFile(), dest);
            }

            // 處理class
            for (DirectoryInput directoryInput : input.getDirectoryInputs()) {
                if (directoryInput.getFile().isDirectory()) {
                    for (File file : FileUtils.getAllFiles(directoryInput.getFile())) {
                        System.out.println("directoryInput--" + file.getName());
                    }
                }
                File dest = outputProvider.getContentLocation(
                        directoryInput.getName(),
                        directoryInput.getContentTypes(),
                        directoryInput.getScopes(),
                        Format.DIRECTORY);
                //建立文件夾
                FileUtils.mkdirs(dest);
                //將class文件及目錄復(fù)制到dest路徑
                FileUtils.copyDirectory(directoryInput.getFile(), dest);
            }

        }

    }
}

這里的關(guān)鍵是重寫transform方法,里面可以從TransformInvocation中拿到所有打包到apk過程中需要的jar文件和class文件,所以這里我們就有機(jī)會對class文件進(jìn)行干預(yù),比如說分析或者改變class字節(jié)碼。然后我們拿到dest目錄(也就是我們拿到了這些文件,處理后需要將其移交給下一個transform),并將干預(yù)后的jar/class文件放到dest目錄下。

需要注意的是這里我們就算什么文件都不想干預(yù),也必須要將文件復(fù)制到dest目錄下,否則打包會失敗。

注冊transform:

val appExtension = project.extensions.getByType(AppExtension::class.java)
appExtension.registerTransform(DemoTransform(project))

由此可見transform的效率并不高,因?yàn)槊恳粋€transform都會遍歷整個打包過程中的jar/class文件,并且每一個transform我們都需要寫上一堆重復(fù)的代碼(獲取jar - 遍歷jar - 拿到class - 處理class - 打包jar - 復(fù)制/移動到dest目錄)。

所以在AGP 7.0以后,transform被標(biāo)記為棄用,并在AGP 8.0中被移除。取而代之的是 TransformActionAsmClassVisitorFactory。

AGP 8.0 變化

以下是 AGP 8.0 的重要 API 更新。

移除了 Transform API

從 AGP 8.0 開始,Transform API 將被移除。這意味著,軟件包 com.android.build.api.transform 中的所有類都會被移除。

Transform API 即將被移除,以提高 build 的性能。使用 Transform API 的項(xiàng)目會強(qiáng)制 AGP 對 build 使用優(yōu)化程度不夠的流程,從而導(dǎo)致構(gòu)建時(shí)間大幅增加。同時(shí)也很難使用 Transform API 以及將其與其他 Gradle 功能結(jié)合使用;這些替代 API 可讓您更輕松地?cái)U(kuò)展 AGP,而不會引起性能問題或 build 正確性問題。

替代 API

Transform API 沒有單一的替代 API,每個用例都會有新的針對性 API。所有替代 API 都位于 androidComponents {} 代碼塊中,在 AGP 7.2 中均有提供。

支持轉(zhuǎn)換字節(jié)碼

如需轉(zhuǎn)換字節(jié)碼,請使用 Instrumentation API。對于庫,您只能為本地項(xiàng)目類注冊插樁;對于應(yīng)用和測試,您既可以選擇僅為本地類注冊插樁,也可以選擇為所有類(包括本地和遠(yuǎn)程依賴項(xiàng))注冊插樁。為了使用此 API,每個類上的插樁都是獨(dú)立運(yùn)行的,并且對類路徑中其他類的訪問會受到限制(如需了解詳情,請參見 createClassVisitor())。此限制提高了完整 build 和增量 build 的性能,并使得 API Surface 變得簡單。每個庫一旦準(zhǔn)備就緒,即會進(jìn)行并行插樁;而不是在所有編譯完成后進(jìn)行插樁。此外,如果是在單個類中做出更改,則意味著只有受影響的類必須在增量 build 中重新進(jìn)行插樁。如需查看 Instrumentation API 使用方法的示例,請參閱使用 ASM 轉(zhuǎn)換類 AGP 配方。

TransformAction

參考:Transform 被廢棄,TransformAction 了解一下~

Transform API是由AGP提供的,而Transform Action則是由Gradle提供。不光是 AGP 需要 Transform,Java 也需要,所以由 Gradle 來提供統(tǒng)一的 Transform API。

關(guān)于 TransformAction 如何使用,Gradle 官方已經(jīng)提供了很詳細(xì)的文檔–Transforming dependency artifacts on resolution,與 AGP 類似,也是需要先注冊,只不過 AGP 是通過 Android Extension 來注冊 Transform ,Gradle 是通過 DependencyHandler 來注冊 TransformAction ,差異并不算很大。

// Plugin#apply()
val artifactType = Attribute.of("artifactType", String::class.java)
project.dependencies.registerTransform(MyTransformAction::class.java) {
    it.from.attribute(artifactType, "jar")
    it.to.attribute(artifactType, "my-custom-type")
}
abstract class MyTransformAction : TransformAction<TransformParameters.None> {
    @get:PathSensitive(PathSensitivity.NAME_ONLY)
    @get:InputArtifact
    abstract val inputArtifact: Provider<FileSystemLocation>

    override fun transform(outputs: TransformOutputs) {
        val file = inputArtifact.get().asFile;
        println("Processing $file. File exists = ${file.exists()}")
        if (file.exists()) {
            val outputFile = outputs.file("copy");
            Files.copy(file.toPath(), outputFile.toPath())
        } else {
            throw RuntimeException("File does not exist: " + file.canonicalPath);
        }
    }
}

具體的使用也可以看看AGP中自帶的JetifyTransformAarTransform

AsmClassVisitorFactory

AGP 8.0文檔中也提到了對字節(jié)碼轉(zhuǎn)換的支持,具體來說,就是AGP為我們又做了一層封裝,提供了AsmClassVisitorFactory來方便我們使用Transform Action進(jìn)行ASM操作。

ASM(全稱:Java ASM)是一種 Java 字節(jié)碼操縱框架,官網(wǎng):https://asm.ow2.io/

如果是用transform api + asm 的方式實(shí)現(xiàn)字節(jié)碼插樁,我們需要寫很多模板式的代碼,具體可以看看sensor埋點(diǎn)的實(shí)現(xiàn)。

但其實(shí)對于ASM而言,我們只需要通過提供不同的classVisitor實(shí)例,就可以實(shí)現(xiàn)我們特定的需求,至于怎么找到class,怎么通過classVisitor訪問class就全是模板代碼了,所以AsmClassVisitorFactory的發(fā)布就是為了解決這個痛點(diǎn)。

val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
androidComponents.onVariants {
        it.transformClassesWith(LogAsmTransform::class.java, InstrumentationScope.ALL) {
            // it -> InstrumentationParameters 攜帶參數(shù)
        }
    }
abstract class LogAsmTransform : AsmClassVisitorFactory<InstrumentationParameters.None> {

    override fun createClassVisitor(
        classContext: ClassContext,
        nextClassVisitor: ClassVisitor
    ): ClassVisitor {
        // 返回一個 ClassVisitor 對象,其內(nèi)部實(shí)現(xiàn)了我們修改 class 文件的邏輯
        return object : ClassVisitor(Opcodes.ASM9, nextClassVisitor) {
            val className = classContext.currentClassData.className

            // 這里,由于只需要修改方法,故而只重載了 visitMethod 找個方法
            override fun visitMethod(
                access: Int,
                name: String?,
                descriptor: String?,
                signature: String?,
                exceptions: Array<out String>?
            ): MethodVisitor {
                val oldMethodVisitor =
                    super.visitMethod(access, name, descriptor, signature, exceptions)
                // 返回一個 MethodVisitor 對象,其內(nèi)部實(shí)現(xiàn)了我們修改方法的邏輯
                return LogMethodVisitor(className, oldMethodVisitor, access, name, descriptor)
            }
        }
    }

    override fun isInstrumentable(classData: ClassData): Boolean {
        return true
    }
}

至于ASM的使用,又是一個大的范疇,故不在此篇做講解。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容