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进行测试

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

相关推荐
千里马学框架1 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台1 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone1 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc1 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo1 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077001 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼1 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone1 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen1 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone1 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui