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 forSwitch
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:
- I discovered that whole view hierarchy is stored as pairs
(viewId, state)
. Also it turned out that all views preserve state asAbsSavedState
onlyCompoundButton
storesCompoundButton.SavedState
. Therefore my guess is that somehow incorrect state was used to restoreCompoundButton
. 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}]}
- I've got
CompoundButtons
(base class forSwitch
) 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 seeMainPage
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
- 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