androidparcelableandroid-r8android-gradle-plugin-8.0

getParcelable() crashes due to null ifTable in AGP 8


I recently updated to AGP 8 and I released an update on to the play store. I am getting reports of some crashes in an area of code that was not changed recently. I am fairly confident this is a crash caused by the AGP 8 upgrade, specifically around R8 fullmode.

The code surrounding the crash is restoring the state of a layout manager.

Restoration code

if (savedInstanceState != null) {
    oldRecyclerLayoutState = savedInstanceState.getParcelableCompat(
        SIS_RECYCLER_LAYOUT_STATE,
    )
}

Save instance code

private lateinit var layoutManager: LinearLayoutManager
...
override fun onSaveInstanceState(outState: Bundle) {
    outState.putParcelable(SIS_RECYCLER_LAYOUT_STATE, layoutManager.onSaveInstanceState())
    super.onSaveInstanceState(outState)

}

The crash report indicates that reading a parcelable is crashing due to the ifTable of a class being null when Class.isAssignableFrom() is called on it. From the AOSP, I can see that an ifTable is a table of interfaces. I assume isAssignableFrom is using the ifTable to determine assignability however it was unable to read it due to the class being null. Unfortunately the error doesn't tell me which class is null.

Full stack trace:

Fatal Exception: java.lang.NullPointerException: Attempt to read from field 'java.lang.Object[] java.lang.Class.ifTable' on a null object reference in method 'boolean java.lang.Class.isAssignableFrom(java.lang.Class)'
       at java.lang.Class.isAssignableFrom(Class.java:579)
       at android.os.Parcel.readParcelableCreatorInternal(Parcel.java:4865)
       at android.os.Parcel.readParcelableInternal(Parcel.java:4778)
       at android.os.Parcel.readValue(Parcel.java:4544)
       at android.os.Parcel.readValue(Parcel.java:4324)
       at android.os.Parcel.-$$Nest$mreadValue()
       at android.os.Parcel$LazyValue.apply(Parcel.java:4422)
       at android.os.Parcel$LazyValue.apply(Parcel.java:4381)
       at android.os.BaseBundle.getValueAt(BaseBundle.java:394)
       at android.os.BaseBundle.getValue(BaseBundle.java:374)
       at android.os.BaseBundle.getValue(BaseBundle.java:357)
       at android.os.BaseBundle.get(BaseBundle.java:696)
       at android.os.Bundle.getParcelable(Bundle.java:947)
       at com.ggstudios.lolcatalyst.util.ext.BundleExtKt.getParcelableCompat(BundleExt.kt:21)
       at com.ggstudios.lolcatalyst.summonerlookup.SummonerProfileFragment.onViewCreated(SummonerProfileFragment.kt:422)
       at androidx.fragment.app.Fragment.performViewCreated(Fragment.java:3137)
       at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:552)
       at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:261)
       at androidx.fragment.app.FragmentStore.moveToExpectedState(FragmentStore.java:113)
       at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1435)
       at androidx.fragment.app.FragmentManager.dispatchStateChange(FragmentManager.java:2979)
       at androidx.fragment.app.FragmentManager.dispatchViewCreated(FragmentManager.java:2890)
       at androidx.fragment.app.Fragment.performViewCreated(Fragment.java:3138)
       at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:552)
       at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:261)
       at androidx.fragment.app.FragmentStore.moveToExpectedState(FragmentStore.java:113)
       at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1435)
       at androidx.fragment.app.FragmentManager.dispatchStateChange(FragmentManager.java:2979)
       at androidx.fragment.app.FragmentManager.dispatchActivityCreated(FragmentManager.java:2897)
       at androidx.fragment.app.FragmentController.dispatchActivityCreated(FragmentController.java:263)
       at androidx.fragment.app.FragmentActivity.onStart(FragmentActivity.java:351)
       at androidx.appcompat.app.AppCompatActivity.onStart(AppCompatActivity.java:251)
       at com.ggstudios.lolcatalyst.activity.abs.BaseActivity.onStart(BaseActivity.kt:90)
       at com.ggstudios.lolcatalyst.activity.MainActivity.onStart(MainActivity.kt)
       at android.app.Instrumentation.callActivityOnStart(Instrumentation.java:1510)
       at android.app.Activity.performStart(Activity.java:8603)
       at android.app.ActivityThread.handleStartActivity(ActivityThread.java:4191)
       at android.app.servertransaction.TransactionExecutor.performLifecycleSequence(TransactionExecutor.java:221)
       at android.app.servertransaction.TransactionExecutor.cycleToPath(TransactionExecutor.java:201)
       at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:173)
       at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97)
       at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2571)
       at android.os.Handler.dispatchMessage(Handler.java:106)
       at android.os.Looper.loopOnce(Looper.java:226)
       at android.os.Looper.loop(Looper.java:313)
       at android.app.ActivityThread.main(ActivityThread.java:8741)
       at java.lang.reflect.Method.invoke(Method.java)
       at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:571)
       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1067)

If anyone knows the cause or what the fix is, that would be greatly appreciated!


Solution

  • tl;dr the fix is to use BundleCompat.getParcelable(Bundle, String, Class) from androidx.core:core-ktx:1.10.0 instead of getParcelable(String, Class).

    I think this crash is caused by an unfortunate combination of two things.

    1. AGP 8 enabled R8's full mode by default. R8's full mode strips default constructors and performs more aggresive optimizations.
    2. The new getParcelable(String, Class) methods introduced in API 33 has some bugs if the Parcelable is not defined in a particular way. This is documented here.

    It looks like the fragile implementation of getParcelable(String, Class) and R8 optimizing certain code is causing this crash. The fix for now appear to be to use the old getParcelable(String) method in API 33. Google has stated they have fixed the issue with getParcelable(String, Class) in API 34.

    Update: androidx.core:core-ktx:1.10.0 contains a fix for this issue. It contains BundleCompat.getParcelable(Bundle, String, Class) which will only call the new getParcelable(Bundle, Class) function on Android U and above.