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

常规招人

文前打个小广告,简单介绍下咱们是哔哩哔哩主站移动端团队,当前须要双端移动端大佬和小佬。iOS/Android同窗1-N年的同窗都要,上海北京都有职位。有兴趣的欢迎来撩我,微信15801995859.html

背景

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

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

当前的解决方案已经放在个人github上了,仍是AndroidAutoTrackgit

盘下这个问题

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

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

异常日志

如下我对异常日志进行了筛选,总体会比大家想的还要在长一点。apache

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编译,同时咱们也加入了混淆和代码压缩,也就是上面的配置信息。markdown

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

并且这个类名也比较骚哦,他居然叫a.class。以后咱们翻查了下mapping.txtapp

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的模块化过滤了。帮助业务线搞定了这个奇奇怪怪,花里胡哨的问题了。

结尾

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

因此各位大佬,大家还在等待什么,不想加入哔哩哔哩和咱们一块儿作一些好玩的事情吗。主站移动端团队一直在等着大家呢。

相关文章
相关标签/搜索