Gradle+Transform+Asm自動(dòng)化注入代碼

Gradle 相關(guān)總結(jié)
APT 和 AGPTransform 區(qū)別
Gradle+Transform+Asm自動(dòng)化注入代碼
Android 360加固+Walle多渠道自動(dòng)化打包上傳蒲公英

最近將公司的項(xiàng)目進(jìn)行重構(gòu),將原本的模塊化進(jìn)行了組件化,在這個(gè)過(guò)程中遇到了很多,最典型的就是如何去初始化其他組件,比如:消息組件,而組件化最主要的是其他組件能夠單獨(dú)運(yùn)行和集成到殼工程,也就是說(shuō)業(yè)務(wù)組件又多種形態(tài),那么有個(gè)問(wèn)題就是怎么去初始化業(yè)務(wù)組件,業(yè)務(wù)組件在單獨(dú)運(yùn)行時(shí)能將初始化可以自己的在Application中去做初始化,而當(dāng)成是Library時(shí)就需要將宿主工程的Application下發(fā)給組件,怎么下發(fā)宿主工程的上下文到組件中?下面有幾種:

  • 在BaseApp中直接初始化,但是這樣耦合度就非常高了,已經(jīng)背離了組件化的初衷;
  • 在公共組件中定義IComponent接口,并通過(guò)配置(注解\文件\SPI)實(shí)現(xiàn)類(lèi)的全類(lèi)名,然后通過(guò)反射實(shí)現(xiàn)業(yè)務(wù)組件的初始化,實(shí)際上SPI底層是通過(guò)通過(guò)解析文件得到類(lèi)名通過(guò)反射實(shí)現(xiàn)的;
  • 在Manifest中配置,然后解析Manifest文件,讀取到類(lèi)的全路徑,然后通過(guò)反射實(shí)現(xiàn)組件初始化;

上面是我目前所知道組件初始化的方式雖然能夠解決初始化問(wèn)題,但是都存在缺點(diǎn),那么除了上面所說(shuō)的方式,我們是否還有更好的方式去做到解耦合并且不會(huì)造成性能損耗呢?

在編譯時(shí),掃描即將打包到apk中的所有類(lèi)的字節(jié)碼,將所有組件類(lèi)收集起來(lái),通過(guò)修改字節(jié)碼的方式生成注冊(cè)代碼到組件管理類(lèi)中,從而實(shí)現(xiàn)編譯時(shí)自動(dòng)注冊(cè)的功能,不用再關(guān)心項(xiàng)目中有哪些組件類(lèi)了。

特點(diǎn):不需要注解,不會(huì)增加新的類(lèi);性能高,不需要反射,運(yùn)行時(shí)直接調(diào)用組件的構(gòu)造方法;能掃描到所有類(lèi),不會(huì)出現(xiàn)遺漏,而且還可以添加組件優(yōu)先排序。怎么實(shí)現(xiàn)呢?

AGP Transform

如果大家了解過(guò)apk打包的過(guò)程那么一定會(huì)知道Android 提供的Transform API,在Android apk打包過(guò)程中會(huì)利用 Transform 去完成每一部分的操作,并且會(huì)有輸入和輸出,比如DexTransform,是將class字節(jié)碼轉(zhuǎn)換成dex文件,那么輸入就是class字節(jié)碼,而輸出就是.dex文件,ProguardTransform則是完成混淆的,實(shí)際上Transform 是Android 提供的一種特殊Task,Task也是有輸入和輸出。

AGP Transform API是從Gradle 1.5.0版本之后提供的,它允許第三方在打包Dex文件之前的編譯過(guò)程中修改字節(jié)碼,在自定義插件中注冊(cè)的Transform會(huì)在ProguardTransformDexTransform之前執(zhí)行,實(shí)際上Transform是Android一種特殊的Task,自定義的Transform是會(huì)在自帶的Transform之前執(zhí)行,所以自動(dòng)注冊(cè)的Transform不需要考慮混淆的情況。

APK 打包流程

我們平時(shí)在開(kāi)發(fā)的過(guò)程中,每天在Android Studio Run項(xiàng)目,Android Studio就會(huì)將apk自動(dòng)安裝到手機(jī)上了,那么這中間都經(jīng)歷過(guò)哪些流程呢,來(lái)看看官方的項(xiàng)目構(gòu)建流程圖

build-process_2x.png

如圖所示,典型 Android 應(yīng)用模塊的構(gòu)建流程通常按照以下步驟執(zhí)行:
*1、 編譯器將源代碼轉(zhuǎn)換成 DEX 文件(Dalvik 可執(zhí)行文件),并將其他所有內(nèi)容轉(zhuǎn)換成編譯后的資源。

2、 APK 打包器將 DEX 文件和編譯后的資源合并到一個(gè) APK 中。不過(guò),在將應(yīng)用安裝并部署到 Android 設(shè)備之前,必須先為 APK 簽名。

3、APK 打包器使用調(diào)試或發(fā)布密鑰庫(kù)為 APK 簽名:

  • 如果您構(gòu)建的是調(diào)試版應(yīng)用(即專(zhuān)用于測(cè)試和分析的應(yīng)用),則打包器會(huì)使用調(diào)試密鑰庫(kù)為應(yīng)用簽名。Android Studio 會(huì)自動(dòng)使用調(diào)試密鑰庫(kù)配置新項(xiàng)目。
  • 如果您構(gòu)建的是打算對(duì)外發(fā)布的發(fā)布版應(yīng)用,則打包器會(huì)使用發(fā)布密鑰庫(kù)為應(yīng)用簽名。要?jiǎng)?chuàng)建發(fā)布密鑰庫(kù),請(qǐng)參閱在 Android Studio 中為應(yīng)用簽名

4、 在生成最終 APK 之前,打包器會(huì)使用 zipalign 工具對(duì)應(yīng)用進(jìn)行優(yōu)化,以減少其在設(shè)備上運(yùn)行時(shí)所占用的內(nèi)存。

可能從圖中并不會(huì)看出什么來(lái),實(shí)際上對(duì)于Java編程語(yǔ)言來(lái)說(shuō),這個(gè)過(guò)程要從Java源代碼到apk,那么我們來(lái)看一張圖:


gradle打包.png

這張圖就非常的清晰了,gradle打包過(guò)程中基本上是通過(guò)官方提供的Transform完成的,文章開(kāi)始我就說(shuō)了自動(dòng)注入就是通過(guò)自定義Transform并將自定義Transform注冊(cè)到自定義gradle插件中,而卻我們自定義的Transform是優(yōu)先于ProguardTransform執(zhí)行的,所以不會(huì)造成因?yàn)榛煜鵁o(wú)法掃描到類(lèi)信息。

自定義Gradle 插件

Gradle官方文檔目前定義插件只有3中方式:

  • Build script,即:腳本插件,直接在構(gòu)建腳本(build.gradle)中直接寫(xiě)插件的代碼,編譯器會(huì)自動(dòng)將插件編譯并添加到構(gòu)建腳本的classpath中。但是該插件在構(gòu)建腳本之外是不可見(jiàn)的,所以不能在定義它的構(gòu)建腳本之外重用該插件。

  • buildSrc project,執(zhí)行Gradle時(shí)會(huì)將根目錄下的buildSrc目錄作為插件源碼目錄進(jìn)行編譯,并將編譯結(jié)果加入到構(gòu)建腳本的classpath中,所以對(duì)整個(gè)項(xiàng)目是可用的,方便調(diào)試插件。

  • Standalone project,在獨(dú)立項(xiàng)目中開(kāi)發(fā)插件,然后將項(xiàng)目打成jar包,發(fā)布到本地或者maven服務(wù)器上,可在多個(gè)項(xiàng)目間復(fù)用,不好調(diào)試插件。

  • 最后,除了第一種方式,后面兩種方式都是可以發(fā)布到本地或者maven服務(wù)器上,提供給其他項(xiàng)目使用的。所以我推薦buildSrc project這種方式開(kāi)發(fā)插件。

我最后選擇的是 buildSrc project 開(kāi)發(fā)插件。

配置自定義gradle 插件的環(huán)境

1、首先在工程下新建一個(gè)java Libray項(xiàng)目,把其他無(wú)用的資源文件和目錄刪掉就保留src目錄和build.gradle文件;

2、在main目錄下新建resources/MATE-INF/gradle-plugins目錄,如:

plugin.png

并在gradle-plugins目下新建xxx.properties文件,而xxx是可以隨意定義名稱(chēng),而這個(gè)名稱(chēng)(xxx)就是你的插件的名稱(chēng),以后要引用該插件你可以通過(guò) apply plugin: 'xxx' 方式引用插件。

當(dāng)然除了這種配置還有另一中比較簡(jiǎn)單的配置方式:

apply plugin: 'java-gradle-plugin'

    gradlePlugin {
        plugins {
        create('compoentPlugin') {
            id = 'xxx'
            implementationClass = 'com.github.plugin.ModuleComponentPluginKt'
        }
    }
}

gradle 中可以這樣定義Plugin ,并不一定在resources/META-INF/gradle-plugins/xxx.properties中定義插件。

3、xxx.properties文件中的內(nèi)容就是

implementation-class=com.github.plugin.ModuleComponentPluginKt

implementation-class是固定寫(xiě)法,而com.github.plugin.ModuleComponentPluginKt就是你的自定義插件類(lèi)的全類(lèi)名,同一個(gè)項(xiàng)目中還可以定義多個(gè)插件,你可以按照功能分插件引入項(xiàng)目。

4、build.gradle配置

apply plugin: 'groovy'
apply plugin: 'java'
apply plugin: 'kotlin'
apply plugin: 'kotlin-android-extensions'


buildscript {
ext.kotlin_version = '1.3.50'
repositories {
    mavenCentral()
    jcenter()
    google()
}
dependencies {
    classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
sourceSets {
    main {
        groovy {
            srcDir '../buildSrc/src/main/groovy'
      }

    java {
        srcDir "../buildSrc/src/main/java"
    }

    kotlin {
        srcDir "../buildSrc/src/main/kotlin"
    }

    resources {
        srcDir '../buildSrc/src/main/resources'
    }
}
}

dependencies {
repositories {
    mavenCentral()
    jcenter()
    google()
}
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation gradleApi()
implementation localGroovy()
implementation group: 'org.ow2.asm', name: 'asm', version: '7.1'
implementation group: 'org.ow2.asm', name: 'asm-commons', version: '7.1'
implementation 'com.android.tools.build:gradle:3.4.2'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
}
sourceCompatibility = "8"
targetCompatibility = "8"

因?yàn)槲业牟寮鞘褂玫腒otlin編寫(xiě),所以這build.gradle的配置會(huì)有kotlin的配置,以及asm的依賴(lài)等等相關(guān)。

Transform Api Android 提供的,所以你必須引入這個(gè)依賴(lài):implementation 'com.android.tools.build:gradle:3.4.2'

開(kāi)發(fā)gradle 插件

正如xxx.properties文件中定義的全類(lèi)名,所以在com.github.plugin包下
ModuleComponentPluginKt類(lèi),讓ModuleComponentPluginKt實(shí)現(xiàn)至org.gradle.api.Plugin接口,代碼如下:

class ModuleComponentPluginKt : Plugin<Project> {
private lateinit var mProject: Project
override fun apply(project: Project) {
    this.mProject = project
    KLogger.inject(project.logger)
    KLogger.e("自定義插件ModuleComponentPluginKt")
    PluginInitializer.initial(project)

    if (project.plugins.hasPlugin(AppPlugin::class.java)) {
        // 監(jiān)聽(tīng)每個(gè)任務(wù)的執(zhí)行時(shí)間
        project.gradle.addListener(BuildTimeListener())
        val android = project.extensions.getByType(AppExtension::class.java)
        //主要操作就是收集滿(mǎn)足條件的類(lèi)
        android.registerTransform(ScannerComponentTransformKt())
        //收集完畢,在這里完成代碼的織入
        android.registerTransform(ScannerAfterTransformKt())
    }
}
}

在ModuleComponentPluginKt 類(lèi)中,通過(guò)project獲取到AppExtension并調(diào)用registerTransform方法將我們自定義的Transform注冊(cè)到AppExtension,而AppExtension就是application plugins。也就是App module的apply plugin: 'com.android.application'插件為com.android.application。

在ModuleComponentPluginKt 中還有 PluginInitializer.initial(project)是什么意思呢?這個(gè)也比較重要,代碼如下:

  object PluginInitializer {
    fun initial(project: Project) {
    val hasAppPlugin = project.plugins.hasPlugin(AppPlugin::class.java)
    val hasLibPlugin = project.plugins.hasPlugin(LibraryPlugin::class.java)
    if (!hasAppPlugin && !hasLibPlugin) {
        throw  GradleException("Component: The 'com.android.application' or 'com.android.library' plugin is required.")
    }
    this.project = project
    //  創(chuàng)建extensions  ,可以通過(guò)extensions.getByType拿到這個(gè)拓展對(duì)象
    project.extensions.create(COMPONENT_CONFIG_NAME, ComponentExtension::class.java)
    }
    lateinit var project: Project
  }

在 PluginInitializer 類(lèi)中比較重要的這行代碼

project.extensions.create("componentExt", ComponentExtension::class.java)

拓展類(lèi):

open class ComponentExtension {
var matcherInterfaceType: String = "" //組件實(shí)現(xiàn)接口 如:com/github/plugin/common/IComponent
var matcherManagerTypeMethod: String = "" //管理類(lèi)初始化方法  如: initComponent
var matcherManagerType: String = "" //管理類(lèi)的全類(lèi)名  如:com/github/plugin/common/InjectManager
}

這行代碼就是給插件創(chuàng)建拓展(extensions)名字是componentExt,為什么會(huì)創(chuàng)建extensions,先看看使用就明白:

componentExt {
    matcherInterfaceType "com.github.plugin.common.IComponent"
    matcherManagerType "com.github.plugin.common.InjectManager"
    matcherManagerTypeMethod "initComponent"
}

是不是明白了extensions的作用了,其實(shí)就是我們需要提供開(kāi)發(fā)者動(dòng)態(tài)的配置一些信息,這樣會(huì)更靈活。

Android Transform 結(jié)合Asm字節(jié)碼插樁完成代碼自動(dòng)注入(重點(diǎn))

自定義插件的代碼比較簡(jiǎn)單,基本上都是套路,通過(guò)拿到AppExtension并將我們自定義的Transform注冊(cè)進(jìn)去,看看那Transform的代碼,感興趣可以去Transform API

class ScannerComponentTransformKt : Transform() {
override fun getName(): String {
    return "scanner_component_result"
}

override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> {
    return TransformManager.CONTENT_CLASS
}

override fun isIncremental(): Boolean {
    return false
}

override fun getScopes(): MutableSet<in QualifiedContent.Scope> {
    return TransformManager.SCOPE_FULL_PROJECT
}

override fun transform(transformInvocation: TransformInvocation) {

    if (!transformInvocation.isIncremental) {
        transformInvocation.outputProvider.deleteAll()
    }

    transformInvocation.inputs.forEach { input ->
        input.directoryInputs.forEach { dirInput ->
            //處理完輸入文件之后,要把輸出給下一個(gè)任務(wù),就是在:transforms\ScannerComponentTransformKt\debug\0目錄中
            // name就是會(huì)在__content__.json文件中的name,唯一的,隨便取,但是一定要保證唯一
            val dest = transformInvocation.outputProvider.getContentLocation(DigestUtils.md5Hex(dirInput.name),
                    dirInput.contentTypes, dirInput.scopes, Format.DIRECTORY).also(FileUtils::forceMkdir)

            //1、遍歷目錄中的文件;
            //2、修改這些文件;
            //3、然后將這些修改過(guò)的文件,復(fù)制到transforms的輸出目錄,那么為什么將這些修改過(guò)的文件放到transforms,
            // 就會(huì)被打包到apk中呢?因?yàn)槲覀冏远x的transforms會(huì)優(yōu)先于其他transform執(zhí)行并且是優(yōu)先于其他的執(zhí)行,詳細(xì)的
            //可以去看看BaseExtension的構(gòu)造方法
            dirInput.file.eachFileRecurse { file ->
                // dest===> transforms\ScannerComponentTransformKt\debug\0 D8編譯成dex文件
                // file===> build\intermediates\javac\debug\compileDebugJavaWithJavac\classes\com\github\plugin\examlple\MainActivity.class javac 編譯生成的字節(jié)碼


                //現(xiàn)在來(lái)認(rèn)證一下,通過(guò)asm修改的字節(jié)碼,是否在javac 或 transforms中?
                //確實(shí)會(huì)存在于transforms目錄中,但是javac中不存在
                if (TypeUtil.isMatchCondition(file.name)) {
                    val outputFile = File(file.absolutePath.replace(dirInput.file.absolutePath, dest.absolutePath))
                    FileUtils.touch(outputFile)


                    //Dest目錄: build\intermediates\transforms\ScannerComponentTransformKt\debug\0
                    //輸入文件:  build\intermediates\javac\debug\compileDebugJavaWithJavac\classes\com\github\plugin\examlple\Inject.class
                    //輸出文件: build\intermediates\transforms\ScannerComponentTransformKt\debug\0\com\github\plugin\exalple\Inject.class
                    KLogger.e("inputFile: ${file.absolutePath}   outputFile: ${outputFile.absolutePath}   destFile: ${dest.absolutePath}")

                    val inputStream = FileInputStream(file)
                    // 開(kāi)始織入代碼,修改這些文件,即:對(duì)輸入的文件進(jìn)行修改
                    val bytes = WeaveSingleClass.weaveSingleClassToByteArray(inputStream)//需要織入代碼
                    //修改輸入文件完畢復(fù)制輸出文件中
                    val fos = FileOutputStream(outputFile)
                    fos.write(bytes)
                    fos.close()
                    inputStream.close()
                }
            }
            //這里和上面的處理是一樣的,將目錄中的文件復(fù)制到dest目錄中
//                FileUtils.copyDirectory(dirInput.file, dest)
        }


        //首先jar是需要解壓因?yàn)閖ar是通過(guò)zip進(jìn)行壓縮的
        // TODO 多模塊需要處理Jar,因?yàn)閘ib最后打包是已jar形式引入
        //common\build\intermediates\runtime_library_classes\debug\classes.jar
        //usercenter\build\intermediates\runtime_library_classes\debug\classes.jar
        input.jarInputs.forEach { jarInput ->
            if (jarInput.file.absolutePath.endsWith(".jar")) {

                //用于存放臨時(shí)操作的class文件,當(dāng)操作完畢,便將臨時(shí)文件拷貝到dest文件即可
                val tmpFile = File(jarInput.file.parent + File.separator + "classes_temp.jar")
                if (tmpFile.exists()) tmpFile.delete() //避免上次的緩存被重復(fù)插入
                val tmpJarOutputStream = JarOutputStream(FileOutputStream(tmpFile))

                //jar文件
                val jarFile = JarFile(jarInput.file)
                //拿到所有的jar中的文件
                val enumeration = jarFile.entries()

                //用于保存JAR文件,修改JAR中的class
                while (enumeration.hasMoreElements()) {
                    val jarEntry = enumeration.nextElement()
                    val entryName = jarEntry.name
                    val zipEntry = ZipEntry(entryName)

                    if (zipEntry.isDirectory) continue

                    //讀取jar中的文件輸入流
                    val inputStream = jarFile.getInputStream(jarEntry)

                    //插樁class
                    if (TypeUtil.isMatchCondition(entryName)) {
                        KLogger.e("ASM 開(kāi)始處理Jar文件中${entryName}文件")
                        tmpJarOutputStream.putNextEntry(zipEntry)
                        val updateCodeBytes = WeaveSingleClass.weaveSingleClassToByteArray(inputStream)
                        tmpJarOutputStream.write(updateCodeBytes)
                        KLogger.e("ASM 結(jié)束處理Jar文件中${entryName}文件")
                    } else {
                        KLogger.e("不滿(mǎn)足條件Jar文件中${entryName}文件")
                        tmpJarOutputStream.putNextEntry(zipEntry)
                        tmpJarOutputStream.write(IOUtils.toByteArray(inputStream))
                    }

                    tmpJarOutputStream.closeEntry()
                }
                //結(jié)束
                tmpJarOutputStream.close()
                jarFile.close()

                // 將臨時(shí)class文件拷貝到目標(biāo)dest文件
                var jarName = jarInput.name//重名名輸出文件,因?yàn)榭赡芡?會(huì)覆蓋
                val md5Name = DigestUtils.md5Hex(jarInput.file.absolutePath)
                //截取.jar,即 去掉.jar       name就是會(huì)在__content__.json文件中的name,唯一的
                // name就是會(huì)在__content__.json文件中的name,唯一的,隨便取,但是一定要保證唯一
                if (jarName.endsWith(".jar")) jarName = jarName.substring(0, jarName.length - 4)
                val dest = transformInvocation.outputProvider.getContentLocation(jarName + md5Name,
                        jarInput.contentTypes, jarInput.scopes, Format.JAR)

                //input: build\intermediates\runtime_library_classes\debug\classes.jar
                //                    //output: build\intermediates\transforms\ScannerComponentTransformKt\debug\0.jar
                //                    //KLogger.e("input: ${jarInput.file.absolutePath}  output: ${dest.absolutePath}")
                //                    //KLogger.e("${jarInput.name}   $jarName     ${jarName + md5Name}")

                FileUtils.copyFile(tmpFile, dest)
                tmpFile.delete()
            }
        }
    }

    KLogger.e("transform..................end")
}
}

可以看到在Transform的transform方法中通過(guò)directoryInputs和jarInputs就可以拿到目錄下的.class文件和Jar中的.class文件,也叫輸入數(shù)據(jù),這里我叫上游,而TransformOutputProvider的getContentLocation方法就是輸出,也叫下游。

注意:在Transform中,無(wú)論是否更新或修改某個(gè)輸入文件,你都必須將這些輸入文件復(fù)制到指定Transform的目錄中,不然打包的APK是找不到類(lèi)的。即: ATransform(上游) 的輸出則作為 BTransform(下游)的輸入,而這個(gè)過(guò)程就是中最后的產(chǎn)物就是APK。

其實(shí)整個(gè)構(gòu)建流程可以比作是以工廠流水線,而Transform則是流水線上專(zhuān)門(mén)負(fù)責(zé)特定某個(gè)任務(wù)節(jié)點(diǎn)。即:上一個(gè)節(jié)點(diǎn)的輸出則作為下一個(gè)節(jié)點(diǎn)的輸入,所以字節(jié)碼插裝就是得益于Android 給我們提供的這個(gè)機(jī)制。

asm操作的是class字節(jié)碼,在整個(gè)構(gòu)建過(guò)程中所有的字節(jié)碼都是在Transform中作為輸入,所以我們只需要遍歷Transform的輸入數(shù)據(jù),對(duì)于我們的Transform而言就是收集滿(mǎn)足條件的字節(jié)碼文件,然后通過(guò)asm織入一個(gè)我們的指定管理類(lèi)即可,對(duì)于Transform Api我不過(guò)多的介紹,網(wǎng)上很多博客寫(xiě)得非常好,大家可以去看看。

還是那句話Transform 不管你是否修改class或不修改class這個(gè)class輸入文件,都必須復(fù)制到指定transform的目錄中,不然打包的apk是找不到類(lèi)的,比如:你的MainActivity 繼承Androidx 的AppCompatActivity,那么如果你不處理AppCompatActivity的Jar,就會(huì)奔潰拋出ClassFileNotFoundException異常。但是你將MainActivity 的父類(lèi)繼承為Activity,那么就不會(huì)奔潰,因?yàn)锳ctivity屬于 Android.jar,而 Android.jar 則屬于系統(tǒng)類(lèi),Transform不會(huì)對(duì)android.jar中class做任何搜集和處理,即Transform你必須處理文件并將其寫(xiě)入輸出文件夾。即使不處理類(lèi)文件,你仍然必須將它們復(fù)制到輸出文件夾。如果你不這樣做,所有的類(lèi)文件都會(huì)被刪除。

asm 代碼如下:

object WeaveSingleClass {
fun weaveSingleClassToByteArray(inputStream: InputStream): ByteArray {
    //1、解析字節(jié)碼
    val classReader = ClassReader(inputStream)
    //2、修改字節(jié)碼
    val classWriter = ExtendClassWriter(ClassWriter.COMPUTE_MAXS)
    val customClassVisitor = CustomInjectClassVisitor(classWriter)
    //3、開(kāi)始解析字節(jié)碼
    classReader.accept(customClassVisitor, ClassReader.EXPAND_FRAMES)
    return classWriter.toByteArray()
        }


fun weaveSingleClassToByteArrayAutoInject(inputStream: InputStream): ByteArray {
    //1、解析字節(jié)碼
    val classReader = ClassReader(inputStream)
    //2、修改字節(jié)碼
    val classWriter = ExtendClassWriter(ClassWriter.COMPUTE_MAXS)
    val customClassVisitor = AutoInjectComponentClassVisitor(classWriter)
    //3、開(kāi)始解析字節(jié)碼
    classReader.accept(customClassVisitor, ClassReader.EXPAND_FRAMES)
    return classWriter.toByteArray()
}
}

// 訪問(wèn)class信息

class AutoInjectComponentClassVisitor(classVisitor: ClassVisitor) : ClassVisitor(Opcodes.ASM7, classVisitor) {
//如果是實(shí)現(xiàn)了IComponent接口的話,將所有組件類(lèi)收集起來(lái),通過(guò)修改字節(jié)碼的方式生成注冊(cè)代碼到組件管理類(lèi)中
override fun visit(version: Int, access: Int, name: String?, signature: String?, superName: String?, interfaces: Array<out String>?) {
    KLogger.e("${interfaces?.joinToString { it }}")
    KLogger.e("name>>>----$name")
    if (interfaces?.contains(PluginInitializer.getComponentInterfaceName()) == true && name != "") {
        ComponentNameCollection.add("$name")
    }
    super.visit(version, access, name, signature, superName, interfaces)
}

override fun visitMethod(access: Int, name: String, descriptor: String, signature: String?, exceptions: Array<out String>?): MethodVisitor {
    KLogger.e("name:$name     descriptor:$descriptor")

    val visitMethod = super.visitMethod(access, name, descriptor, signature, exceptions)
    if (PluginInitializer.getComponentManagerTypeInitMethodName() != name) {
        return visitMethod
    }
    return AutoInjectComponentMethodVisitor(visitMethod, access, name, descriptor)
}
}

// 訪問(wèn)method信息

class AutoInjectComponentMethodVisitor(methodVisitor: MethodVisitor?, access: Int, name: String?, descriptor: String?)
: AdviceAdapter(Opcodes.ASM7, methodVisitor, access, name, descriptor) {
override fun onMethodExit(opcode: Int) {
    KLogger.e("${ComponentNameCollection.size}    $opcode")

    mv.visitVarInsn(ALOAD, 0)
    mv.visitFieldInsn(GETFIELD, PluginInitializer.getComponentManagerTypeName(), "components", "Ljava/util/List;")
    mv.visitMethodInsn(INVOKEINTERFACE, "java/util/List", "clear", "()V", true)

    ComponentNameCollection.forEach { name ->
        KLogger.e(">><<<>>>>>>${name}")
        // 加載this
        mv.visitVarInsn(ALOAD, 0)
        //拿到類(lèi)的成員變量     坑,你需要注意的類(lèi)名不要寫(xiě)錯(cuò)了
        mv.visitFieldInsn(GETFIELD, PluginInitializer.getComponentManagerTypeName().replace(".", "/"), "components", "Ljava/util/List;")
        //用無(wú)參構(gòu)造方法創(chuàng)建一個(gè)組件實(shí)例
        mv.visitTypeInsn(Opcodes.NEW, name)
        mv.visitInsn(Opcodes.DUP)
        mv.visitMethodInsn(Opcodes.INVOKESPECIAL, name, "<init>", "()V", false)
        mv.visitMethodInsn(INVOKEINTERFACE, "java/util/List", "add", "(Ljava/lang/Object;)Z", true)
        mv.visitInsn(POP)
    }
}
}

最后產(chǎn)生的字節(jié)碼之前和之后對(duì)比如下:

public class InjectManager {
public synchronized void initComponent() { }
}

   ..........之后..........

public class InjectManager {
private List<IComponent> components = new ArrayList();
public synchronized void initComponent() {
    this.components.clear();
    this.components.add(new MainComponent());
    this.components.add(new UserComponent());
    this.components.add(new OrderComponent());
}
  }

這樣就完成了組件化在編譯期自動(dòng)注入其他組件初始化,當(dāng)你要使用的就直接調(diào)用InjectManager .initComponent()就可以了。其實(shí)還有更好的方式就是像 android hilt 那樣通過(guò)修改類(lèi)的繼承方式,把所有的邏輯放在了父類(lèi)中,讓我們Application 去繼承該Application即可。

為什么我會(huì)定義IComponent接口并讓所有初始化組件實(shí)現(xiàn),這是因?yàn)楹笃诳赡茉黾右恍┢渌δ芑虿僮?,比如:增加組件初始化的優(yōu)先級(jí),那么 IComponent接口 直接增加一個(gè)優(yōu)先級(jí)方法即可完成,而不需要在去修改我們織入字節(jié)碼的操作,那太復(fù)雜了容易出錯(cuò)。

AOP 的利器:ASM 3.0 介紹

參考借鑒文章:

我看很多人留言說(shuō)需要代碼,我最近給大家寫(xiě)了個(gè)模板,希望對(duì)大家有用:

abstract class IncrementalTransform extends Transform {

// 共享線程池
//protected final WaitableExecutor globalSharedThreadPool = WaitableExecutor.useGlobalSharedThreadPool()

protected final ThreadPool threadPool = new ThreadPool()

private Project project

IncrementalTransform(Project project) {
    this.project = project
}

@Override
void transform(TransformInvocation transformInvocation)
        throws TransformException, InterruptedException, IOException {
    doTransform(transformInvocation)
}

private void doTransform(TransformInvocation invocation) {
    TransformOutputProvider outputProvider = invocation.outputProvider

    if (!invocation.isIncremental()) {
        outputProvider.deleteAll()
    }

    invocation.inputs.each { TransformInput transformInput ->
        // JAR
        transformInput.jarInputs.each { JarInput jarInput ->
            threadPool.addTask(new ITask() {
                @Override
                Void call() throws Exception {
                    return handleJar(jarInput, outputProvider, invocation)
                }
            })
        }

        // DIR
        transformInput.directoryInputs.each { DirectoryInput directoryInput ->
            threadPool.addTask(new ITask() {
                @Override
                Void call() throws Exception {
                    return handleDirectory(directoryInput, outputProvider, invocation)
                }
            })
        }
    }

    //等待所有任務(wù)結(jié)束
    //globalSharedThreadPool.waitForTasksWithQuickFail(true)

    threadPool.startWork()
}

private void handleJar(
        JarInput jarInput,
        TransformOutputProvider outputProvider,
        TransformInvocation invocation) {

    //得到上一個(gè)Transform輸入文件
    File inputJar = jarInput.file
    // 得到當(dāng)前Transform輸出Jar文件
    File outputJar =
            outputProvider.getContentLocation(
                    jarInput.name, jarInput.contentTypes,
                    jarInput.scopes, Format.JAR)

    if (invocation.isIncremental()) {// 增量處理
        if (jarInput.status == Status.NOTCHANGED) {//文件沒(méi)有改變
            println("IncrementalTransform >>> File NOTCHANGED")
        } else if (jarInput.status == Status.ADDED) {//有新增文件

            dispatchAction(inputJar, outputJar, true)

        } else if (jarInput.status == Status.CHANGED) {//有修改文件

            FileUtils.deleteIfExists(outputJar)// 先把上次生成的文件刪除
            dispatchAction(inputJar, outputJar, true)

        } else if (jarInput.status == Status.REMOVED) {//文件被移除

            //把上次當(dāng)前Transform輸出文件刪除
            FileUtils.delete(outputJar)
        }
    } else {// 全量處理
        dispatchAction(inputJar, outputJar, true)
    }
}

private void handleDirectory(
        DirectoryInput directoryInput, TransformOutputProvider outputProvider,
        TransformInvocation invocation) {
    //得到上一個(gè)Transform輸入文件目錄
    File inputDir = directoryInput.file

    // 得到當(dāng)前Transform輸出文件目錄
    File outputDir =
            outputProvider.getContentLocation(
                    directoryInput.name, directoryInput.contentTypes,
                    directoryInput.scopes, Format.DIRECTORY)

    if (invocation.isIncremental()) {
        directoryInput.changedFiles.entrySet().each { Map.Entry<File, Status> entry ->
            File inputFile = entry.key

            if (entry.value == Status.NOTCHANGED) {//文件沒(méi)有改變
                println("IncrementalTransform >>> File NOTCHANGED")
            } else if (entry.value == Status.ADDED) {//有增加文件

                File outputFile = FileUtil.toOutputFile(outputDir, inputDir, inputFile)
                dispatchAction(inputFile, outputFile, false)

            } else if (entry.value == Status.CHANGED) {//文件有修改

                File outputFile = FileUtil.toOutputFile(outputDir, inputDir, inputFile)
                FileUtils.deleteIfExists(outputFile)// 先把上次生成的文件刪除
                dispatchAction(inputFile, outputFile, false)

            } else if (entry.value == Status.REMOVED) {//文件被移除

                //把上次輸出的目錄刪除
                File outputFile = FileUtil.toOutputFile(outputDir, inputDir, inputFile)
                FileUtils.deleteIfExists(outputFile)
            }
        }
    } else {
        // 上一個(gè)Transform的輸出目錄下的所有文件
        FluentIterable<File> dirChildFiles = FileUtils.getAllFiles(inputDir)

        dirChildFiles.each { File inputFile ->
            // 當(dāng)前Transform輸出文件
            File outputFile = FileUtil.toOutputFile(outputDir, inputDir, inputFile)
            dispatchAction(inputFile, outputFile, false)
        }
    }
}


protected void dispatchAction(
        File inputFile, File outputFile, boolean handleJar) {
    if (handleJar) {//JAR
        // 輸出目標(biāo)jar文件
        FileOutputStream fos = new FileOutputStream(outputFile)
        JarOutputStream outputJarOs = new JarOutputStream(fos)

        //處理輸入Jar文件
        JarFile inputJarFile = new JarFile(inputFile)
        Enumeration<JarEntry> entries = inputJarFile.entries()

        while (entries.hasMoreElements()) {
            JarEntry inputJarEntry = entries.nextElement()
            String inputJarEntryName = inputJarEntry.getName()

            // 拿到j(luò)ar包里面的輸入流
            InputStream inputJarEntryInputStream =
                    inputJarFile.getInputStream(inputJarEntry)

            // 構(gòu)造目標(biāo)jar文件里的文件實(shí)體  即保持和上一個(gè)JarEntry名稱(chēng)一致
            outputJarOs.putNextEntry(new ZipEntry(inputJarEntryName))

            //如果不做是否處理,那么僅僅只是將該文件復(fù)制到當(dāng)前Transform的目標(biāo)輸出文件即可
            boolean isHandle =
                    doJarAction(inputJarEntryInputStream, outputJarOs)
            if (!isHandle) {
                //將修改過(guò)的字節(jié)碼copy到dest
                outputJarOs.write(
                        IOUtils.toByteArray(inputJarEntryInputStream)
                )
            }
            inputJarEntryInputStream.close()
        }

        outputJarOs.closeEntry()
        outputJarOs.close()
        inputJarFile.close()
        return
    }
    //DIR

    //如果不做是否處理,那么僅僅只是將該文件復(fù)制到當(dāng)前Transform的目標(biāo)輸出文件即可
    boolean isHandle = doDirectoryAction(inputFile, outputFile)
    if (!isHandle) {
        //將修改過(guò)的字節(jié)碼copy到dest
        FileUtil.copyFileAndMkdirsAsNeed(inputFile, outputFile)
    }
}

/**
 *  處理Jar文件的資源
 *
 *  這些都是在工作線程中執(zhí)行的
 *
 * @param inputStream 上一個(gè)Transform的輸入流
 * @param outputStream 當(dāng)前Transform的輸出流
 * @return isHandle 是否已經(jīng)處理了該文件,如果已經(jīng)處理了文件返回 true
 *
 */
protected abstract boolean doJarAction(InputStream inputStream, OutputStream outputStream)


/**
 * 處理目錄的資源文件
 *
 *
 * 這些都是在 工作線程中執(zhí)行的
 *
 * @param inputJar 上一個(gè)Transform的輸入文件
 * @param outputJar 當(dāng)前Transform的輸出文件
 * @return 是否已經(jīng)處理了該文件,如果已經(jīng)處理了文件返回 true
 */
protected abstract boolean doDirectoryAction(File inputJar, File outputJar)
}

最后就是你只需要繼承該類(lèi),然后實(shí)現(xiàn)doJarActiondoDirectoryAction方法實(shí)現(xiàn)相應(yīng)功能即可,當(dāng)然這是Groovy版本的。該模本實(shí)現(xiàn)了 增量更新并發(fā)處理 打打提升編譯速度。

升級(jí) AGP 7.0

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

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

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