阅读 1141

一个Java9特性导致的编译失败 | 疑难杂症

背景

哎,上周又被坑了啊。最近某个子app升级了一下基础组件的版本,也就是在下负责的支付sdk,然后突然发现打release包挂掉了。根据gradle错误堆栈,发现是dexBuilderRelease这个task挂了。之后联系到了我,让我帮忙一起看下。

从堆栈日志一看就知道又是一个蛋疼的问题咯,因为之前也有读者大佬问我如何去定位这种问题哦,今天就给大家盘一下这个大菜。

当前的解决方案已经放在我的github上了,还是AndroidAutoTrack

盘下这个问题

这次问题的排查过程比较复杂,整体解决这个编译问题用了大概一天时间,中间几个Task也问了几个大佬的意见,大部分的思路其实都是几个大佬给的,所以我也就只是当了个工具人而已。

  1. dexBuilderRelease 报错了,报错内容为类信息异常。
  2. 开了了代码混淆,所以导致要根据mapping文件追述混淆前的类。
  3. 开启了代码压缩(shrink),所以jar和class被合并成了一个jar。
  4. 没有transform,导致有点难定位到是哪个jar输入的异常类。

异常日志

以下我对异常日志进行了筛选,整体会比你们想的还要在长一点。

Caused by: com.android.tools.r8.CompilationFailedException: Compilation failed to complete, origin: /Users/zhangyang/missevan-android/app/build/intermediates/shrunk_jar/release/minified.jar:a.class
.......
Suppressed: java.lang.RuntimeException: java.util.concurrent.ExecutionException: com.android.tools.r8.errors.CompilationError: Illegal class file: Class a is missing a super type. Class file version 53.
  at com.android.tools.r8.utils.ExceptionUtils.unwrapExecutionException(ExceptionUtils.java:195)
  at com.android.tools.r8.dex.ApplicationReader.read(ApplicationReader.java:168)
  ... 45 more
Caused by: java.util.concurrent.ExecutionException: com.android.tools.r8.errors.CompilationError: Illegal class file: Class a is missing a super type. Class file version 53.
  at com.google.common.util.concurrent.AbstractFuture.getDoneValue(AbstractFuture.java:552)
  at com.google.common.util.concurrent.AbstractFuture.get(AbstractFuture.java:513)
  at com.google.common.util.concurrent.FluentFuture$TrustedFuture.get(FluentFuture.java:86)
  at com.android.tools.r8.utils.ThreadUtils.awaitFutures(ThreadUtils.java:114)
  at com.android.tools.r8.dex.ApplicationReader.read(ApplicationReader.java:159)
  ... 45 more
复制代码

从这一部分堆栈,其实我们可以分析出是因为一个字节码信息异常,简单的说就是一个类缺少了super type信息相关的,而且类版本貌似也略微有点小高啊。

buildTypes {
    release {
        minifyEnabled true
        shrinkResources true
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        signingConfig signingConfigs.findByName('release') ?: signingConfigs.debug
        debuggable false
        proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    }
}
复制代码

assembleRelease这个任务,我们开启了R8编译,同时我们也加入了混淆和代码压缩,也就是上面的配置信息。

所以在dexbuilder构建的时候其实已经完成了混淆了。所以我们要从mapping中去找到这个类混淆前产物。之后我们才能根据这个类文件产物去盘他。

而且这个类名也比较骚哦,他竟然叫a.class。之后我们翻查了下mapping.txt

a.class -> module-info.class
复制代码

咦,这个文件有点奇奇怪怪的啊,貌似以前从来没有见过这种东西呀。之后我们也对这个类进行了javap操作,发现的确是有点不符合常规我们对一个类的定义。

module-info.class

官方对于module info的描述

module-info.java不是类,不是接口,是一些模块描述信息。module也不是关键字。 java9新增的模块信息

所以明明安卓当前最多只能支持到java8,那么哪里来的java9的新特性呢?而且为什么会导致这么奇奇怪怪的问题吗?

module-info的描述上来看,这并不是一个一定需要的东西,他是一个对外部输出的描述信息,告诉你当前jar的一些模块化信息而已,所以如果使用低版本来进行编译,特别是安卓这种,就必然会出现这个奇怪的问题。

但是因为安卓很多和java的共性,所以就会导致安卓会用到很多java原生的类库,所以如果当java和安卓的公用库逐渐升级,后续这种问题还是会注意暴露出来的。

继续排查

当我们找到了犯罪分子之后,我们最好就是能找出是谁引入了这个仓库,最简单的方式就是按两下shift,之后用idea提供的查找当时去找到这个类,但是这次也不知道为啥,我就是没找到。

那么只能从产物层面去寻找了。因为项目开启了代码压缩,如果是分立开的一个个jar包是没有办法查出哪些类没有被实际引用到的,所以FilterShrinkerRulesTransform这个就会对产物进行一次聚合。入下图所示。

image.png

因为这个时候产物已经只有一个jar了,所以更加加大了我们去追踪凶手的难度。

这里展开下,我去问了下我们另外一个不愿意透露姓名,但是牛逼到离谱的字节码大佬,哔哩哔哩之前其实已经解决过这个问题了。这次出现的是另外一个子业务。

另外就是因为这个工程是没有Transform的字节码操作的,所以这个时候想要去追溯这个问题,我感觉就要写个Transform了,而且估计可能也要加输出语句了。

解决方案

这个时候我们其实有两个方案可以去解决这个问题哦。

  1. 找到这个带有module-info的第三方,然后把他降低到好的那个版本。
  2. 通过字节码大佬说的写个Transform,主动的把这些无效的class文件过滤掉。

其实一开始我只打算走第一步的,但是上面也说了开启了shrink代码压缩,而且由于这个工程没有任何Transform所以我们去找产物也变得困难。

我在1的路上也跟踪了很久,我找到了两个很奇怪的库。

image.png

但是发现实际因为依赖关系,所以也没有办法有效的剔除他们,最后还是走上了2的不归路啊。

顺便说下这次问题的元凶,找到他也是通过在Transform中把module-info的输入路径打出来才真实获取到的。

image.png

因为是Gson,作为一个java共用的工具,所以拥有java9的特性我也是可以理解的。貌似在2.8.6版本之后就都会有,如果有出现类似问题的小伙伴们可以先考虑下降级到2.8.5版本上去。

优化下BaseTransform

BaseTransform 是我对Transform流程做的一个简单的抽象,有兴趣的可以看下我的github项目 AndroidAutoTrack

因为这个问题哦,所以我在BaseTransform上做了些小调整优化。我对module-info.class的类进行过滤,因为前文介绍过着是java9模块化使用的,也就是说在低版本上有没有这个类,其实完全没有用,他并不会实际被使用到。

tips 小贴士,这里有个极端情况就是在META-INF文件夹下的moudle-info是不能被删除的。

所以我们只要在class扫描阶段对这些高版本特性的进行一次过滤就可以了。比较特殊的地方就是我们要对jar包和class文件都进行处理,毕竟谁也无法保证真的有人在安卓工程下面也定义了这个。

fun copyIfLegal(srcFile: File?, destFile: File) {
    if (srcFile?.name?.contains("module-info") != true) {
        try {
            srcFile?.apply {
                org.apache.commons.io.FileUtils.copyFile(srcFile, destFile)
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    } else {
        Log.info("copyIfLegal module-info:" + srcFile.name)
    }
}
复制代码

这部分比较简单,只要判断下当前文件名是否包含module-info,有就不进行文件copy操作,没有则就继续文件拷贝。

剩下的就是对jar包内的处理逻辑了,因为jar涉及到拆包之后重新组包的逻辑,虽然其实也不复杂,但是各位还是要注意这部分。

fun modifyJarFile(jarFile: File, tempDir: File?, transform: BaseTransform): File {
        /** 设置输出到的jar  */
        val hexName = DigestUtils.md5Hex(jarFile.absolutePath).substring(0, 8)
        val optJar = File(tempDir, hexName + jarFile.name)
        val jarOutputStream = JarOutputStream(FileOutputStream(optJar))
        jarOutputStream.use {
            val file = JarFile(jarFile)
            val enumeration = file.entries()
            enumeration.iterator().forEach { jarEntry ->
                val inputStream = file.getInputStream(jarEntry)
                val entryName = jarEntry.name
                if (entryName.contains("module-info.class") && !entryName.contains("META-INF")) {
                    Log.info("jar file module-info:$entryName jarFileName:${jarFile.path}")
                } else {
                    val zipEntry = ZipEntry(entryName)
                    jarOutputStream.putNextEntry(zipEntry)
                    var modifiedClassBytes: ByteArray? = null
                    val sourceClassBytes = IOUtils.toByteArray(inputStream)
                    if (entryName.endsWith(".class")) {
                        try {
                            modifiedClassBytes = transform.process(entryName, sourceClassBytes)
                        } catch (ignored: Exception) {
                        }
                    }
                    /**
                     * 读取原jar
                     */
                    if (modifiedClassBytes == null) {
                        jarOutputStream.write(sourceClassBytes)
                    } else {
                        jarOutputStream.write(modifiedClassBytes)
                    }
                    jarOutputStream.closeEntry()
                }
            }
        }
        return optJar
    }
复制代码

上面是BaseTransform内的jar扫描逻辑,当前的操作比较简单,如果发现文件名是module-info,则在生成新的jar的时候对这个文件进行跳过操作,就这么点。

基本上这样我们就可以完成对java9的模块化过滤了。帮助业务线搞定了这个奇奇怪怪,花里胡哨的问题了。

结尾

我个人其实对这些奇奇怪怪疑难杂症还是很有兴趣的,毕竟当你解决了这种问题所能给你带来的愉悦感,十分的酸爽,而且会让人更有成就感。

所以各位大佬,你们还在等待什么,不想加入哔哩哔哩和我们一起做一些好玩的事情吗。主站移动端团队一直在等着你们呢。

文章分类
Android
文章标签