恢复Android状态时崩溃-无法强制转换AbsSavedState

时间:2018-11-27 10:28:36

标签: android xamarin xamarin.forms

我从Crashlytics收到有关Xamarin.Forms项目中以下崩溃的通知:

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)
  • 不幸的是,我无法复制它。
  • 我检查了CompoundButtonSwitch的基类,并且我在主页上有两个开关。
  • 我只有一项主要活动。
  • 我使用的Xamarin.Forms在Xamarin.Android中没有任何自定义布局。
  • 我对状态保存/恢复没有任何自定义操作。
  • 我检查了SwitchRenderer及其基类的Xamarin.Forms源代码,也没有看到任何状态保存代码。

在有关堆栈溢出的许多问题中,都声称该问题可能是由重复的android:id引起的,但是如上所述,我没有自定义布局。


更新

我决定进一步研究,并开始验证整个状态保存机制。以下是我的发现:

  1. 我发现整个视图层次结构存储为(viewId, state)对。此外,事实证明,所有视图仅将AbsSavedState的状态保存为CompoundButton存储CompoundButton.SavedState。因此,我的猜测是使用某种不正确的状态来还原CompoundButton。样本状态:
{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. 我在两页上有CompoundButtonsSwitch的基类):MainPage和模式页。毕竟,我认为恢复状态时可能出现的这种不匹配是由重复的ID以某种方式引起的。我决定写一段代码来打印带有id的整个层次结构。在下面您可以查看MainPage和模式页面,共有3个开关。但是,这里没有重复。
-- 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. 后来我认为状态恢复后Xamarin的id生成机制可能会失败。但是我检查了一下,恢复后适当地增加了它。我什至在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;

看起来不错,除非存在某些比赛条件。我的想法不多了。


更新2

我将Switch控件子类化,并覆盖了OnRestoreSavedInstance和奇怪的东西,而这在我的设备上从未调用过。但是,OnSaveInstanceState被调用。请注意,我正确地模拟了状态恢复(在MainActivity中被称为状态恢复,但不会传播到Switch)。

我发现了这种行为的原因。请看一下View.dispatchRestoreState的Android实现:

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通过增加计数器自动设置ID。因此,创建页面后,它将ID从1设置为n。再次进行娱乐之后(例如,在旋转屏幕之后),它将ID从n+1设置为2n+1。因此,没有一个控件能够恢复其状态,因为在保存状态时,它将被保存为id=x的状态,但是在重新创建Activity之后,此控件将具有不同的ID。

因此,由于没有状态恢复,因此永远不会发生此崩溃...


更新3

我注意到Android实现中也有些奇怪。 CompoundButton具有以下实现:

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

但是,TextViewCompoundButton的祖先)具有以下实现:

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

    // ...
}

如您所见,TextView首先验证此转换是否成功,CompoundButton则不成功。也许这是Android中的缺陷。但是我仍然看不到状态不匹配以及AbsSavedState而不是CompoundButton传递给CompoundButton.SavedState的可能性。

2 个答案:

答案 0 :(得分:1)

这不能解决您的总体问题,但是我相信可以为您的 Update 3 部分提供一些帮助。

首先让我重申您的问题:TextViewCompoundButton为什么有两种不同的策略来实施onRestoreInstanceState()

TextView根据传递给它的特定Parcelable执行条件逻辑:

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

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

CompoundButton没有:

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

这样做的原因是TextViewCompoundButton有两种不同的实现onSaveInstanceState()的策略,因此每个类都有相应的策略来恢复状态。

TextView可以从onSaveInstanceState()返回两种不同的类型:

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

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

    return superState;
}

TextView仅在SavedState调用未保存其所需内容的情况下(例如,当TextView被要求冻结其文本或具有以下内容时)返回其自己的自定义super类:选择)。在所有其他情况下,它仅委托给super调用并直接返回。

由于onRestoreInstanceState()将收到返回的onSaveInstanceState(),因此TextView在接收到super返回值或它自己的SavedState时必须能够工作。

另一方面,CompoundButton只能从onSaveInstanceState()返回一种类型:

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

由于我们知道传入的state对象将始终为SavedState类型,因此我们不必执行任何条件逻辑。我们可以抛弃它。


希望此答案为其他回答者可以建立的基础,甚至可能最终回答您的主要问题。

答案 1 :(得分:0)

毕竟,在保留状态下似乎必须有重复的ID,但是我看不到任何合理的解释。我都无法在设备上复制它。正如我上面所述:

  

Xamarin.Forms通过增加计数器自动设置ID。因此,创建页面后,它将ID从1设置为n。再次进行娱乐之后(例如,在旋转屏幕之后),它将ID从n+1设置为2n+1。因此,没有一个控件能够恢复其状态,因为在保存状态时,它将被保存为id = x的状态,但是在重新创建Activity之后,该控件将具有不同的id。

尽管如此,我还是找到了一种解决崩溃的方法。

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;
            }
        }
    }
}

它禁用所有Switch控件的状态保存。以防万一,我还设置了Id = -1来覆盖Xamarin分配的ID。 -1是Android中的常量,表示“无id”。

此解决方法不会破坏Xamarin.Forms中的状态保留,因为在Page之后,重新创建状态取决于您的绑定,而不是Android的机制。

但是,如果您想使其工作而不禁用状态保留。您可以设置一些较大的ID,这些ID在两次运行之间是恒定的。当然,您需要为每个Switch设置一个不同的ID,因此您可能需要创建自定义Switch并添加一些属性,例如AndroidId。请注意,id应该小于0x00ffffff,并且应该足够大以避免与Xamarin自动生成的ID发生冲突。