資源混淆是如何影響到Kotlin協程的

騰訊音樂技術發表於2022-12-07

導言

隨著kotlin的使用,協程也慢慢在我們工程中被開始被使用起來,但在我們工程中卻遇到了一個問題,經過資源混淆處理之後的apk包,協程卻不如期工作。那麼兩者到底有什麼關聯呢,資源混淆又是如何影響到協程的使用的,透過閱讀本篇你會馬上知曉。

本篇會從如下幾個方面講述這個問題

問題定義->問題分析->問題解決

問題定義

看下面這段demo程式碼:

package com.example.coroutinenotworkdemo

import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.provider.Settings
import android.util.Log
import android.widget.Toast
import android.widget.Toast.LENGTH_SHORT
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlin.system.measureTimeMillis

class MainActivity : AppCompatActivity(), CoroutineScope {
   override val coroutineContext: CoroutineContext
       get() = Job()

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       clickid.setOnClickListener {
           GlobalScope.async {
               Log.i("pisa","start call async")
               val cost=measureTimeMillis {
                   val result=demoSupendFun()
                   Log.i("pisa","get result=$result")
                   //下面經過資源混淆之後,withContext裡面的塊沒得到執行。。
                   withContext(Dispatchers.Main){
                       textview.text=result
                   }
               }
               Log.i("pisa","cost=$cost")
               0
           }
           Toast.makeText(this,"click result",LENGTH_SHORT)
       }

   }

   suspend fun demoSupendFun(): String {
       return suspendCoroutine {
           //模擬一個非同步請求,然後回撥,得到結果
           async {
               delay(1000)
               it.resume("get result")
           }
       }
   }
}

我們發現經過資源混淆之後,下面這段程式碼中,textview.text=result始終沒有得到執行。

withContext(Dispatchers.Main){
   textview.text=result
}

那麼這是為什麼呢?

問題分析

既然跟資源混淆有關,那麼我們看看經過資源混淆之後的apk和之前的apk到底又哪些改變。
資源混淆用的是之前微信開源的的andResguard,簡單來說,資源混淆包括如下幾個步驟:

  1. 解壓縮apk

  2. 混淆演算法開始混淆res檔案,並改下resources.arsc檔案

  3. 用7zip重壓縮apk,重簽名

看起來,1和2對於影響到協程使用可能性很低,那麼3呢,在對比前後apk過程中我們馬上發現混淆前後的apk的METF-INF檔案相差比較大,混淆後只保留了SF,MF,RSA檔案,而混淆前的apk的METF-INF檔案中包含了一些kotlin_module資訊以及services資料夾,那麼會不會和這些檔案的丟失有關呢。

資源混淆是如何影響到Kotlin協程的

怎麼驗證呢。很簡單,gradle裡面配置packageOptions主動移除META-INF資料夾下的kotlin_module檔案和services資料夾,然後debug除錯一下發現問題復現。那麼肯定和這裡有關啦。

現在先不急著馬上解決它,讓我們看看為啥這幾個檔案的丟失就會導致上面那段協程程式碼工作不正常呢。既然有demo,那我們單步除錯進去看看吧。

上面例子中呼叫了async函式,透過原始碼可以知道,如果start引數是用的預設的情況下,那麼最後都會走到startCoroutineCancellable函式,而這個函式內部會呼叫runSafely,內部所有的異常都會被這個函式catch住,所以業務層沒拋crash,直接把這個問題隱藏了,也給快速定位問題加大了難度。

資源混淆是如何影響到Kotlin協程的

既然用demo復現了這個問題,那麼單步除錯一下,看看withContext裡面到底掛在了哪裡?最終除錯發現,果然這裡runSafely裡面catch住了一個exception,異常資訊如下:
Module with the Main dispatcher is missing.Add dependency providing the Main dispatcher, e.g. 'kotlinx-coroutines-android
所以上面withContext裡面的程式碼就沒有執行到了。

那麼這裡的MainDispatcher是什麼呢?原來是在呼叫withContext來切換執行緒的時候,會用到類MainCoroutineDispatcher。這個類是個抽象類,會經過MainDispatcherFactory工廠來建立具體的dispatcher,在Android上是AndroidDispatcherFactory來負責建立,MainDispatcherFactory這個類是透過自定義的ServiceLoader載入進來的,在kotlin中定義了一個FastServiceLoader,這個類與java的ServiceLoader最大的區別是跳過了jar的校驗,可以直接從jar包中載入某一個類的資訊,如果用常規的ServiceLoader是需要讀取整個jar包之後,在定位到對應的class檔案資訊,載入進來,這整個過程是一個非常耗時的操作,可能導致android裝置發生ANR的現象。

看看FastServiceLoader是如何載入AndroidDispatcherFactory的,如下圖所示:

資源混淆是如何影響到Kotlin協程的

看到這個類瞬間明白了,kotlin在編譯的時候,會在META-INF資料夾下生成一個services的資料夾資訊,該資料夾下面放一些支援類的資訊,那麼具體在放了哪些類呢,在原始碼當中有一個pro檔案可以說明一切。

資源混淆是如何影響到Kotlin協程的

這樣在呼叫相關類的時候會優先先用FastServiceLoader載入該類。一旦載入不到,就會構造一個MissingMainCoroutineDispatcher,並呼叫missing方法丟擲異常。

資源混淆是如何影響到Kotlin協程的

問題解決

經過上述問題分析之後,其實解決方案就非常簡單了。修改資源混淆重打包的流程,在重簽名的時候保留META-INF的servcies資料夾資訊即可

回顧總結

再來回顧一下問題的解決過程,雖然最終解決的方案比較簡單,但有兩個點需要我們特別關注一下

  1. 協程當中async內部有try catch機制,所以任何異常都會被內部catch住,而這個在我們開發當中很容易導致一些問題沒有及時發現

  2. 在遇到一些奇怪的問題的時候,小而簡單的demo外加原始碼閱讀是必要的,這樣方便我們快速能夠追查到問題原因所在。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31557897/viewspace-2661326/,如需轉載,請註明出處,否則將追究法律責任。

相關文章