ASM字节码插桩之Transform的替代方案

前言

最近在学习asm字节码插桩相关的知识,发现在高版本的gradle上以前的Transform已经废弃,于是研究了一下新版本字节码插桩的实现,本文将简单地介绍下新版本gradle上字节码插桩的实现。

Demo代码:GitHub - SmilingTeresa/AsmDemo: ASM插桩Demo

新建插桩Module

在工程下新建一个module,可以选择Android/JAVA Library,module下只需要保留java目录、gitignore和gradle文件 

image.png

 在src下新建resources资源文件夹,在resources添加META-INF/gradle-plugins路径,在此路径下添加com.test.asmplugin.properties 属性文件(命名一般是xxx.xxx.xxx.properties,这个命名会关系到在引用插件的时候的名称)。

在properties文件中指明gradle插件的入口类 

image.png

修改gradle配置

引入ASM依赖

在gradle中添加asm所需要的依赖

plugins {
    id 'kotlin'
    id 'kotlin-kapt'
}
apply plugin: 'groovy'

dependencies {
    //gradle sdk
    implementation gradleApi()
    //groovy sdk
    implementation localGroovy()

    implementation 'org.ow2.asm:asm-commons:9.2'
    implementation 'com.android.tools.build:gradle:7.2.1'
}

demo插件是使用kotlin语言编写的,所以这个地方需要引入kotlin的插件依赖,如果涉及到groovy插件的编写,还需要引入groovy插件依赖。部分asm的插件已经包含在了gradle插件里面了,所以此处只需要额外引入gradle插件依赖和asm-commons依赖。

maven-publish配置

插件编写完了之后,需要上传到maven库,然后其他module就可以正常依赖了。因此需要配置maven发布参数。此处采用maven-publish,配置如下:

plugins {
    id 'maven-publish'
}

publishing {
    publications {
        release(MavenPublication) {
            groupId "com.test"
            artifactId "asmplugin"
            version "0.0.1"

            from components.java
        }
    }

    repositories {
        maven {
            //推送到本地
            url = uri('../plugin')
        }
    }
}

引入maven-publish插件之后,依次配置好groupId、artifactId、version参数,然后repositories里面的maven地址,测试的时候可以按照上面写的,先推送到工程目录下。最后测试完了需要上传maven库的时候可替换为对应的maven库地址。

编写插桩代码

接下来简单的实现一个插桩功能,根据gradle配置向指定的类中插入一段Log打印的代码。

创建Plugin

需要继承自Plugin<Project>,重写apply方法。 apply中实现分为以下几步:

  1. 创建Extension对象
  2. 读取Extension配置
  3. Android变体中设置ASM
  4. 变体中设置AsmClassVisitorFactory用于字节码插桩,并且传递参数到AsmClassVisitorFactory中
class AsmPlugin: Plugin<Project> {

    override fun apply(project: Project) {
        project.extensions.create("asmconfig", AsmExtension::class.java)

        val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
        androidComponents.onVariants { variant ->
            val extension = project.extensions.getByType(AsmExtension::class.java)
            println("AsmPlugin ${extension.specificClass}")
            variant.instrumentation.setAsmFramesComputationMode(
                FramesComputationMode.COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS
            )
            variant.instrumentation.transformClassesWith(
                TestClassVistorFactory::class.java,
                InstrumentationScope.PROJECT

            ) { params ->
                params.specificClass.set(extension.specificClass)
            }
        }


    }

}

创建Extension

extension的作用,是可以用于在gradle里面配置相应的参数,并且在apply中读取出来传递到相应的代码逻辑中使用。 需要新建一个extension类决定需要配置的参数。

open class AsmExtension {
    open var specificClass: MutableList<String> = mutableListOf()
}

以AsmExtension为例,配置了需要插桩的类。 然后在apply方法中创建gradle配置项,只有创建了之后gradle中才能识别自定义的配置项。

project.extensions.create("asmconfig", AsmExtension::class.java)

其次在需要的时候读取我们gradle中的配置。

val extension = project.extensions.getByType(AsmExtension::class.java) 

创建AsmClassVisitorFactory

代码如下:

abstract class TestClassVistorFactory: AsmClassVisitorFactory<AsmParameters> {

    override fun createClassVisitor(
        classContext: ClassContext,
        nextClassVisitor: ClassVisitor
    ): ClassVisitor {
        return TestClassVisitor(nextClassVisitor)
    }

    override fun isInstrumentable(classData: ClassData): Boolean {
        println("isInstrumentable classData ${classData.className}")
        parameters.get().specificClass.get().forEach {
            if (classData.className.contains(it)) {
                println("isInstrumentable classData true")
                return true
            }
        }
        return false
    }
}

首先可以看到class是继承自AsmClassVisitorFactory<AsmParameters>,后面的泛型里面的类是用来传递参数到AsmClassVisitorFactory里面使用的,这个地方自定义了AsmParameters是因为要把前面的Extension读取到的配置传递到AsmClassVisitorFactory里面,正常不存在参数传递的话,直接使用InstrumentationParameters.None就可以了。

AsmParameters的代码如下:

interface AsmParameters: InstrumentationParameters {

    @get: Internal
    val specificClass: ListProperty<String>
}

涉及到的注解的写法,以及property的使用可以点到InstrumentationParameters源码里面去查看。

然后TestClassVistorFactory里面做了两件事情:

  • 重写createClassVisitor用于访问class类,进行后续的字节码插桩。
  • 重写isInstrumentable用来限制哪些类需要访问,可以看到代码逻辑里面读取了parameters里面的specificClass属性,这个就是之前extension里面传递过来的配置,指定了需要插桩的类。

创建ClassVisitor和MethodVisitor

最后就是插桩具体实现,首先需要创建ClassVistor来访问类,代码如下:

class TestClassVisitor(classVisitor: ClassVisitor): ClassVisitor(Opcodes.ASM7, classVisitor) {

    override fun visitMethod(
        access: Int,
        name: String?,
        descriptor: String?,
        signature: String?,
        exceptions: Array<out String>?
    ): MethodVisitor {
        val methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions)
        return TestMethodVisitor(api, methodVisitor, access, name?:"", descriptor?:"")
    }
}

重写了visitMethod方法,用来访问方法。然后在方法返回中返回自定义的MethodVisitor来实现对代码的插桩,代码如下:

class TestMethodVisitor(
    api: Int,
    methodVisitor: MethodVisitor,
    access: Int,
    name: String,
    descriptor: String
): AdviceAdapter(api, methodVisitor, access, name, descriptor) {

    override fun onMethodEnter() {
        println("onMethodEnter")
        super.onMethodEnter()
        mv.visitLdcInsn("Test.class")
        mv.visitLdcInsn("aaa start")
        mv.visitMethodInsn(
            INVOKESTATIC, "android/util/Log", "d",
            "(Ljava/lang/String;Ljava/lang/String;)I", false
        )
        mv.visitInsn(POP)
    }

    override fun onMethodExit(opcode: Int) {
        mv.visitLdcInsn("Test.class")
        mv.visitLdcInsn("aaa end")
        mv.visitMethodInsn(
            INVOKESTATIC, "android/util/Log", "d",
            "(Ljava/lang/String;Ljava/lang/String;)I", false
        )
        mv.visitInsn(POP)
        super.onMethodExit(opcode)
    }
}

methodVisitor是继承自ASM的AdviceAdapter类,这个类中已经封装好了onMethodEnter和onMethodExit两个方法。分别用于在方法进入和退出的时候进行代码插桩。里面的字节码的写法可以使用AMS Bytecode Viewer这个插件,编写好正常的java或者kotlin代码。然后用这个插件转换一下变成字节码的写法拷贝过来使用即可。

此处可以看到在onMethodEnter和onMethodExit里面分别插入了一行Log打印。

发布插件

最后就是执行gradle中的publish命令来将插件发布到工程目录下

image.png

 发布后的仓库目录如下:

image.png

 0.0.1中就存放了jar包。

编写测试代码

gradle配置

plugins {
    id 'com.test.asmplugin'
}

asmconfig {
    specificClass = ['com.test.asmdemo.Test']
}

项目根目录下的gradle需要添加插件依赖,引入的路径就是上面的发布配置中的groupId:artifactId:version

buildscript {
    dependencies {
        classpath 'com.test:asmplugin:0.0.1'
    }
}

对应的需要使用的app module中引入插件,引入的名称就是之前properties文件的名称

plugins {
    id 'com.test.asmplugin'
}

asmconfig {
    specificClass = ['com.test.asmdemo.Test']
}

底下进行extension的配置,设置想要插桩的类

添加测试类

object Test {
    fun test() {
        Log.i("Test",  "test")
    }
}

activity中调用test进行测试

测试效果如下,可以看到插桩已经生效

image.png

参考文档

  1. Android Gradle 插件 API 更新:Android Gradle 插件 API 更新  |  Android Studio  |  Android Developers
  2. Android Gradle 插件版本说明:Android Gradle 插件 8.4 版本说明  |  Android Studio  |  Android Developers
  3. 官方Demo:gradle-recipes/Kotlin/modifyProjectClasses/app/build.gradle.kts at agp-7.4 · android/gradle-recipes · GitHub

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值