众所周知, Transform 接口在 AGP8.0 中已经被移除了, 而以往的大量工程实践都需求运用该接口结合 ASM 对字节码进行操作, 例如: 代码插桩, 代码替换等.

接下来将探索在 AGP8.0 中有哪些计划能够替代 Transform 到达相同的效果.

一. Instrumentation

Instrumentation 作为 Google 首推的 Transform 替代计划, 只需求完成 AsmClassVisitorFactory 接口即完成对字节码处理.

Transform 处理流程:

flowchart LR
input[Jar/classes]
transform1([Transform])
intermediate1[Jar/classes]
transform2([Transform])
output[Jar/classes]
input --> transform1
transform1 --> intermediate1
intermediate1 --> transform2
transform2 --> output

Instrumentation 处理流程:

flowchart LR
input[Jar/classes]
transform1([ClassVisitor])
transform2([ClassVisitor])
output[Jar/classes]
input --> transform1
transform1 --> transform2
transform2 --> output

相较于 Transform 处理流程, Instrumentation 流程免去对中间产品的读写, 而且一定是以 Jar/classes 文件为单位的增量处理, 使得全量编译和增量编译速度都有较大的提升.

插桩实践

下面以完成一个的 TraceAsmClassVisitorFactory 为例, 实践如何运用 Instrumentation 相关接口.

TraceAsmClassVisitorFactory 将在一切方法进口和出口插桩 Trace#beginSectionTrace#endSection 代码.

TraceClassVisitor

class TraceClassVisitor(
    api: Int,
    next: ClassVisitor
) : ClassVisitor(api, next) {
    private var className: String? = null
    // 开端处理 class 文件时, 记载当时 class 的姓名
    override fun visit(
        version: Int,
        access: Int,
        name: String?,
        signature: String?,
        superName: String?,
        interfaces: Array<out String>?
    ) {
        className = name
        super.visit(version, access, name, signature, superName, interfaces)
    }
    // 遍历 method 时, 运用 MethodVisitor 参加插桩逻辑
    override fun visitMethod(
        access: Int,
        name: String?,
        descriptor: String?,
        signature: String?,
        exceptions: Array<out String>?
    ): MethodVisitor {
        val mv = super.visitMethod(access, name, descriptor, signature, exceptions)
        return TraceMethodVisitor(api, mv, access, name, descriptor)
    }
    private inner class TraceMethodVisitor(
        api: Int,
        methodVisitor: MethodVisitor?,
        access: Int,
        name: String?,
        descriptor: String?
    ) : AdviceAdapter(
        api,
        methodVisitor,
        access,
        name,
        descriptor
    ) {
        // method 进口刺进 `Trace#beginSection` 代码
        override fun onMethodEnter() {
            mv.visitLdcInsn("$className#$name")
            mv.visitMethodInsn(
                Opcodes.INVOKESTATIC,
                "android/os/Trace",
                "beginSection",
                "(Ljava/lang/String;)V",
                false
            )
        }
        // method 出口刺进 `Trace#endSection` 代码
        override fun onMethodExit(opcode: Int) {
            mv.visitMethodInsn(
                INVOKESTATIC,
                "android/os/Trace",
                "endSection",
                "()V",
                false
            )
        }
    }
}

TraceAsmClassVisitorFactory

abstract class TraceAsmClassVisitorFactory: AsmClassVisitorFactory<InstrumentationParameters.None> {
    // 对一切 class 文件都进行插桩处理
    override fun isInstrumentable(classData: ClassData): Boolean {
        return true
    }
    // 遍历 class 时, 运用 ClassVisitor 参加插桩逻辑
    override fun createClassVisitor(
        classContext: ClassContext,
        nextClassVisitor: ClassVisitor
    ): ClassVisitor {
        val apiVersion = instrumentationContext.apiVersion.get()
        return TraceClassVisitor(apiVersion, nextClassVisitor)
    }
}

TraceTransformPlugin

abstract class TraceTransformPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        project.plugins.withId("com.android.application") {
            project.extensions.configure(ApplicationAndroidComponentsExtension::class.java) { extension ->
                extension.onVariantsAsmClassVisitorFactory { variant ->
                    // 注册用于插桩的 AsmClassVisitorFactory
                    variant.instrumentation.transformClassesWith(
                        TraceAsmClassVisitorFactory::class.java,
                        InstrumentationScope.ALL
                    ) {}
                    //  因为对 method 进行了插桩, 需求从头计算被插桩函数的栈帧
                    variant.instrumentation.setAsmFramesComputationMode(FramesComputationMode.COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS)
                }
            }
        }
    }
}

优下风

  • 优势: 运用简略, 只需重视 ClassVisitor 逻辑, 无需处理 class 文件的读取与写入.
  • 下风: class 处理进程是固定的, 只能完成相对简略的代码插桩与替换, 无法满意个性化需求.

二. Transform Task

经过 Instrumentation 的确能很方便的完成插桩与替换逻辑, 但是理想很丰满,现实很骨感, 许多时候并不能满意需求, 例如:

  • 先剖析再操作: 在完成 hook 框架时, 一般需求先对工程中的一切 class 进行一次遍历剖析后, 再对相关的 class 文件进行字节码操作.

  • 输出文件产品: 上文中的 TraceClassVisitor 在调用 Trace#beginSection 时传入了完整的类名与方法名, 这往往会导致生成的 trace 文件过大. 工程实践中一般会 生成 methodId类名#方法名 的映射, 在 Trace#beginSection 调用时只是传入 methodId, 这时就需求在插桩结束后保存当时的 methodId类名#方法名 的映射文件.

当对 class 的处理进程有自定义需求时, 就需求经过 Transform Task 来完成. Transform Task 经过自己完成一个 Gradle Task, 然后运用 ApplicationAndroidComponentsExtension 提供的接口将其注册到 class 的处理流程中.

class 处理流程:

flowchart LR
source[classes]
dependencies[jars]
intermediate_classes[classes]
intermediate_jars[jars]
output[jar]
asm_source([transformClassesWithAsm])
asm_dependencies([transformJartsWithAsm])
transform_task([Transform Task])
source --> asm_source
asm_source --> intermediate_classes
dependencies --> asm_dependencies
asm_dependencies --> intermediate_jars
intermediate_classes --> transform_task
intermediate_jars --> transform_task
transform_task --> output

Transform Task 会在 Instrumentation 履行完成后再履行, 而且输出的产品是一个包含一切 class 的 jar 文件.

插桩实践

下面以完成一个的 TraceTransformTask 为例, 调整上文的中的 TraceClassVisitor插桩逻辑, 在调用 Trace#beginSection 时传入 methodId 参数, 并在插桩完成后输出 methodId类名#方法名 的映射文件.

TraceClassVisitor

class TraceClassVisitor(
    api: Int,
    next: ClassVisitor,
    private  val mapping: (String) -> Long,
) : ClassVisitor(api, next) {
    private var className: String? = null
    override fun visit(
        version: Int,
        access: Int,
        name: String?,
        signature: String?,
        superName: String?,
        interfaces: Array<out String>?
    ) {
        className = 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 {
        val mv = super.visitMethod(access, name, descriptor, signature, exceptions)
        return TraceMethodVisitor(api, mv, access, name, descriptor)
    }
    private inner class TraceMethodVisitor(
        api: Int,
        methodVisitor: MethodVisitor?,
        access: Int,
        name: String?,
        descriptor: String?
    ) : AdviceAdapter(
        api,
        methodVisitor,
        access,
        name,
        descriptor
    ) {
        override fun onMethodEnter() {
            // 运用 methodId 替代 className#methodName
            val methodId = mapping("$className#$name")
            mv.visitLdcInsn("$methodId")
            mv.visitMethodInsn(
                Opcodes.INVOKESTATIC,
                "android/os/Trace",
                "beginSection",
                "(Ljava/lang/String;)V",
                false
            )
        }
        override fun onMethodExit(opcode: Int) {
            mv.visitMethodInsn(
                INVOKESTATIC,
                "android/os/Trace",
                "endSection",
                "()V",
                false
            )
        }
    }
}

TraceTransformTask

abstract class TransformTask : DefaultTask() {
    // 一切输入的 jar 文件
    @get:InputFiles
    abstract val allJars: ListProperty<RegularFile>
    // 一切输入的 classes 目录
    @get:InputFiles
    abstract val allDirectories: ListProperty<Directory>
    // 输出的 jar 文件
    @get:OutputFile
    abstract val outputJar: RegularFileProperty
    // 输出的 mapping 文件
    @get:OutputFile
    abstract val mappingFile: RegularFileProperty
    @TaskAction
    fun transform() {
        var currentMethodId = 0L
        val mapping = mutableMapOf<String, Long>()
        val factory: (ClassWriter) -> ClassVisitor = { next ->
            TraceClassVisitor(Opcodes.ASM9, next) { method ->
                mapping.getOrPut(method) { currentMethodId++ }
            }
        }
        JarOutputStream(
            outputJar.get().asFile
                .outputStream()
                .buffered()
        ).use { output ->
            // 遍历一切的 jar 文件输入, 并进行插桩
            allJars.get().forEach { jar ->
                transformJar(output, jar.asFile, factory)
            }
            // 遍历一切的 classes 目录输入, 并进行插桩
            allDirectories.get().forEach { dir ->
                transformClasses(output, dir.asFile, factory)
            }
        }
        // 插桩后, 输出 mapping 信息
        mappingFile.get().asFile
            .writeText(mapping.toString())
    }
    private fun transformJar(
        output: JarOutputStream,
        jar: File,
        factory: (ClassWriter) -> ClassVisitor
    ) {
        JarInputStream(
            jar.inputStream()
                .buffered()
        ).use { input ->
            while (true) {
                val entry = input.nextEntry ?: break
                if (entry.isDirectory) continue
                if (!entry.name.endsWith(".class")) continue
                transform(entry.name, input, output, factory)
            }
        }
    }
    private fun transformClasses(
        output: JarOutputStream,
        rootDir: File,
        factory: (ClassWriter) -> ClassVisitor
    ) {
        rootDir.walk().forEach { child ->
            if (child.isDirectory) return@forEach
            if (!child.name.endsWith(".class")) return@forEach
            val name = child.toRelativeString(rootDir)
            child.inputStream()
                .buffered()
                .use { input ->
                    transform(name, input, output, factory)
                }
        }
    }
    // 创建 ClassReader, ClassVisitor, ClassWriter 进行插桩
    private fun transform(
        name: String,
        input: InputStream,
        output: JarOutputStream,
        factory: (ClassWriter) -> ClassVisitor
    ) {
        val entry = ZipEntry(name)
        output.putNextEntry(entry)
        val cr = ClassReader(input)
        val cw = ClassWriter(ClassWriter.COMPUTE_MAXS)
        cr.accept(factory(cw), ClassReader.EXPAND_FRAMES)
        output.write(cw.toByteArray())
        output.closeEntry()
    }
}

TraceTransformPlugin

abstract class TraceTransformPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        project.plugins.withId("com.android.application") {
            project.extensions.configure(ApplicationAndroidComponentsExtension::class.java) { extension ->
                extension.onVariants {
                    // 装备 mapping 文件输出路径
                    val transformTask = project.tasks.register(
                        "transform${it.name.capitalized()}",
                        TransformTask::class.java
                    ) { task ->
                        task.mappingFile.set(project.buildDir.resolve("outputs/transform/mapping.txt"))
                    }
                    // 将 TransformTask 注册到 class 处理流程中
                    it.artifacts
                        .forScope(ScopedArtifacts.Scope.ALL)
                        .use(transformTask)
                        .toTransform(
                            ScopedArtifact.CLASSES,
                            TransformTask::allJars,
                            TransformTask::allDirectories,
                            TransformTask::outputJar
                        )
                }
            }
        }
    }
}

优下风

  • 优势:

    • 处理 class 文件的方法更加灵活, 能够按需完成对应的处理流程, 例如能够先对全工程进行剖析再进行字节码操作.
    • 能够与其它的 Gradle Task 合作, 运用其它 Gradle Task 的输出作为输入或将其输出作为其它 Gradle Task 的输入.
  • 下风:

    • 相较于 Instrumentation 需求处理许多额定的逻辑, 例如对 class 文件的读写.
    • 完成一个高性能的 Task 需求付出许多额定的尽力, 例如完成 Incremental tasks 等.

参考资料

  • Transform API is removed