kotlin 和 r8 的量子纠缠 | 类加载机制偷鸡

4,133 阅读4分钟

前言

戏接上文,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 扩展函数。

image.png

这个是我通过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.1R8就能修复这个异常了。

正常情况下我们都是在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 库和应用 "瘦身"