概要
本文主要为大家介绍 Kotlin 反射的一些原理,并提示大家如果需要在使用 Kotlin 反射的工程中进行混淆,千万注意,对所有反射涉及的类和接口的父类都需要 Keep。
问题的产生
话说我曾经遇到过一个问题,这个问题可以用下面的代码复现:
代码语言:javascript复制open class SuperClass
class SubClass: SuperClass(){
companion object {
fun seeErrorAfterProguard(){
try {
SubClass::class.supertypes.forEach {
debug(it)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
如果混淆的时候没有 keep 这两个类,那么使用 Kotlin 反射时会抛一个无法找到 SuperClass
这个类的异常。
可能有些朋友可能还没有反应过来这是什么意思,如果我们使用 Java 反射的话,哪怕 SuperClass
这个类已经被混淆成了 a
,那么使用 SubClass
获取它的父类也应该是去找 a
这个类,而不会仍然回去找什么 SuperClass
,毕竟这个类信息已经在混淆的时候被改了呀,运行时的 Kotlin 反射又是怎么知道它的存在的呢?
Kotlin 反射的小伎俩
Kotlin 反射能够拿到 Java 反射拿不到的很多东西,这一点毋庸置疑,毕竟 Kotlin 的语法特性更复杂,需要的信息也更多。可是,既然最后大家都编译成了字节码,如果字节码里面原来就有这些信息,Java 反射无论如何也不应该金屋藏娇,捂着不给人看吧——Kotlin 反射一定是另辟蹊径的,不然它又怎么会突破字节码的限制呢?
那么它是怎么做到的呢?当然是这个:
代码语言:javascript复制@Metadata(
mv = {1, 1, 9},
bv = {1, 0, 2},
k = 1,
d1 = {"..."},
d2 = {"Lcom/bennyhuo/kotlinspecifics/reflect/SubClass;", "Lcom/bennyhuo/kotlinspecifics/reflect/SuperClass;", "()V", "Companion", "production sources for module app"}
)
public final class SubClass extends SuperClass {
...
}
上面给出的是 SubClass
编译后的字节码反编译得到的 Java 代码,我们看到 Metadata
这个注解当中包含了很多信息,其中 d2
当中就包含了这个类所有的父类信息。
这时候想必各位同仁已然猜到了,其实 Kotlin 反射获取信息的过程就是一个注解读取的过程。
混淆有何罪过
既然知道了 Kotlin 反射的原理,那么我们来想想混淆,混淆对于类的处理,对于通过字面量反射操作类和对象来说是致命的,因为字面量不会作为混淆的对象。同样的,前面的注解的值也不会。也真是这个原因,混淆后 SuperClass
被混淆为了 a
, SubClass
的注解中存的仍然是 SuperClass
。
没错,混淆之后出现找不到类的问题是合乎情理的。只要是通过字面量反射操作类和对象,无论是直接还是间接,都需要注意混淆的问题。
如果大家不信,大家也可以试一下属性引用或者函数引用,看看他们在被混淆之后有什么情况发生。
那么这个应该是混淆的锅咯?当然不是,Kotlin 反射毕竟有点儿曲线救国的意思,混淆也只能长叹一口气,“你们这。。让我防不胜防啊”。
有解决方案吗?
有啊,凡涉及到使用 Kotlin 反射的类及其父类(接口)、属性、函数等,均要小心这个问题,如果出现无法找到类或者属性、函数的情形,记得 Keep 它们。
最后再说一句
解析注解这种操作。。。额,难怪它慢。。