文前打個小廣告,簡單介紹下咱們是嗶哩嗶哩主站移動端團隊,當前須要雙端移動端大佬和小佬。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的模塊化過濾了。幫助業務線搞定了這個奇奇怪怪,花裏胡哨的問題了。
我我的其實對這些奇奇怪怪疑難雜症仍是頗有興趣的,畢竟當你解決了這種問題所能給你帶來的愉悅感,十分的酸爽,並且會讓人更有成就感。
因此各位大佬,大家還在等待什麼,不想加入嗶哩嗶哩和咱們一塊兒作一些好玩的事情嗎。主站移動端團隊一直在等着大家呢。