Kotlin 填坑记之 Metadata
最近用 KAPT 来生成 Kotlin 代码,遇到了一个头疼的问题,生成的 Kotlin 代码需要调用源 Kotlin 代码中被 Annotation
标注的属性,理论上讲,直接用 .
操作符来调用属性不就行了吗?然而,事情并没有想象的那么简单。
Kotlin Property
在 Kotlin 中,Property 在 JVM 层面既有可能是一个字段,也有可能是一个方法,取决于在属性上有没有其它 JVM 相关的注解,例如在下面的例子中:
1 | object Data { |
value
作为 __Property__,在 JVM 层面对外公开的 API 其实是 getValue(): String
,但是,在下面的例子中:
1 | object Data { |
value
在 JVM 层面对外公开的 API 其实是一个 static
字段,对于生成的 Kotlin 代码中,如果要调用这个 value
属性,到底应该是调用 value
这个字段还是调用 getValue()
这个方法?
Kotlin Metadata
用过 KAPT 的同学或多或少地知道,KAPT 其实是基于 APT 来实现的,只不过会在编译期为 Kotlin 代码生成对的 Java 存根,这样 APT 才有机会发挥作用,那对于 Kotlin 编译器而言,它是如何解决 Kotlin Property 的调用问题的呢?这得从 KAPT 为 Kotlin 代码生成的 Java 存根说起。
在 Java 存根中,每个 Class 上都被标注了一个 kotlin.Metadata
的 __Annotation__,如下所示:
1 | .Metadata( |
相信看到这堆字符的同学会一脸懵逼,一堆字符到底是啥意思,d1
是个啥?d2
是个啥?我第一次看到它也一脸懵,如何破解这一堆被编码的符号呢?我的第一反应是 —— 从 Kotlin
官方渠道找设计文档,结果找了一圈,并没有找到相关的文档说明,那还是老老实实研究一下 Kotlin 源代码吧,结果发现了一个有意思的类 —— JvmProtobufUtil.kt,不难发现有这样一个方法:
1 |
|
通过 Annotation 标注的 Element
,我们可以很方便的获取到 Metadata
:
1 | val metadata = ele.getAnnotation(Metadata::class.java) |
结合上面的 @kotlin.Metadata
中的内容,如果把 Metadata
的 data1
和 data2
作为参数传进去会怎么样呢?
1 | fun parseMetadata(ele: Element) { |
试了一下,居然能解析成功!Metadata
里的内容到底是啥呢?根据 Metadata.kt 中的注释,其字段的定义如下:
字段 | 描述 |
---|---|
k | 本注解编码的类型:
|
mv | Metadata 的版本 |
xi | 标志位 |
d1 | metadata.proto |
d2 | 字符串常量池 |
通过 JvmProtoBufUtil.readClassDataFrom
返回的 JvmNameResolver
和 ProtoBuf.Class
,便可以解析出 Metadata
中的编码的所有内容,对于 __Kotlin Property__,便可以通过 ProtoBuf.Class
的 getPropertyList()
来获取到所有的属性:
1 | klass.propertyList.forEach { |
Interoperability
如果我们用 KAPT 生成代码的时候要根据 Annotation 标注的元素的类型来生成相应的 Kotlin 代码,就会发现 Kotlin 中的 String
不能用 Java 中的 String
来代替,因为从类型上来讲,它们确实是两种不同的类型,例如:
1 | object Data { |
如果要对 value
生成一个包装类的话,大概长这样:
1 | class ValueWrapper : Wrapper<java.lang.String> { |
但是,在 ValueWrapper.get()
返回 Data.value
会报错:
1 | Type mismatch. |
WTF!!!怎么会这样???
用 Kotlin 的时候,对于标准库提供的类型,例如:String
,其定义是 kotlin.String
,那为什么在存根文件中和字节码层面却是 java.lang.String
呢?要一窥究竟,还得去扒 Kotlin 源代码 —— ClassMapperLite.kt,原来是 Kotlin 的编译器会将 Kotlin 的标准类型自动转换为 Java 的标准类型,所以,在存根文件中,我们会发现原来定义的 kotlin.String
类型都已经被转换为 java.lang.String
类型了。
因此,如果想要将生成的代码中的 Java 标准类型变成 Kotlin 标准类型,那就需要逆映射,也就是将 ClassMapperLite.kt 中的是映射关系反过来,这样,就可以生成漂亮的 Kotlin 代码了,如下所示:
1 | class ValueWrapper : Wrapper<kotlin.String> { |
Incompatible Kotlin Version
还在使用 Kotlin 1.5.0 以下的版本的同学在引入第三方 Kotlin 库的时候,有可能会遇到这样的问题:
1 | "Module was compiled with an incompatible version of Kotlin. The binary version of its metadata is 1.5.x, expected version is 1.x.y" |
根据我们对 Kotlin Metadata 的了解,便可以推断出 —— Kotlin 在 1.5.0 对 Metadata
进行的修改不能向后兼容,如果遇到这种情况,那就只有两个选择:
- 升级工程中使用的 Kotlin 的版本
- 使用三方库的低版本(前提是三方库有提供用 Kotlin 1.5.0 之前的版本编译的版本)
看到这里,大家是不是觉得 —— 原来 Kotlin 还有这种坑!!!没错,Kotlin 的版本兼容性问题多着呢 😿
- 本文链接:https://johnsonlee.io/2021/10/29/do-you-really-know-kotlin-1/
- 版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。