Monday, 3 December 2018

Crash when restoring Android state - AbsSavedState cannot be cast

I receive notifications from Crashlytics about the following crash in my Xamarin.Forms project:

Fatal Exception: java.lang.RuntimeException: Unable to start activity 
ComponentInfo{com.xxx.xxx/xxxxx.MainActivity}: 
java.lang.ClassCastException: android.view.AbsSavedState$1 cannot be cast to 
android.widget.CompoundButton$SavedState
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2957)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3032)
at android.app.ActivityThread.-wrap11(Unknown Source)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1696)
at android.os.Handler.dispatchMessage(Handler.java:105)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6944)
at java.lang.reflect.Method.invoke(Method.java)
at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:327)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1374)

Caused by java.lang.ClassCastException: 
android.view.AbsSavedState$1 cannot be cast to android.widget.CompoundButton$SavedState
at android.widget.CompoundButton.onRestoreInstanceState(CompoundButton.java:619)
at android.view.View.dispatchRestoreInstanceState(View.java:18884)
at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3936)
at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3936)
at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3936)
at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3936)
at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3936)
at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3936)
at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3936)
at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3936)
at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3936)
at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3936)
at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3936)
at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3936)
at android.view.View.restoreHierarchyState(View.java:18862)
at com.android.internal.policy.PhoneWindow.restoreHierarchyState(PhoneWindow.java:2248)
at android.app.Activity.onRestoreInstanceState(Activity.java:1153)
at android.app.Activity.performRestoreInstanceState(Activity.java:1108)
at android.app.Instrumentation.callActivityOnRestoreInstanceState(Instrumentation.java:1266)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2930)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3032)
at android.app.ActivityThread.-wrap11(Unknown Source)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1696)
at android.os.Handler.dispatchMessage(Handler.java:105)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6944)
at java.lang.reflect.Method.invoke(Method.java)
at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:327)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1374)

  • Unfortunately, I can't reproduce it.
  • I checked that CompoundButton is a base class for Switch and I've got two switches on my main page.
  • I've got only one main activity.
  • I use Xamarin.Forms without any custom layouts in Xamarin.Android.
  • I don't have any custom actions on state preservation/restoration.
  • I checked Xamarin.Forms source code for SwitchRenderer and its base classes and I don't see any state preservation code either.

In many questions on Stack Overflow it is claimed that the issue might be caused by duplicated android:id, however as I mentioned above, I don't have custom layouts.


Update

I decided to go deeper with investigation and I started verifying whole state preservation mechanism. Below are my findings:

  1. I discovered that whole view hierarchy is stored as pairs (viewId, state). Also it turned out that all views preserve state as AbsSavedState only CompoundButton stores CompoundButton.SavedState. Therefore my guess is that somehow incorrect state was used to restore CompoundButton. Sample state:
{Bundle[{  android:viewHierarchyState=Bundle[{android:views=
{1=android.view.AbsSavedState$1@e738983,2=android.view.AbsSavedState$1@e738983,
3=android.view.AbsSavedState$1@e738983, 4=android.view.AbsSavedState$1@e738983,     
5=android.view.AbsSavedState$1@e738983, 6=android.view.AbsSavedState$1@e738983, 
7=android.view.AbsSavedState$1@e738983, 8=android.view.AbsSavedState$1@e738983, 
9=android.view.AbsSavedState$1@e738983, 10=android.view.AbsSavedState$1@e738983,    
11=android.view.AbsSavedState$1@e738983, 12=android.view.AbsSavedState$1@e738983, 
13=android.view.AbsSavedState$1@e738983, 14=android.view.AbsSavedState$1@e738983, 
15=android.view.AbsSavedState$1@e738983, 16=android.view.AbsSavedState$1@e738983,   
17=android.view.AbsSavedState$1@e738983, 18=android.view.AbsSavedState$1@e738983, 
19=android.view.AbsSavedState$1@e738983, 20=android.view.AbsSavedState$1@e738983, 
21=android.view.AbsSavedState$1@e738983, 22=android.view.AbsSavedState$1@e738983,   
23=android.view.AbsSavedState$1@e738983, 24=CompoundButton.SavedState{26e683d checked=false},
25=android.view.AbsSavedState$1@e738983, 26=CompoundButton.SavedState{8f32832 checked=true}, 
27=android.view.AbsSavedState$1@e738983, 28=android.view.AbsSavedState$1@e738983,   
29=android.view.AbsSavedState$1@e738983, 30=android.view.AbsSavedState$1@e738983, 
31=android.view.AbsSavedState$1@e738983, 32=android.view.AbsSavedState$1@e738983, 
33=android.view.AbsSavedState$1@e738983, 34=android.view.AbsSavedState$1@e738983,   
35=android.view.AbsSavedState$1@e738983, 36=android.view.AbsSavedState$1@e738983,
37=android.view.AbsSavedState$1@e738983,    
16908290=android.view.AbsSavedState$1@e738983, 
2131558525=android.view.AbsSavedState$1@e738983,    
2131558526=android.view.AbsSavedState$1@e738983}}], 
android:lastAutofillId=1073741825, 
android:fragments=android.app.FragmentManagerState@969a700}]}
  1. I've got CompoundButtons (base class for Switch) on two pages: MainPage and modal page. After all I thought that maybe this possible mismatch while restoring state is caused by duplicated ids somehow. I decided to write a piece of code to print whole hierarchy with ids. Below you can see MainPage and modal page, in total 3 switches. However, there is no duplication here.
-- 16908290 - ContentFrameLayout
---- -1 - RelativeLayout
------ -1 - PlatformRenderer
-------- 1 - PageRenderer
---------- -1 - DefaultRenderer
------------ -1 - DefaultRenderer
-------------- 2 - ImageRenderer
------------ -1 - CustomScrollViewRenderer
-------------- -1 - ScrollViewContainer
---------------- -1 - DefaultRenderer
------------------ -1 - DefaultRenderer
-------------------- -1 - DefaultRenderer
---------------------- -1 - DefaultRenderer
------------------------ 3 - ImageRenderer
---------------------- 4 - LabelRenderer
---------------------- 5 - LabelRenderer
---------------------- -1 - DefaultRenderer
------------------------ 6 - ImageRenderer
------------------ -1 - DefaultRenderer
-------------------- -1 - DefaultRenderer
---------------------- 7 - LabelRenderer
---------------------- 8 - LabelRenderer
---------------------- -1 - DefaultRenderer
------------------------ 9 - ImageRenderer
------------------ -1 - DefaultRenderer
-------------------- -1 - DefaultRenderer
---------------------- -1 - DefaultRenderer
------------------------ -1 - GaugeChartRenderer
------------------------ 10 - LabelRenderer
------------------------ 11 - LabelRenderer
------------------------ -1 - GaugeChartRenderer
------------------------ 12 - LabelRenderer
------------------------ 13 - LabelRenderer
------------------ -1 - DefaultRenderer
-------------------- 14 - LabelRenderer
-------------------- 15 - LabelRenderer
------------------ -1 - LinearChartRenderer
-------------------- 16 - LinearChart
------------------ -1 - DefaultRenderer
-------------------- -1 - CustomButtonRenderer
---------------------- 17 - Button
-------------------- -1 - CustomButtonRenderer
---------------------- 18 - Button
-------------------- -1 - CustomButtonRenderer
---------------------- 19 - Button
-------------------- -1 - CustomButtonRenderer
---------------------- 20 - Button
-------------------- -1 - CustomButtonRenderer
---------------------- 21 - Button
-------------------- -1 - CustomButtonRenderer
---------------------- 22 - Button
------------------ -1 - DefaultRenderer
------------------ -1 - DefaultRenderer
-------------------- -1 - DefaultRenderer
---------------------- 23 - LabelRenderer
---------------------- 24 - LabelRenderer
---------------------- 25 - LabelRenderer
---------------------- 26 - LabelRenderer
---------------------- 27 - LabelRenderer
-------------------- -1 - DefaultRenderer
---------------------- -1 - DefaultRenderer
------------------------ -1 - DefaultRenderer
-------------------------- 33 - LabelRenderer
-------------------------- 34 - LabelRenderer
-------------------------- 35 - LabelRenderer
------------------ -1 - DefaultRenderer
-------------------- -1 - CustomSwitchRenderer
---------------------- 28 - Switch
-------------------- 29 - LabelRenderer
-------------------- -1 - DefaultRenderer
---------------------- 36 - ImageRenderer
------------------ -1 - DefaultRenderer
-------------------- -1 - CustomSwitchRenderer
---------------------- 30 - Switch
-------------------- 31 - LabelRenderer
------------------ -1 - DefaultRenderer
-------------------- 37 - ImageRenderer
-------------------- -1 - CustomButtonRenderer
---------------------- 32 - Button
-------- 44 - ModalContainer
---------- -1 - View
---------- 38 - PageRenderer
------------ -1 - DefaultRenderer
-------------- -1 - DefaultRenderer
---------------- -1 - DefaultRenderer
------------------ 39 - LabelRenderer
------------------ -1 - DefaultRenderer
-------------------- 45 - ImageRenderer
---------------- -1 - SearchBarRenderer
------------------ 40 - SearchView
-------------------- 16909226 - LinearLayout
---------------------- 16909225 - AppCompatTextView
---------------------- 16909227 - AppCompatImageView
---------------------- 16909229 - LinearLayout
------------------------ 16909231 - AppCompatImageView
------------------------ 16909232 - LinearLayout
-------------------------- 16909233 - AutoCompleteTextView
-------------------------- 16909228 - AppCompatImageView
------------------------ 16909321 - LinearLayout
-------------------------- 16909230 - AppCompatImageView
-------------------------- 16909235 - AppCompatImageView
-------------- -1 - DefaultRenderer
---------------- -1 - ListViewRenderer
------------------ -1 - SwipeRefreshLayout
-------------------- 41 - ListView
---------------------- -1 - Container
---------------------- -1 - Container
------------------------ -1 - DefaultRenderer
-------------------- -1 - ImageView
-------------- -1 - DefaultRenderer
---------------- -1 - DefaultRenderer
------------------ -1 - CustomSwitchRenderer
-------------------- 42 - Switch
------------------ 43 - LabelRenderer
  1. Later I thought that maybe Xamarin's id generation mechanism fails after state restoration. But I checked it and after restoration it is properly increased. I even checked source code in Xamarin.Forms/Platform.cs:
internal static int GenerateViewId()
{
    if ((int)Build.VERSION.SdkInt >= 17)
        return global::Android.Views.View.GenerateViewId();
    if (s_id >= 0x00ffffff)
        s_id = 0x00000400;
    return s_id++;
}

static int s_id = 0x00000400;

It looks fine, unless there is some race condition. I'm running out of ideas.


Update 2

I subclassed Switch control and overrode OnRestoreSavedInstance and strange thing that it's never called on my devices. However, OnSaveInstanceState is called. Please mind that I properly simulated state restoration (it is called in MainActivity, but doesn't propagate to Switch).

I found the reason why it behaves in this way. Please take a look at Android's implementation for View.dispatchRestoreState:

protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) 
{
    if (mID != NO_ID) {
        Parcelable state = container.get(mID);  // <--- HERE
        if (state != null) {
            // Log.i("View", "Restoreing #" + Integer.toHexString(mID)
            // + ": " + state);
            mPrivateFlags &= ~SAVE_STATE_CALLED;
            onRestoreInstanceState(state);
            if ((mPrivateFlags & SAVE_STATE_CALLED) == 0) {
                throw new IllegalStateException(
                        "Derived class did not call super.onRestoreInstanceState()");
            }
        }
    }
}

Xamarin.Forms sets ids automatically by increasing counter. Therefore after creating page it sets ids from 1 to n. After another recreation (for example after rotating screen) it sets ids from n+1 to 2n+1. Therefore none control will be able to restore its state, because when preserving state it will be saved as state for id=x, however after recreating Activity this control will have a different id.

Therefore this crash should never occur, because of no state restoration...


Update 3

I noticed also something strange in Android's implementation. CompoundButton has this implementation:

@Override
public void onRestoreInstanceState(Parcelable state) {
    SavedState ss = (SavedState) state;
    super.onRestoreInstanceState(ss.getSuperState());
    setChecked(ss.checked);
    requestLayout();
}

However, TextView (CompoundButton's ancestor) has this implementation:

@Override
public void onRestoreInstanceState(Parcelable state) {
    if (!(state instanceof SavedState)) {
        super.onRestoreInstanceState(state);
        return;
    }
    SavedState ss = (SavedState) state;
    super.onRestoreInstanceState(ss.getSuperState());

    // ...
}

As you can see, TextView validates first if this cast will be successful, CompoundButton doesn't. Maybe it's a defect in Android. But still I don't see how it is possible that state has been mismatched and AbsSavedState has been passed to CompoundButton instead of CompoundButton.SavedState.



from Crash when restoring Android state - AbsSavedState cannot be cast

No comments:

Post a Comment