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

前言

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

Demo代码:github.com/SmilingTere...

新建插桩Module

在工程下新建一个module,可以选择Android/JAVA Library,module下只需要保留java目录、gitignore和gradle文件 在src下新建resources资源文件夹,在resources添加META-INF/gradle-plugins路径,在此路径下添加com.test.asmplugin.properties 属性文件(命名一般是xxx.xxx.xxx.properties,这个命名会关系到在引用插件的时候的名称)。

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

修改gradle配置

引入ASM依赖

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

bash 复制代码
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,配置如下:

javascript 复制代码
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中
kotlin 复制代码
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类决定需要配置的参数。

kotlin 复制代码
open class AsmExtension {
    open var specificClass: MutableList<String> = mutableListOf()
}

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

arduino 复制代码
project.extensions.create("asmconfig", AsmExtension::class.java)

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

ini 复制代码
val extension = project.extensions.getByType(AsmExtension::class.java)

创建AsmClassVisitorFactory

代码如下:

kotlin 复制代码
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的代码如下:

less 复制代码
interface AsmParameters: InstrumentationParameters {

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

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

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

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

创建ClassVisitor和MethodVisitor

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

kotlin 复制代码
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来实现对代码的插桩,代码如下:

kotlin 复制代码
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命令来将插件发布到工程目录下

发布后的仓库目录如下:

0.0.1中就存放了jar包。

编写测试代码

gradle配置

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

arduino 复制代码
buildscript {
    dependencies {
        classpath 'com.test:asmplugin:0.0.1'
    }
}

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

bash 复制代码
plugins {
    id 'com.test.asmplugin'
}

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

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

添加测试类

kotlin 复制代码
object Test {
    fun test() {
        Log.i("Test",  "test")
    }
}

activity中调用test进行测试

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

相关推荐
Dnelic-3 小时前
【单元测试】【Android】JUnit 4 和 JUnit 5 的差异记录
android·junit·单元测试·android studio·自学笔记
Eastsea.Chen5 小时前
MTK Android12 user版本MtkLogger
android·framework
长亭外的少年12 小时前
Kotlin 编译失败问题及解决方案:从守护进程到 Gradle 配置
android·开发语言·kotlin
建群新人小猿15 小时前
会员等级经验问题
android·开发语言·前端·javascript·php
1024小神16 小时前
tauri2.0版本开发苹果ios和安卓android应用,环境搭建和最后编译为apk
android·ios·tauri
兰琛16 小时前
20241121 android中树结构列表(使用recyclerView实现)
android·gitee
Y多了个想法17 小时前
RK3568 android11 适配敦泰触摸屏 FocalTech-ft5526
android·rk3568·触摸屏·tp·敦泰·focaltech·ft5526
NotesChapter18 小时前
Android吸顶效果,并有着ViewPager左右切换
android
_祝你今天愉快19 小时前
分析android :The binary version of its metadata is 1.8.0, expected version is 1.5.
android
暮志未晚Webgl19 小时前
109. UE5 GAS RPG 实现检查点的存档功能
android·java·ue5