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(一)