Gradle Plugin开发基础-Transform

/ 0评 / 0

前言

Gradle Plugin开发基础流程中介绍了如何创建我们自己的插件工程,这次来介绍下如何使用Transform配合ASM进行插桩。使用 AGP Transform API 进行字节码插桩已经非常普遍了,例如 Booster、神策等框架中都有 Transform 的影子。Transform 听起来很高大上,其本质就是一个 Gradle Task

ps:Transform API在AGP 8.0以上已经被删除!!!

Transform 的基本原理

先大概了解下 Transform 的工作机制:

1、工作时机: Transform 工作在 Android 构建中由 Class → Dex 的节点;
2、处理对象: 处理对象包括 Javac 编译后的 Class 文件、Java 标准 resource 资源、本地依赖和远程依赖的 JAR/AAR。Android 资源文件不属于 Transform 的操作范围,因为它们不是字节码;
3、Transform Task: 每个 Transform 都对应一个 Task,Transform 的输入和输出可以理解为对应 Transform Task 的输入输出。每个 TransformTask 的输出都分别存储在 app/build/intermediates/transform/[Transform Name]/[Variant] 文件夹中;
4、Transform 链: TaskManager 会将每个 TransformTask 串联起来,前一个 Transform 的输出会作为下一个 Transform 的输入。

Transform 增量模式

1、增量模式标记位: Transform API 有两个增量标志位,不要混淆:

Transform#isIncremental(): Transform 增量构建的开关,返回 true 才有可能触发增量构建;
TransformInvocation#isIncremental(): 当次 TransformTask 是否增量执行,返回 true 表示正在增量模式。

2、Task 增量模式与 Transform 增量模式的区别: Task 增量模式与 Transform 增量模式的区别在于,Task 增量执行时会跳过整个 Task 的动作列表,而 Transform 增量执行依然会执行 TransformTask,但输入内容会增加变更内容信息。
3、增量模式的输入: 增量模式下的所有输入都是带状态的,需要根据这些状态来做不同的处理,不需要每次所有流程都重新来一遍。比如新增的输入就需要处理,而未修改的输入就不需要处理。Transform 定义了四个输入文件状态:

public enum Status {

    // 未修改,不需要处理,也不需要复制操作
    NOTCHANGED,

    // 新增,正常处理并复制给下一个任务
    ADDED,

    // 已修改,正常处理并复制给下一个任务
    CHANGED,

    // 已删除,需同步移除 OutputProvider 指定的目标文件
    REMOVED;
}

自定义 Transform 模板

我们把整个流程图做成一个抽象模板类,子类需要重写 provideFunction() 方法,从输入流读取 Class 文件,修改完字节码后再写入到输出流。甚至不需要考虑 Trasform 的输入文件遍历、加解压、增量等,舒服!

file

abstract class BaseCustomTransform(private val debug: Boolean) : Transform() {

    abstract fun provideFunction(): ((InputStream, OutputStream) -> Unit)?

    open fun classFilter(className: String) = className.endsWith(SdkConstants.DOT_CLASS)

    override fun isIncremental() = true

    override fun transform(transformInvocation: TransformInvocation) {
        super.transform(transformInvocation)

        log("Transform start, isIncremental = ${transformInvocation.isIncremental}.")

        val inputProvider = transformInvocation.inputs
        val referenceProvider = transformInvocation.referencedInputs
        val outputProvider = transformInvocation.outputProvider

        // 1. Transform logic implemented by subclasses.
        val function = provideFunction()

        // 2. Delete all transform tmp files when not in incremental build.
        if (!transformInvocation.isIncremental) {
            log("All File deleted.")
            outputProvider.deleteAll()
        }

        for (input in inputProvider) {
            // 3. Transform jar input.
            log("Transform jarInputs start.")
            for (jarInput in input.jarInputs) {
                val inputJar = jarInput.file
                val outputJar = outputProvider.getContentLocation(
                    jarInput.name,
                    jarInput.contentTypes,
                    jarInput.scopes,
                    Format.JAR
                )
                if (transformInvocation.isIncremental) {
                    // 3.1 Transform jar input in incremental build.
                    when (jarInput.status ?: Status.NOTCHANGED) {
                        Status.NOTCHANGED -> {
                            // Do nothing.
                        }

                        Status.ADDED, Status.CHANGED -> {
                            // Do transform.
                            transformJar(inputJar, outputJar, function)
                        }

                        Status.REMOVED -> {
                            // Delete.
                            FileUtils.delete(outputJar)
                        }
                    }
                } else {
                    // 3.2 Transform jar input in full build.
                    transformJar(inputJar, outputJar, function)
                }
            }
            // 4. Transform dir input.
            log("Transform dirInput start.")
            for (dirInput in input.directoryInputs) {
                val inputDir = dirInput.file
                val outputDir = outputProvider.getContentLocation(
                    dirInput.name,
                    dirInput.contentTypes,
                    dirInput.scopes,
                    Format.DIRECTORY
                )
                if (transformInvocation.isIncremental) {
                    // 4.1 Transform dir input in incremental build.
                    for ((inputFile, status) in dirInput.changedFiles) {
                        val outputFile = concatOutputFilePath(inputDir, outputDir, inputFile)
                        when (status ?: Status.NOTCHANGED) {
                            Status.NOTCHANGED -> {
                                // Do nothing.
                            }

                            Status.ADDED, Status.CHANGED -> {
                                // Do transform.
                                if (inputFile.isFile) {
                                    doTransformFile(inputFile, outputFile, function)
                                } else if (inputFile.isDirectory) {
                                    log("${inputFile.absolutePath} is dir!!!")
                                }
                            }

                            Status.REMOVED -> {
                                // Delete
                                FileUtils.delete(outputFile)
                            }
                        }
                    }
                } else {
                    // 4.2 Transform dir input in full build.
                    // Traversal fileTree (depthFirstPreOrder).
                    for (inputFile in FileUtils.getAllFiles(inputDir)) {
                        val outputFile = concatOutputFilePath(inputDir, outputDir, inputFile)
                        if (classFilter(inputFile.name)) {
                            doTransformFile(inputFile, outputFile, function)
                        } else {
                            // Copy.
                            Files.createParentDirs(outputFile)
                            FileUtils.copyFile(inputFile, outputFile)
                        }
                    }
                }
            }
        }
        log("Transform end.")
    }

    /**
     * Do transform Jar.
     */
    private fun transformJar(
        inputJar: File,
        outputJar: File,
        function: ((InputStream, OutputStream) -> Unit)?
    ) {
        // Create parent directories to hold outputJar file.
        Files.createParentDirs(outputJar)
        // Unzip.
        FileInputStream(inputJar).use { fis ->
            ZipInputStream(fis).use { zis ->
                // Zip.
                FileOutputStream(outputJar).use { fos ->
                    ZipOutputStream(fos).use { zos ->
                        var entry = zis.nextEntry
                        while (entry != null && isValidZipEntryName(entry)) {
                            if (!entry.isDirectory) {
                                zos.putNextEntry(ZipEntry(entry.name))
                                if (classFilter(entry.name)) {
                                    // Apply transform function.
                                    applyFunction(zis, zos, function)
                                } else {
                                    // Copy.
                                    zis.copyTo(zos)
                                }
                            }
                            entry = zis.nextEntry
                        }
                    }
                }
            }
        }
    }

    /**
     * Do transform file.
     */
    private fun doTransformFile(
        inputFile: File,
        outputFile: File,
        function: ((InputStream, OutputStream) -> Unit)?
    ) {
        // Create parent directories to hold outputFile file.
        Files.createParentDirs(outputFile)
        FileInputStream(inputFile).use { fis ->
            FileOutputStream(outputFile).use { fos ->
                // Apply transform function.
                applyFunction(fis, fos, function)
            }
        }
    }

    private fun concatOutputFilePath(baseDir: File, outputDir: File, inputFile: File): File {
        val relativePath = baseDir.toURI().relativize(inputFile.parentFile.toURI()).path
        val realOutputFile = File(outputDir, relativePath)
        return File(realOutputFile, inputFile.name)
    }

    private fun applyFunction(
        input: InputStream,
        output: OutputStream,
        function: ((InputStream, OutputStream) -> Unit)?
    ) {
        try {
            if (null != function) {
                function.invoke(input, output)
            } else {
                // Copy
                input.copyTo(output)
            }
        } catch (e: UncheckedIOException) {
            throw e.cause!!
        }
    }

    private fun log(logStr: String) {
        if (debug) {
            println("$name - $logStr")
        }
    }
}

示例

往所有Activity的onCreate插入一个Toast调用

class InsertToastTransform(private val project: Project) : BaseCustomTransform(true) {

    override fun getName() = "InsertToastTransform"

    override fun getInputTypes(): Set<QualifiedContent.ContentType> =
        setOf(QualifiedContent.DefaultContentType.CLASSES)

    override fun getScopes(): MutableSet<in QualifiedContent.Scope> =
        EnumSet.of(QualifiedContent.Scope.PROJECT)

    override fun provideFunction(): ((InputStream, OutputStream) -> Unit)? {
        return { inputStream, outputStream ->
            val reader = ClassReader(inputStream)
            val writer = ClassWriter(reader, 0)
            val cv = ToastClassVisitor(writer)
            reader.accept(cv, 0)
            val bytes = writer.toByteArray()
            outputStream.write(bytes)
        }
    }
}

class ToastClassVisitor(cv: ClassVisitor) : ClassVisitor(Opcodes.ASM7, cv) {
    private var isActivity = false

    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)
        isActivity = superName == "androidx/appcompat/app/AppCompatActivity"
    }

    override fun visitMethod(
        access: Int,
        name: String,
        desc: String,
        signature: String?,
        exceptions: Array<out String>?
    ): MethodVisitor {
        var mv = cv.visitMethod(access, name, desc, signature, exceptions)
        if (isActivity && name == "onCreate" && desc == "(Landroid/os/Bundle;)V") {
            println("visitMethod ==> ${name} // ${desc}")
            mv = AddToastAdviceAdapter(mv, access, name, desc)
        }
        return mv
    }
}

class AddToastAdviceAdapter(mv: MethodVisitor, access: Int, name: String?, desc: String?) :
    MethodVisitor(Opcodes.ASM7, mv) {

    override fun visitCode() {
        super.visitCode()
        mv.visitFieldInsn(
            Opcodes.GETSTATIC,
            "com/demo/threadmonitornew/utils/ToastUtils",
            "INSTANCE",
            "Lcom/demo/threadmonitornew/utils/ToastUtils;"
        )
        mv.visitMethodInsn(
            Opcodes.INVOKEVIRTUAL,
            "com/demo/threadmonitornew/utils/ToastUtils",
            "showToast",
            "()V",
            false
        )
    }
}

插件使用

class ThreadPluginPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        val androidExtension = project.extensions.getByType(BaseExtension::class.java)
        androidExtension.registerTransform(InsertToastTransform(project))
    }
}

补充

可以安装一个ASM Bytecode Viewer Support Kotlin插件,在对应的源码上右键ASM Bytecode Viewer,即可直接查看asm代码,并且可以直接对比上一次的结果。

参考地址
https://github.sheincorp.cn/pengxurui/AndroidFamilyDemo/blob/main/HelloTransform/lib/src/main/kotlin/com/pengxr/toastplugin/ToastTransform.kt

https://juejin.cn/post/7098752199575994405#heading-16

另外推荐参考如下仓库:https://github.sheincorp.cn/CB2Git/asm_hook

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注