认识下 Kotlin 反射背后的男人:@Metadata

Posted Kotlin

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了认识下 Kotlin 反射背后的男人:@Metadata相关的知识,希望对你有一定的参考价值。

概述

本文简单介绍了下注解 Metadata 各个字段的含义及其与反射的关系。

正文

Kotlin 允许我们对各种 Kotlin 的语法特性进行访问,不过,这里应该有一个问题没有搞清楚:既然 Java 反射对于 Kotlin 的很多特性都无法访问和识别,换句话说,Java 虚拟机也是无法知道他们的,那么 Kotlin 的反射是如何做到这一点的呢?

这实际上主要是得益于 kotlin.Metadata 这个注解。Kotlin 反射过程中,注解的内容解析之后会实例化一个叫做 KotlinClassHeader 的类。下面我们给出这二者成员的对应关系:

Metadata KotlinClassHeader 说明
k kind 注解标注目标类型,例如类、文件等等
mv metadataVersion 该元数据的版本
bv bytecodeVersion 字节码版本
d1 data 自定义元数据
d2 strings 自定义元数据补充字段
xs extraString 附加字段
xi extraInt 1.1 加入的附加标记,标记类文件的来源类型

有关这些字段的详细含义,建议大家直接参考源码注释。

Metadata 当中还有一个字段 pn,表示包名,该字段在反射中暂时没有用到。

下面我们重点介绍下 d1、d2 这两个字段。

  • d1:存储了自定义格式的元数据,官方声称针对不同的类型格式不定,甚至可以为空,研究发现目前采用 Protobuf 进行序列化存储。这些数据会被 Kotlin 反射读取,是反射的一个非常重要的数据来源。其中包含不限于类型、函数、属性等的可见性、类型是否可空、函数是否为 suspend等等信息。

  • d2:存储明文字符串字面量,主要存储 Jvm 签名等信息。之所以这样设计,主要是为了将这些字符串在运行时直接加载到虚拟机内存的常量池中予以复用,减少内存开销。

例如:

 
   
   
 
  1. open class SuperClass

  2. class SubClass : SuperClass() {

  3.    val aProp: Int = 0

  4.    fun aFun(param: String): Long {

  5.        return 0

  6.    }

  7. }

我们定义了两个类,反编译了他们的字节码之后,我们得到的 Java 类上面会出现 Metadata 注解:

 
   
   
 
  1. @Metadata(

  2.   mv = {1, 1, 9}, //元数据版本为 1.1.9

  3.   bv = {1, 0, 2}, //字节码版本为 1.0.2

  4.   k = 1, // 1 表示 Class,即注解标注的对象为 Class

  5.   d1 = {"经过编码的二进制,不可直接阅读,省略之"},

  6.   d2 = {"Lcn.kotliner/SubClass;", "Lcn.kotliner/SuperClass;", "()V", "aProp", "", "getAProp", "()I", "aFun", "", "param", "", "production sources for module Reflections_main"}

  7. )

  8. public final class SubClass extends SuperClass {

  9.    ...

  10. }

d1 存储的内容是经过 Protobuf 序列化之后的结果,为了满足Java虚拟机注解值类型的要求,这里将序列化之后的字节转为字符串。此外,d1 中使用换行符 \n 来分隔,分开的结果与 d2 的元素一一对应。这里还有一个小细节, d1 为什么是一个字符串数组而不是一个字符串,原因主要是字符串长度有限制,如果 d1 存储的内容超过了字符串长度的上限,就拆成多个字符串存储。

Java 虚拟机字节码中字符串使用 CONSTANTUtf8info 结构来存储,该结构中使用两个字节的无符号数来存储字符串的长度, 换句话说,Java 虚拟机字节码中字符串的最大长度为 65535 (216 - 1)。

d1 与 d2 之间有对应关系,在这个例子当中,从 d2 的值很容易看出 d2 存储的内容是被标注的类的类名、父类名、属性、函数等等。值得一提的是,d2 当中也存储了函数参数名 param,也正是这样,Kotlin 反射才可以在 Java 1.8 之前的字节码版本中获取函数参数名。

Java 反射从 1.8 之后才可以在特定条件下访问函数的参数名。

由于 Kotlin 反射是通过读取 Metadata 当中的值来获取类的信息的,那么我们对编译后的类文件进行混淆,必须注意要保留 Metadata 这个注解,同时,涉及到反射获取类及其成员的情况,需要注意这些类和成员都不可以被混淆。



以上是关于认识下 Kotlin 反射背后的男人:@Metadata的主要内容,如果未能解决你的问题,请参考以下文章

Kotlin 反射有个坑你们知道么!

Kotlin|Kotlin反射

.NET知识梳理——2.反射

这位 GitHub 冠军项目背后的“老男人”,堪称 10 倍程序员本尊!

Kotlin|Kotlin反射

年入百万美元,“网页版PS”和它背后的男人