前言
戏接上文,kotlin
升级没想到啊还有一个大坑。我们之前说了我们使用的agp版本是7.0.3
,在这个版本的R8
竟然会出现kotlin
混淆的bug。
断更一个月,不更文的一个原因就是因为最近感觉太菜了,并没有文章素材了。
问题排查
接下来还是一点点进行问题分析,我们先从kotlin
元数据开始讲这个问题。
元数据
大家可以参考下官方的这篇文章R8 编译器: 为 Kotlin 库和应用 "瘦身"。
kotlin
中的一部分类信息都会生成在Metadata注解中,(Metadata
就是kotlin元数据)。另外工程内有一部分代码使用了kotlin-reflect
的能力,而kotlin-reflect
很多能力都是通过读取元数据来完成的。
Kotlin 元数据 是存储在 Java 类文件的注解中的一些额外信息,它由 Kotlin JVM 编译器生成。元数据确定了类文件中的类和方法是由哪些 Kotlin 代码构成的。比如,Kotlin 元数据可以告诉 Kotlin 编译器类文件中的一个方法实际上是 Kotlin 扩展函数。
这个是我通过jadx反编译出来的一个类的信息,我们可以发现很多关键信息都存放在元数据中。其中如果元数据丢失了可能就会影响到的就是一些kotlin和java的互相调用,还有就是一些kotlin-reflect
的调用。
但是我们在release
混淆包中,这部分kotlin 1.7.10
生成出来的元数据竟然被R8
代码优化掉了,导致了release
包的部分功能异常。
Gradle中的类加载机制
这里要展开这个可能会比较突兀哦,但是其实大家可以继续向下看下去就知道了。
JVM类加载机制、双亲委派和SPI机制
面试中我们经常被问到的一个问题就是类的生命周期,以前的时候我对于这个东西是没有什么概念的,因为毕竟没有什么实际的应用场景,但是这里雀食是有的。
上图就是类的生命周期了,类加载机制有个特性,如果当前的ClassLoader
内已经加载过这个类则后续就会使用这个类去完成构造,当然如果不存在则会去挂载jar
,然后从jar
中去构造出。当然我们一般在写安卓的时候很少会出现加载两个不同版本的jar
的情况,但是这个在Gradle
编译中是被允许的,所以先后加载jar
的顺序就决定了我们会使用哪个版本的jar
。
我们之前就写过一个很意思的bug,我们在Settings
插件内先加载了低版本的AGP
,之后我们即时在build.gradle
内定义了高版本的AGP
,因为类加载机制的原因,也会把AGP
锁定在一个低版本上,因为这个jar
已经被ClassLoader
优先加载了。
单独升级R8
接下来我们就需要偷偷的使用上面的方法,跳过AGP 7.0.3
中低版本的R8
,直接使用高版本AGP 7.2.1
的R8
就能修复这个异常了。
正常情况下我们都是在build.gradle
内的buildscript
去定义AGP
版本的。这次我们只需要把这个R8
的版本放到settings.gralde
中就可以解决这个问题了。
buildscript{
dependencies {
classpath("com.android.tools:r8:3.2.60")
classpath('com.google.guava:guava:30.1.1-jre')
}
}
当然大部分情况下其实我是不建议使用这种黑魔法的,因为经常会出现方法签名等等匹配不上的情况。而R8
因为了其中有中间层的特殊性,所以可以比较容易的被替换成另外一个版本。
总结
全TM是坑啊,其实还有好几个问题我都没说。只能说世事无常大肠包小肠。
另外因为我们有一部分方法签名检查的a8
就是基于r8
开发的,所以后面就可能还有一篇吧。
我打算后续吹嘘下Gradle Enterprise
,试用阶段发现真的还是挺好用的。
参考文献
Data class metadata is removed with proguard / R8 for Kotlin 1.6.0
R8 编译器: 为 Kotlin 库和应用 "瘦身"