前言
最近在学习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中实现分为以下几步:
- 创建Extension对象
- 读取Extension配置
- Android变体中设置ASM
- 变体中设置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进行测试
测试效果如下,可以看到插桩已经生效