Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 296
Wojciech Kulik Avatar asked Nov 27 '18 10:11

Wojciech Kulik


2 Answers

After all it looks like there must be duplicated id in preserved state, however I don't see any reasonable explanation why. Neither I can reproduce it on my devices. And as I described above:

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.

Nevertheless I found a workaround to stop crashes.

using Android.Content;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;

[assembly: ExportRenderer(typeof(Switch), typeof(MyApp.Droid.CustomRenderers.CustomSwitchRenderer))]
namespace MyApp.Droid.CustomRenderers
{
    public class CustomSwitchRenderer : SwitchRenderer
    {
        public CustomSwitchRenderer(Context context) : base(context)
        {
        }

        protected override void OnElementChanged(ElementChangedEventArgs<Switch> e)
        {
            base.OnElementChanged(e);

            if (this.Control != null)
            {
                this.Control.Id = -1;
                this.Control.SaveEnabled = false;
            }
        }
    }
}

It disables state preservation for all Switch controls. Just in case I also set Id = -1 to override id assigned by Xamarin. -1 is a constant in Android which means "no id".

This workaround doesn't break state preservation in Xamarin.Forms, because after Page recreation state relies on your bindings, not Android's mechanism.

However if you would like to make it work without disabling state preservation. You can set some big id which will be constant between runs. Of course you need to set a different ID for each Switch therefore you may need to create a custom Switch and add some property like AndroidId. Note that id should be lower than 0x00ffffff and big enough to avoid collisions with auto generated ids by Xamarin.

like image 133
Wojciech Kulik Avatar answered Nov 19 '22 04:11

Wojciech Kulik


This does not address your overall question, but I believe I can shed some light on your Update 3 section.

First let me re-state your question: why is it that TextView and CompoundButton have two different strategies for implementing onRestoreInstanceState()?

TextView performs conditional logic based on the particular Parcelable passed to it:

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

    SavedState ss = (SavedState) state;
    super.onRestoreInstanceState(ss.getSuperState());
    ...
}

While CompoundButton does not:

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

The reason for this is that TextView and CompoundButton have two different strategies for implementing onSaveInstanceState(), and so each class has a corresponding strategy to restore the state.

TextView can return two different types from onSaveInstanceState():

@Override
public Parcelable onSaveInstanceState() {
    Parcelable superState = super.onSaveInstanceState();
    ...

    if (freezesText || hasSelection) {
        SavedState ss = new SavedState(superState);
        ...
        return ss;
    }

    return superState;
}

TextView will only return its own custom SavedState class in situations where the super call doesn't save everything it needs (i.e. when the TextView has been asked to freeze its text or when it has a selection). In all other cases, it just delegates to the super call and returns that directly.

Since onRestoreInstanceState() will receive whatever onSaveInstanceState() returned, TextView needs to be able to work when it receives either the super return value or its own SavedState.

On the other hand, CompoundButton can only return one type from onSaveInstanceState():

@Override
public Parcelable onSaveInstanceState() {
    Parcelable superState = super.onSaveInstanceState();
    SavedState ss = new SavedState(superState);
    ss.checked = isChecked();
    return ss;
}

Because we know that the passed-in state object will always be of type SavedState, we don't have to do any conditional logic. We can just cast it and go.


Hopefully this answer provides a foundation that other answerers can build on, and perhaps eventually answer your primary question.

like image 33
Ben P. Avatar answered Nov 19 '22 06:11

Ben P.