一個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的模塊化過濾了。幫助業務線搞定了這個奇奇怪怪,花裏胡哨的問題了。

結尾

我我的其實對這些奇奇怪怪疑難雜症仍是頗有興趣的,畢竟當你解決了這種問題所能給你帶來的愉悅感,十分的酸爽,並且會讓人更有成就感。

因此各位大佬,大家還在等待什麼,不想加入嗶哩嗶哩和咱們一塊兒作一些好玩的事情嗎。主站移動端團隊一直在等着大家呢。

相關文章
相關標籤/搜索