文前打个小广告,简单介绍下咱们是哔哩哔哩主站移动端团队,当前须要双端移动端大佬和小佬。iOS/Android同窗1-N年的同窗都要,上海北京都有职位。有兴趣的欢迎来撩我,微信15801995859.html
哎,上周又被坑了啊。最近某个子app升级了一下基础组件的版本,也就是在下负责的支付sdk,而后忽然发现打release包挂掉了。根据gradle
错误堆栈,发现是dexBuilderRelease
这个task挂了。以后联系到了我,让我帮忙一块儿看下。java
从堆栈日志一看就知道又是一个蛋疼的问题咯,由于以前也有读者大佬问我如何去定位这种问题哦,今天就给你们盘一下这个大菜。android
此次问题的排查过程比较复杂,总体解决这个编译问题用了大概一天时间,中间几个Task也问了几个大佬的意见,大部分的思路其实都是几个大佬给的,因此我也就只是当了个工具人而已。github
如下我对异常日志进行了筛选,总体会比大家想的还要在长一点。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.txt
。app
a.class -> module-info.class
复制代码
咦,这个文件有点奇奇怪怪的啊,貌似之前历来没有见过这种东西呀。以后咱们也对这个类进行了javap
操做,发现的确是有点不符合常规咱们对一个类的定义。
module-info.java不是类,不是接口,是一些模块描述信息。module也不是关键字。 java9新增的模块信息
因此明明安卓当前最多只能支持到java8,那么哪里来的java9的新特性呢?并且为何会致使这么奇奇怪怪的问题吗?
module-info的描述上来看,这并非一个必定须要的东西,他是一个对外部输出的描述信息,告诉你当前jar的一些模块化信息而已,因此若是使用低版原本进行编译,特别是安卓这种,就必然会出现这个奇怪的问题。
可是由于安卓不少和java的共性,因此就会致使安卓会用到不少java原生的类库,因此若是当java和安卓的公用库逐渐升级,后续这种问题仍是会注意暴露出来的。
当咱们找到了犯罪分子以后,咱们最好就是能找出是谁引入了这个仓库,最简单的方式就是按两下shift
,以后用idea提供的查找当时去找到这个类,可是此次也不知道为啥,我就是没找到。
那么只能从产物层面去寻找了。由于项目开启了代码压缩,若是是分立开的一个个jar包是没有办法查出哪些类没有被实际引用到的,因此FilterShrinkerRulesTransform
这个就会对产物进行一次聚合。入下图所示。
由于这个时候产物已经只有一个jar了,因此更加加大了咱们去追踪凶手的难度。
这里展开下,我去问了下咱们另一个不肯意透露姓名,可是牛逼到离谱的字节码大佬,哔哩哔哩以前其实已经解决过这个问题了。此次出现的是另一个子业务。
另外就是由于这个工程是没有Transform
的字节码操做的,因此这个时候想要去追溯这个问题,我感受就要写个Transform
了,并且估计可能也要加输出语句了。
这个时候咱们其实有两个方案能够去解决这个问题哦。
module-info
的第三方,而后把他下降到好的那个版本。Transform
,主动的把这些无效的class文件过滤掉。其实一开始我只打算走第一步的,可是上面也说了开启了shrink代码压缩,并且因为这个工程没有任何Transform
因此咱们去找产物也变得困难。
我在1的路上也跟踪了好久,我找到了两个很奇怪的库。
可是发现实际由于依赖关系,因此也没有办法有效的剔除他们,最后仍是走上了2的不归路啊。
顺便说下此次问题的元凶,找到他也是经过在Transform
中把module-info
的输入路径打出来才真实获取到的。
由于是Gson
,做为一个java共用的工具,因此拥有java9的特性我也是能够理解的。貌似在2.8.6版本以后就都会有,若是有出现相似问题的小伙伴们能够先考虑降低级到2.8.5版本上去。
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的模块化过滤了。帮助业务线搞定了这个奇奇怪怪,花里胡哨的问题了。
我我的其实对这些奇奇怪怪疑难杂症仍是颇有兴趣的,毕竟当你解决了这种问题所能给你带来的愉悦感,十分的酸爽,并且会让人更有成就感。
因此各位大佬,大家还在等待什么,不想加入哔哩哔哩和咱们一块儿作一些好玩的事情吗。主站移动端团队一直在等着大家呢。