崩潰堆棧
首先,崩潰上報的堆棧:
09-22 15:41:59.245 9040 9040 V ActivityThread: callActivityOnCreate
09-22 15:41:59.311 9040 9040 E AndroidRuntime: FATAL EXCEPTION: main
09-22 15:41:59.311 9040 9040 E AndroidRuntime: Process: com.XXXX.android, PID: 9040
09-22 15:41:59.311 9040 9040 E AndroidRuntime: android.util.SuperNotCalledException: Activity {com..XXXX.android/com..XXXX.activity.XXXXActivity} did not call through to super.onCreate()
09-22 15:41:59.311 9040 9040 E AndroidRuntime: at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3875)
09-22 15:41:59.311 9040 9040 E AndroidRuntime: at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:4081)
09-22 15:41:59.311 9040 9040 E AndroidRuntime: at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:91)
09-22 15:41:59.311 9040 9040 E AndroidRuntime: at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:149)
09-22 15:41:59.311 9040 9040 E AndroidRuntime: at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:103)
09-22 15:41:59.311 9040 9040 E AndroidRuntime: at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2462)
09-22 15:41:59.311 9040 9040 E AndroidRuntime: at android.os.Handler.dispatchMessage(Handler.java:110)
09-22 15:41:59.311 9040 9040 E AndroidRuntime: at android.os.Looper.loop(Looper.java:219)
09-22 15:41:59.311 9040 9040 E AndroidRuntime: at android.app.ActivityThread.main(ActivityThread.java:8393)
09-22 15:41:59.311 9040 9040 E AndroidRuntime: at java.lang.reflect.Method.invoke(Native Method)
09-22 15:41:59.311 9040 9040 E AndroidRuntime: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513)
09-22 15:41:59.311 9040 9040 E AndroidRuntime: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1055)
這是我們app升級androidx之后,第一次外灰時發(fā)現(xiàn)的線上問題。來自線上的偶現(xiàn)bug,主要分布在10的機(jī)器以及少部分9的機(jī)器上。收集到的crash堆棧,都是顯示的super.onCreate()未被調(diào)用,而且是很多個不同的activity都在上報,那基本確認(rèn)是base類的問題。
但是在我們封裝的BaseActivity類里都是加了tryCatch邏輯,不會因為業(yè)務(wù)代碼崩了就不去調(diào)用super.onCreate().
后來隨著繼續(xù)翻找console log,我們發(fā)現(xiàn)這個崩潰由一個BadParcelableException引起,說白了是android內(nèi)部的onCreate()方法crash后沒有走完:
09-22 12:47:36.940 E/Parcel (31316): Class not found when unmarshalling: androidx.fragment.app.FragmentManagerState
09-22 12:47:36.940 E/Parcel (31316): java.lang.ClassNotFoundException: androidx.fragment.app.FragmentManagerState
09-22 12:47:36.940 E/Parcel (31316): at java.lang.Class.classForName(Native Method)
09-22 12:47:36.940 E/Parcel (31316): at java.lang.Class.forName(Class.java:454)
09-22 12:47:36.940 E/Parcel (31316): at android.os.Parcel.readParcelableCreator(Parcel.java:3028)
09-22 12:47:36.940 E/Parcel (31316): at android.os.Parcel.readParcelable(Parcel.java:2978)
09-22 12:47:36.940 E/Parcel (31316): at android.os.Parcel.readValue(Parcel.java:2880)
09-22 12:47:36.940 E/Parcel (31316): at android.os.Parcel.readArrayMapInternal(Parcel.java:3258)
09-22 12:47:36.940 E/Parcel (31316): at android.os.BaseBundle.initializeFromParcelLocked(BaseBundle.java:297)
09-22 12:47:36.940 E/Parcel (31316): at android.os.BaseBundle.unparcel(BaseBundle.java:241)
09-22 12:47:36.940 E/Parcel (31316): at android.os.Bundle.getParcelable(Bundle.java:951)
09-22 12:47:36.940 E/Parcel (31316): at androidx.fragment.app.FragmentActivity$2.onContextAvailable(FragmentActivity.java:148)
09-22 12:47:36.940 E/Parcel (31316): at androidx.activity.contextaware.ContextAwareHelper.dispatchOnContextAvailable(ContextAwareHelper.java:99)
09-22 12:47:36.940 E/Parcel (31316): at androidx.activity.ComponentActivity.onCreate(ComponentActivity.java:322)
09-22 12:47:36.940 E/Parcel (31316): at androidx.fragment.app.FragmentActivity.onCreate(FragmentActivity.java:273)
09-22 12:47:36.940 E/Parcel (31316): at com..XXXX.android.commonui.component.BaseActivity.onCreate(BaseActivity.java:93)
09-22 12:47:36.940 E/Parcel (31316): at com..XXXX.android.app.ui.filmdetail.FilmDetailActivity.onCreate(FilmDetailActivity.java:50)
09-22 12:47:36.940 E/Parcel (31316): at android.app.Activity.performCreate(Activity.java:7872)
09-22 12:47:36.940 E/Parcel (31316): at android.app.Activity.performCreate(Activity.java:7861)
09-22 12:47:36.940 E/Parcel (31316): at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1353)
09-22 12:47:36.940 E/Parcel (31316): at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3500)
09-22 12:47:36.940 E/Parcel (31316): at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3664)
09-22 12:47:36.940 E/Parcel (31316): at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:83)
09-22 12:47:36.940 E/Parcel (31316): at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
09-22 12:47:36.940 E/Parcel (31316): at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
09-22 12:47:36.940 E/Parcel (31316): at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2246)
09-22 12:47:36.940 E/Parcel (31316): at android.os.Handler.dispatchMessage(Handler.java:107)
09-22 12:47:36.940 E/Parcel (31316): at android.os.Looper.loop(Looper.java:230)
09-22 12:47:36.940 E/Parcel (31316): at android.app.ActivityThread.main(ActivityThread.java:7768)
09-22 12:47:36.940 E/Parcel (31316): at java.lang.reflect.Method.invoke(Native Method)
09-22 12:47:36.940 E/Parcel (31316): at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:508)
09-22 12:47:36.940 E/Parcel (31316): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1034)
09-22 12:47:36.940 E/Parcel (31316): Caused by: java.lang.ClassNotFoundException: androidx.fragment.app.FragmentManagerState
09-22 12:47:36.940 E/Parcel (31316): at java.lang.Class.classForName(Native Method)
09-22 12:47:36.940 E/Parcel (31316): at java.lang.BootClassLoader.findClass(ClassLoader.java:1358)
09-22 12:47:36.940 E/Parcel (31316): at java.lang.BootClassLoader.loadClass(ClassLoader.java:1418)
09-22 12:47:36.940 E/Parcel (31316): at java.lang.ClassLoader.loadClass(ClassLoader.java:312)
09-22 12:47:36.940 E/Parcel (31316): ... 30 more
09-22 12:47:36.940 E/Parcel (31316): Caused by: java.lang.NoClassDefFoundError: Class not found using the boot class loader; no stack trace available
他山之石
這些都是網(wǎng)上搜到的問題及解決方案:
- 錯誤處理Class not found when unmarshalling: android.support.v4.app.FragmentManagerState
- 異常處理-android.os.BadParcelableException: ClassNotFoundException when unmarshalling
-
ClassNotFoundException when unmarshalling: androidx.fragment.app.FragmentManagerState 問題分析
綜合這些問題,和我們收集的錯誤信息還是能對的上,所以我們加上了一個FragmentStateFixer來修復(fù)這個問題,調(diào)用時機(jī)在onCreate()中走到super.onCreate()之前。
object FragmentStateFixer {
private val modelListStr = "all" //這里可以搞成可配置機(jī)型
fun fixState(context: Context, bundle: Bundle?) {
if (bundle != null && checkCondition(context)) {
bundle.classLoader = context.javaClass.classLoader
}
}
private fun checkCondition(context: Context): Boolean {
if (Build.VERSION.SDK_INT != 29 && Build.VERSION.SDK_INT != 28) {
// 只對9和10的機(jī)器進(jìn)行修正
return false
}
if (modelListStr == "all") {
return true
}
val modelList = modelListStr.split(",")
return modelList.contains(Build.MODEL)
}
}
但是帶著這個改動,再次灰度的時候發(fā)現(xiàn)問題并沒有解決,還是報同樣的錯誤。后面就進(jìn)入了漫長的彎路,直到找到了復(fù)現(xiàn)機(jī)器的全日志以及路徑。
復(fù)現(xiàn)路徑
找到復(fù)現(xiàn)路徑這一點很關(guān)鍵,這個bug一開始來自于平臺上報,不知道如何復(fù)現(xiàn)。查了網(wǎng)上的文章之后,大概猜測和重建有關(guān),但是使用開發(fā)者選項中的“不保存活動”,也不能復(fù)現(xiàn)這個crash。
只能通過找很多臺9或者10的手機(jī),跑monkey test來進(jìn)行穩(wěn)定性測試,來復(fù)現(xiàn)該問題。這就導(dǎo)致驗證成本極高,而且沒有現(xiàn)場信息,只有上報上來的一大堆log,調(diào)查的難度很高。
最后是在測試同學(xué)協(xié)助下,發(fā)現(xiàn)一臺測試機(jī)上的崩潰都是由一個二級或者三級頁面中的crash,導(dǎo)致前面幾個頁面會報這個錯誤。由此想到這種crash導(dǎo)致的頁面重建可能正是一種符合的場景,因此在一個層級比較深的頁面手動觸發(fā)一個崩潰來造成類似的場景,果然復(fù)現(xiàn)了這個crash!
找到復(fù)現(xiàn)路徑,就可以壓縮驗證的時間成本,而且還可以debug(需要打開開發(fā)者選項中的"等待調(diào)試器",然后在觸發(fā)崩潰后進(jìn)入debug),因為是android源碼的崩潰,很多地方加不了log,可以debug之后對定位問題非常有幫助。
問題定位
根據(jù)崩潰堆棧,結(jié)合debug信息,找到最終崩潰的位置:

就像網(wǎng)上鏈接所說,這個地方的classloader是BootClassLoader,所以在解析時會跑錯誤。而正確的是什么樣的呢?可以對比一下Android 11的機(jī)器走到這里的信息:

我們可以發(fā)現(xiàn),11走到這里的時候,ClassLoader是PathClassLoader,所以是可以正常解析的,不會崩潰。(正常Android中我們解析類都是用PathClassLoader,這部分ClassLoader的知識可以查看其它文章)
所以通過debug,定位到了位置,的確是ClassLoader在搗鬼,但是這個位置又不是網(wǎng)上鏈接所定位和修改的地方,所以之前的改法是沒有用的。那這個地方的savedInstanceState又是怎么來的呢?和我們在onCreate中拿到的savedInstanceState又是什么關(guān)系呢?
此外,這里我在debug時發(fā)現(xiàn)一個有趣的現(xiàn)象,就是這個savedInstanceState被取出來之后,無論調(diào)用任何方法,都會拋出異常BadParcelableException,而且只要是在debug時調(diào)用某個方法讓它“釋放”了這個異常,ClassLoader就會變成正確的PathClassLoader,后面都可以正確運行。

上圖就是我調(diào)用了一個keySet()方法。這里也會拋出異常應(yīng)該是因為Bundle的很多方法,包括keySet(),get()的第一句都是unparcel(),所以是崩在unparcel()里面。但是為啥這里崩了一次之后,ClassLoader就變了,后面程序就能正常運行,這是我沒能理解的地方。
我們先回過頭看一下,在重建activity時傳給onCreate的savedInstantceState參數(shù)是什么。

可以看到這里面有4個參數(shù),其中有一個android:fragment,但其實你要按照之前文章里的說法去remove或者想給它設(shè)置ClassLoader,也是沒有用的。因為其實并不是用的這里。還得繼續(xù)從堆棧處找。
回到剛剛崩潰處,可以發(fā)現(xiàn)那里用的savedInstanceState是從savedSateRegistry中用consumeRestoreStateForKey取出來的,然后看看這個方法的內(nèi)容:

這里傳入的key是"android:support:fragments",簡單來說是這個叫作mRestoreState的Bundle中取了一個key為"android:support:fragments"的Bundle,而這個Bundle的classloader是BootClassLoader,所以導(dǎo)致了現(xiàn)在的崩潰。那這個mRestoreState是怎么來的呢?接著找源碼,發(fā)現(xiàn)了賦值的地方在另一個方法performRestore()中。

這里使用的這個SAVED_COMPONENTS_KEY,它的值是:
private static final String SAVED_COMPONENTS_KEY =
"androidx.lifecycle.BundlableSavedStateRegistry.key";
而這個performRestore方法正是在FragmentActivity的onCreate()里第一句調(diào)用的

所以代碼追蹤到這里,問題基本就定位到了,其實就是在activity重建時,傳入的savedInstanceState本身ClassLoader是沒問題的,問題出在它的子Bundle的子Bundle上,層級結(jié)構(gòu)如下:
savedInstanceState.getBundle("androidx.lifecycle.BundlableSavedStateRegistry.key").getBundle("android:support:fragment")
問題是出在這個bundle的ClassLoader上,這里在android 10和9的機(jī)器上使用的是BootClassLoader,然后在解parcel的時候就崩潰了。
解決方案
根據(jù)上面的分析結(jié)果,現(xiàn)在只要在我們的FragmentStateFixer上加上倆句話即可:
fun fixState(context: Context, bundle: Bundle?) {
if (bundle != null && checkCondition(context)) {
bundle.classLoader = context.javaClass.classLoader
// 9.22新增:需要修改的是bundle下androidx.lifecycle.BundlableSavedStateRegistry.key中子項的classloader
bundle.getBundle("androidx.lifecycle.BundlableSavedStateRegistry.key")?.let {
it.keySet()?.forEach { key ->
(it.get(key) as? Bundle)?.classLoader = context.javaClass.classLoader
}
}
}
}
這里為了保險起見,首先之前給savedInstanceState本身設(shè)置ClassLoader的代碼我沒有去掉;
并且我將androidx.lifecycle.BundlableSavedStateRegistry.key所對應(yīng)Bundle下的所有子Bundle都設(shè)置了一遍ClassLoader。
總結(jié)
- 這個問題首先是android本身的一個bug,在之前網(wǎng)上的文章里也有提到,在Android 10 的preview版本上(國內(nèi)的手機(jī)部分9的機(jī)器也會有這個問題)這里的ClassLoader使用的不對;
- 網(wǎng)上目前提到的解法并沒有解決我們的問題,不確定是不是androidx不同版本源碼不一樣;
- 因為上傳的log不完整,走了很多彎路,在按照網(wǎng)上方案修改后,一度以為是解決了FragmentManagerState這個問題,追查了很久其它原因。后來是在測試的協(xié)助下,找到了真正的原因和復(fù)現(xiàn)手段,這對最終問題解決非常重要;
- 最后的解決方案保留了網(wǎng)上的改法,并在其基礎(chǔ)上擴(kuò)展,可以參考上面的解決方案段落;