前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一个Java9特性导致的编译失败 | 疑难杂症

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

作者头像
逮虾户
发布2022-03-06 09:51:30
1.1K0
发布2022-03-06 09:51:30
举报
文章被收录于专栏:逮虾户逮虾户

theme: smartblue

背景

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

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

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

盘下这个问题

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

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

异常日志

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

代码语言:javascript
复制
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信息相关的,而且类版本貌似也略微有点小高啊。

代码语言:javascript
复制
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

代码语言:javascript
复制
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
image.png

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

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

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

解决方案

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

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

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

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

image.png
image.png

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

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

image.png
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文件都进行处理,毕竟谁也无法保证真的有人在安卓工程下面也定义了这个。

代码语言:javascript
复制
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涉及到拆包之后重新组包的逻辑,虽然其实也不复杂,但是各位还是要注意这部分。

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

结尾

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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2021/07/25 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • theme: smartblue
  • 背景
  • 盘下这个问题
    • 异常日志
      • module-info.class
        • 继续排查
          • 解决方案
            • 优化下BaseTransform
            • 结尾
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档