likes
comments
collection
share

asm字节码手册 - ClassReader与ClassWriter(一)

作者站长头像
站长
· 阅读数 78

ClassReader

本小节 主要是熟悉一下asm 对class的 描述,遇到问题时 完全可以自己写一个ClassReader,看一下对应的asm解析是什么值,方便我们后续修改代码

类型描述符

asm字节码手册 - ClassReader与ClassWriter(一)

看上去很多,其实很好记, 只有boolean 是 Z, long 是J其余的基本类型都是首字母

类的描述符 都是 大写的L 开头,然后 用 / 去做分隔符 比如string就是 Ljava/lang/String

数组就是 [ 作为描述符 多维数组就是多个 [

方法描述符

方法描述符以左括号开头,然后是每个形参的类型描述符,然后是一个右括号,接下来是返回类型的类型描述符,如果该方法返回 void,则是 V(方法描述符中不包含方法的名字或参数名)。

asm字节码手册 - ClassReader与ClassWriter(一)

ClassVisitor

基本用法: 一个ClassReader 来接收一个classVisitor

fun main(){
    val classReader = ClassReader("java.util.ArrayList")
    val classVisitor = ClassReadVisitor(Opcodes.ASM7)
    classReader.accept(classVisitor, ClassReader.SKIP_DEBUG)
}

可以看下这个接口,我们读取class的时候 主要就是依托于这个类

asm字节码手册 - ClassReader与ClassWriter(一)

这里要注意的是 这些visit的调用方法 是有顺序的

visit->visitSource->visitOuterClass->(visitAnnotation和visitAttribute)->(visitInnerClass、visitField 和 visitMethod)->visitEnd

尤其要注意的是 visit的access这个参数

asm字节码手册 - ClassReader与ClassWriter(一)

这个参数 我们拿到的是一个int值,它对应着Opcodes里面的值

asm字节码手册 - ClassReader与ClassWriter(一)

我们主要关注 ACC开头的即可

这里要注意的是 这里的变量都是 16进制来表示的, 也就是说 我们判断一个access的时候 有可能出现这个access的int值 囊括了 多种acc的 值,linux的权限系统,等等 也是用的16进制来表示 常量。

  1. 方便组合权限:使用二进制数字可以将每个权限看作是一个二进制位,便于将多个权限组合为一个数字,而按位或操作可以将多个二进制位合并为一个新的二进制数,表示同时具有这些权限。
  2. 方便检查权限:使用按位与操作符可以方便地检查一个二进制数字中是否包含某个特定权限。如果该权限对应的二进制位是1,则按位与结果为非零值,表示具有该权限,否则按位与结果为零,表示没有该权限。

所以这里我们要做的就是 要写一个函数,传入一个access的int值 然后返回对应的 acc的field name即可

object Utils {
    // key 是16进制转10进制的int值, value是对应的 field name
    private val mapAccess = mutableMapOf<Int, String>()
    // key 是 field name, value是int值
    private val mapOpcodes = mutableMapOf<String, Int>()

    init {
        Opcodes::class.java.fields.forEach {
            if (it.name.startsWith("ACC_")) {
                mapAccess[it.getInt(null)] = it.name
            } else if (it.type == Int::class.java) {
                mapOpcodes[it.name] = it.getInt(null)
            }
        }
    }

    fun Int.accCode2String(): String {
        val sb = StringBuilder()
        mapAccess.forEach {
            if ((this and it.key) > 0) {
                sb.append("${it.value} ")
            }
        }
        return sb.toString()
    }

}

有了上述代码 我们就可以逐一 利用若干个visit的 函数回调 来逐一体会 asm对类的解析操作了

override fun visit(
    version: Int,
    access: Int,
    name: String?,
    signature: String?,
    superName: String?,
    interfaces: Array<out String>?
) {
    println("method visit---------start----------------")
    // 可以从这里获取到这个类 的acc 类型
    println("access:$access ${access.accCode2String()} ")
    // 包含包名的
    println("name:$name ")
    // 这稍微看下就好,实际asm代码很少用到他
    println("signature:$signature")
    // 父类
    println("superName:$superName")
    // 继承了哪些接口
    interfaces?.forEach {
        println("interface:${it}")
    }
    println("method visit-----------end--------------")


    super.visit(version, access, name, signature, superName, interfaces)
}

asm字节码手册 - ClassReader与ClassWriter(一)

内部类

override fun visitInnerClass(name: String?, outerName: String?, innerName: String?, access: Int) {

    println("method visitInnerClass---------start----------------")
    println("access:$access ${access.accCode2String()} ")
    println("name:$name ")
    println("outerName:$outerName")
    println("innerName:$innerName")
    println("method visitInnerClass-----------end--------------")

    super.visitInnerClass(name, outerName, innerName, access)
}

这里要注意的就是 visitInnerClass 会执行多次的,因为一个类显然可以有多个内部类, asm字节码手册 - ClassReader与ClassWriter(一)

唯一要注意的是 name和innerName 的 区别

field

这里要注意的是kotlin 默认生成的返回值FieldVisitor 是不带?的,如果不带? 运行起来会报错, 所以这里一定要记得手动改一下 让这个函数的返回值是一个可空类型

override fun visitField(
    access: Int,
    name: String?,
    descriptor: String?,
    signature: String?,
    value: Any?
): FieldVisitor? {

    println("method visitInnerClass---------start----------------")
    println("access:$access ${access.accCode2String()} ")
    println("name:$name ")
    println("descriptor:$descriptor")
    println("signature:$signature")
    println("value:$value")
    println("method visitInnerClass-----------end--------------")

    return super.visitField(access, name, descriptor, signature, value)
}

这个地方其实要关注的就是Descriptor,

asm字节码手册 - ClassReader与ClassWriter(一)

method

这个写起来和field 其实没啥区别

override fun visitMethod(
    access: Int,
    name: String?,
    descriptor: String?,
    signature: String?,
    exceptions: Array<out String>?
): MethodVisitor? {

    println("method visitMethod---------start----------------")
    println("access:$access ${access.accCode2String()} ")
    println("name:$name ")
    println("descriptor:$descriptor")
    println("signature:$signature")
    exceptions?.forEach {
        println("exceptions:$it")
    }
    println("method visitMethod-----------end--------------")
    return super.visitMethod(access, name, descriptor, signature, exceptions)
}

唯一要注意的是程序的输出

这个clinit方法是类初始化的方法,切记是类初始化 asm字节码手册 - ClassReader与ClassWriter(一)

而init方法 才是我们常关注的对象的构造方法,两者不要搞混了

asm字节码手册 - ClassReader与ClassWriter(一)

ClassWriter

生成一个类

假设我们想生成的类长成这样:

asm字节码手册 - ClassReader与ClassWriter(一)

注意asm生成的类直接就是可运行的字节码了,并不是注解处理器那种生成的源代码,这有本质不同

可以看下生成的文件 ,其实就是一个class文件 asm字节码手册 - ClassReader与ClassWriter(一)

fun main(){
    val classWriter = ClassWriter(0)
    // 第二个参数 注意写法 复合类型 这里因为是一个接口 不能被实例化 所以一定得包含ACC_ABSTRACT 属性
    // 第三个参数 类的名字,注意要包含包名
    // 第四个参数 因为没有泛型 所以传null 即可
    // 第五个参数 不用说了。。
    // 第6个参数 就是这个类继承了哪些接口
    classWriter.visit(
        Opcodes.V1_7, Opcodes.ACC_PUBLIC + Opcodes.ACC_ABSTRACT + Opcodes.ACC_INTERFACE,
        "pkg/Comparable",null, "java/lang/Object", arrayOf("pkg/Mesureable"))
    classWriter.visitField(Opcodes.ACC_PUBLIC+ Opcodes.ACC_FINAL + Opcodes.ACC_STATIC, "LESS", "I", null,  -1).visitEnd()
    classWriter.visitField(Opcodes.ACC_PUBLIC+ Opcodes.ACC_FINAL + Opcodes.ACC_STATIC, "EQUAL", "I", null,  0).visitEnd()
    classWriter.visitField(Opcodes.ACC_PUBLIC+ Opcodes.ACC_FINAL + Opcodes.ACC_STATIC, "GREATER", "I", null,  1).visitEnd()
    classWriter.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_ABSTRACT, "compareTo", "(Ljava/lang/Object;)I" ,null, null).visitEnd()
    classWriter.visitEnd()
    val byte = classWriter.toByteArray()
    // 实际写文件
    File("asm_example/files/Comparable.class").sink().buffer().apply {
        write(byte)
        flush()
        close()
    }
}

转换类

上面我们动态生成了一个类,且版本是1.7版本。现在我们尝试用classreader和writer的方式 复制一下这个类 并且让这个类的java版本变成1.8版本 (因为纯复制一个类 并没有什么用,io复制都行)

前面一个小节 我们写过Comparable的字节码,所以这里只要专注于 method和field 就可以了,

fun main() {
    val inputStream =
        FileInputStream(File("/asm-module/asm_example/files/Comparable.class"))
    val clasReader = ClassReader(inputStream)
    val classWriter = ClassWriter(0);
    clasReader.accept(object : ClassVisitor(Opcodes.ASM7) {
        override fun visit(
            version: Int,
            access: Int,
            name: String?,
            signature: String?,
            superName: String?,
            interfaces: Array<out String>?
        ) {
            // 在这里修改了java8的版本号
            classWriter.visit(Opcodes.V1_8, access, name, signature, superName, interfaces)
        }

        override fun visitField(
            access: Int,
            name: String?,
            descriptor: String?,
            signature: String?,
            value: Any?
        ): FieldVisitor {
            return classWriter.visitField(access, name, descriptor, signature, value)
        }

        override fun visitMethod(
            access: Int,
            name: String?,
            descriptor: String?,
            signature: String?,
            exceptions: Array<out String>?
        ): MethodVisitor {
            return classWriter.visitMethod(access, name, descriptor, signature, exceptions)
        }
    }, ClassReader.SKIP_CODE)

    val byte = classWriter.toByteArray()
    // 实际写文件
    File("asm_example/files2/Comparable.class").sink().buffer().apply {
        write(byte)
        flush()
        close()
    }

}

可以看到这个版本号已经修改成1.8了

asm字节码手册 - ClassReader与ClassWriter(一)

然而上述的修改class的方式一般不会这么使用, 因为在效率上有点低 ,通常而言我们会这样使用:

val classWriter2 = ClassWriter(classReader2, ClassWriter.COMPUTE_MAXS)  //将 classWrite 传入一个 classReader 参数

其实主要就是将reader 传到writer内部即可

移除类的成员

在某些时候 我们需要移除类的成员,不同的成员移除方法并不一样

asm字节码手册 - ClassReader与ClassWriter(一)

对于内部类 外部类 等返回值void的 成员来说,我们只要 不要实现对应的方法即可。

但是对于field 以及 method来说, 要移除他们的关键 在于 对移除的成员进行return null处理

比如看下面这个类, 有2个属性 name和author,以及对应的get set 方法 asm字节码手册 - ClassReader与ClassWriter(一)

现在我们想 删除这个name的属性,并且也删除对应的get set 方法

clasReader.accept(object : ClassVisitor(Opcodes.ASM7) {
    override fun visit(
        version: Int,
        access: Int,
        name: String?,
        signature: String?,
        superName: String?,
        interfaces: Array<out String>?
    ) {
        // 在这里修改了java8的版本号
        classWriter.visit(Opcodes.V1_8, access, name, signature, superName, interfaces)
    }
    
    override fun visitField(
        access: Int,
        name: String?,
        descriptor: String?,
        signature: String?,
        value: Any?
    ): FieldVisitor? {
        if (name == "name") {
            return null
        }
        return classWriter.visitField(access, name, descriptor, signature, value)
    }

    override fun visitMethod(
        access: Int,
        name: String?,
        descriptor: String?,
        signature: String?,
        exceptions: Array<out String>?
    ): MethodVisitor? {
        if (name == "getName" || name == "setName") {
            return null
        }
        println("visitMethod name:$name")
        return classWriter.visitMethod(access, name, descriptor, signature, exceptions)
    }
}, ClassReader.SKIP_CODE)

增加类成员

增加类成员也是有固定模版的,我们在对应成员的visit方法中判断一下 是否包含了这个成员,然后在visit end方法中 进行实际的添加即可 (这里切记不要在原有visit成员方法中直接添加 否则会报错)

我们就用上一个小节中的例子,对InnerDemo这个类 来添加一个 int类型的age 字段

// 这次应为是添加类成员,所以这里的构造函数 要把这个writer也传进去
clasReader.accept(object : ClassVisitor(Opcodes.ASM7,classWriter) {

    var ageExist = false
    override fun visit(
        version: Int,
        access: Int,
        name: String?,
        signature: String?,
        superName: String?,
        interfaces: Array<out String>?
    ) {
        // 在这里修改了java8的版本号
        classWriter.visit(Opcodes.V1_8, access, name, signature, superName, interfaces)
    }

    override fun visitField(
        access: Int,
        name: String?,
        descriptor: String?,
        signature: String?,
        value: Any?
    ): FieldVisitor? {
        if (name == "age") {
            ageExist = true
        }
        return classWriter.visitField(access, name, descriptor, signature, value)
    }

    override fun visitMethod(
        access: Int,
        name: String?,
        descriptor: String?,
        signature: String?,
        exceptions: Array<out String>?
    ): MethodVisitor? {
        return classWriter.visitMethod(access, name, descriptor, signature, exceptions)
    }

    override fun visitEnd() {
        super.visitEnd()
        if (!ageExist){
            val filedVisitor = cv?.visitField(Opcodes.ACC_PUBLIC, "age", "I", null, 0)
            filedVisitor?.visitEnd()
        }

    }
}, ClassReader.SKIP_CODE)

type 辅助工具

前面介绍过一些asm 对类的描述符,应该和我们平常写java代码的时候很不一样,比如int 对应的是I,asm内置了type工具 可以辅助我们 理解asm的描述符,这对我们新手来说 很重要,也很有帮助。

class TypeDemo {
    fun testMethod(value: String):Int {
        return value.length
    }
}

/**
 * Type 类型是 ASM 提供的一个辅助类,用于对内部类型转换
 */
fun main() {
    // getInternalName 方法返回一个 Type 的内部名。例如,
    // Type.getType(String.class). getInternalName()给出 String 类
    val getInternalName = Type.getType(String::class.java).internalName
    val getInternalName2 = Type.getInternalName(String::class.java)
    println("$getInternalName,$getInternalName2")
    // getDescriptor 方法返回一个 Type 的述符。
    val getDescriptor = Type.getDescriptor(String::class.java)
    val getDescriptor2 = Type.getType(String::class.java).descriptor
    println("$getDescriptor,$getDescriptor2")

    // Type 获取方法的描述符,传入 Method 对象
    val getMethodDescriptor = Type.getMethodDescriptor(TypeDemo::class.java.getDeclaredMethod("testMethod", String::class.java))
    println("getMethodDescriptor = $getMethodDescriptor")
    // Type 获取方法的描述符,传入方法的返回值类型和参数类型
    val getMethodDescriptor2 = Type.getMethodDescriptor(Type.INT_TYPE, Type.LONG_TYPE)
    println("getMethodDescriptor2 = $getMethodDescriptor2")

    val getArgumentType = Type.getArgumentTypes(TypeDemo::class.java.getDeclaredMethod("testMethod", String::class.java))
    println("getArgumentType = ${getArgumentType[0]}")

    val getReturnType = Type.getReturnType(TypeDemo::class.java.getDeclaredMethod("testMethod", String::class.java))
    println("getReturnType = $getReturnType")
}

看下执行结果

asm字节码手册 - ClassReader与ClassWriter(一)

从io中获取class的有效信息

对于android开发来说,更多使用asm的场景是在于gradle编译期间,我们习惯于从io中获取一个class文件, 那么对于这种形式,怎么利用type 来读取这个class文件的信息呢?

先定义一个classloader

class MyClassloader : ClassLoader() {

    @Throws(IOException::class)
    fun loadClassFromFile(filePath: String, className: String): Class<*> {
        val classBytes = getClassBytesFromFile(filePath)
        return defineClass(className, classBytes, 0, classBytes.size)
    }

    @Throws(IOException::class)
    private fun getClassBytesFromFile(filePath: String): ByteArray {
        FileInputStream(filePath).use { inputStream ->
            ByteArrayOutputStream().use { outputStream ->
                val buffer = ByteArray(4096)
                var bytesRead: Int
                while (inputStream.read(buffer).also { bytesRead = it } != -1) {
                    outputStream.write(buffer, 0, bytesRead)
                }
                return outputStream.toByteArray()
            }
        }
    }
}

然后利用该classloader 去加载出实际的class文件 即可


val myClassloader = MyClassloader()
// 这里最重要的就是不要遗漏包名
val loadedClass = myClassloader.loadClassFromFile("asm-module/asm_example/files2/TestInner.class", "com.andoter.asm_example.part2.vivo.TestInner")
val getMethodDescriptor3 = Type.getMethodDescriptor(loadedClass::class.java.getDeclaredMethod("getName"))
println("getMethodDescriptor3 = $getMethodDescriptor3")


TraceClassVisitor

这个和之前的ClassWriter的例子 区别不大,但是很重要的是,这个Trace可以很方便的让我们知道 我们到底干了啥, 他可以很方便的打印出日志, 让我们知道 我们想生成的字节码到底是个什么东西

interface TraceClassVisitorDemo {
    var className: String
    var classVersion: Int

    fun getTraceInfo(): String
}

/**
 * 依照上面的 TraceClassVisitorDemo 为例
 */
fun main() {
    val classWriter = ClassWriter(0)

    // 最重要的就是这里了 可以将我们的过程 用标准输出 打印出来方便调试
    val traceClassWriter =
        TraceClassVisitor(classWriter, PrintWriter(System.out))
    traceClassWriter.visit(
        Opcodes.V1_7,
        Opcodes.ACC_PUBLIC + Opcodes.ACC_INTERFACE + Opcodes.ACC_ABSTRACT,
        "com.andoter.asm_example.part2/TraceClassVisitorDemo",
        null,
        "java/lang/Object",
        null
    )
    traceClassWriter.visitSource("TraceClassVisitorDemo.class", null)
    traceClassWriter.visitField(Opcodes.ACC_PUBLIC+ Opcodes.ACC_FINAL + Opcodes.ACC_STATIC, "className", "Ljava/lang/String;", null, "").visitEnd()
    traceClassWriter.visitField(Opcodes.ACC_PUBLIC+ Opcodes.ACC_FINAL + Opcodes.ACC_STATIC, "classVersion", "I", null, 50).visitEnd()
    traceClassWriter.visitMethod(
        Opcodes.ACC_PUBLIC + Opcodes.ACC_ABSTRACT,
        "getTraceInfo",
        "()Ljava/lang/String;",
        null,
        null
    ).visitEnd()
    traceClassWriter.visitEnd()

    ClassOutputUtil.byte2File("asm_example/files/TraceClassVisitorDemo1.class", classWriter.toByteArray())
}

运行起来可以看日志:

asm字节码手册 - ClassReader与ClassWriter(一)

CheckClassAdapter

这个相比于trace而言,更加好用,可以帮我们检测出来很多错误,免的我们生成的字节码有错误,造成很多不必要的麻烦

使用起来非常简单 ,用这个adapter 包裹一下即可:

val classWriter = ClassWriter(0)
val checkClassAdapter = CheckClassAdapter(classWriter)

checkClassAdapter.visit()......

当你写错的时候 会有很友好的提示:

asm字节码手册 - ClassReader与ClassWriter(一)

asm字节码手册 - ClassReader与ClassWriter(一)

转载自:https://juejin.cn/post/7212488918498558012
评论
请登录