【问题标题】:Crash when restoring Android state - AbsSavedState cannot be cast恢复 Android 状态时崩溃 - 无法强制转换 AbsSavedState
【发布时间】:2019-04-29 01:43:49
【问题描述】:

我从 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.Android 中使用没有任何自定义布局的 Xamarin.Forms。
  • 我没有任何关于状态保存/恢复的自定义操作。
  • 我检查了 SwitchRenderer 及其基类的 Xamarin.Forms 源代码,也没有看到任何状态保存代码。

在有关 Stack Overflow 的许多问题中,都声称该问题可能是由重复的 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}}],
安卓:lastAutofillId=1073741825,
android:fragments=android.app.FragmentManagerState@969a700}]}
  1. 我在两个页面上有CompoundButtonsSwitch 的基类):MainPage 和模态页面。毕竟我认为在恢复状态时这种可能的不匹配可能是由重复的 id 造成的。我决定编写一段代码来打印带有 id 的整个层次结构。下面你可以看到MainPage和模态页面,一共3个开关。但是,这里没有重复。
-- 16908290 - ContentFrameLayout
---- -1 - 相对布局
------ -1 - 平台渲染器
-------- 1 - 页面渲染器
---------- -1 - DefaultRenderer
------------ -1 - DefaultRenderer
-------------- 2 - 图像渲染器
------------ -1 - CustomScrollViewRenderer
-------------- -1 - ScrollViewContainer
---------------- -1 - DefaultRenderer
------------------ -1 - DefaultRenderer
----------------- -1 - DefaultRenderer
---------------------- -1 - DefaultRenderer
------------------------------------ 3 - 图像渲染器
---------------------- 4 - 标签渲染器
---------------------- 5 - 标签渲染器
---------------------- -1 - DefaultRenderer
------------------------------------ 6 - 图像渲染器
------------------ -1 - DefaultRenderer
----------------- -1 - DefaultRenderer
---------------------- 7 - 标签渲染器
---------------------- 8 - 标签渲染器
---------------------- -1 - DefaultRenderer
------------------------------------ 9 - 图像渲染器
------------------ -1 - DefaultRenderer
----------------- -1 - DefaultRenderer
---------------------- -1 - DefaultRenderer
------------------------------------ -1 - GaugeChartRenderer
------------------------------------ 10 - 标签渲染器
------------------------------------ 11 - 标签渲染器
------------------------------------ -1 - GaugeChartRenderer
------------------------------------ 12 - 标签渲染器
------------------------------------ 13 - 标签渲染器
------------------ -1 - DefaultRenderer
-------------------- 14 - 标签渲染器
-------------------- 15 - 标签渲染器
------------------ -1 - 线性图表渲染器
-------------------- 16 - 线性图
------------------ -1 - DefaultRenderer
----------------- -1 - 自定义按钮渲染器
---------------------- 17 - 按钮
----------------- -1 - 自定义按钮渲染器
---------------------- 18 - 按钮
----------------- -1 - 自定义按钮渲染器
---------------------- 19 - 按钮
----------------- -1 - 自定义按钮渲染器
---------------------- 20 - 按钮
----------------- -1 - 自定义按钮渲染器
---------------------- 21 - 按钮
----------------- -1 - 自定义按钮渲染器
---------------------- 22 - 按钮
------------------ -1 - DefaultRenderer
------------------ -1 - DefaultRenderer
----------------- -1 - DefaultRenderer
---------------------- 23 - 标签渲染器
---------------------- 24 - 标签渲染器
---------------------- 25 - 标签渲染器
---------------------- 26 - 标签渲染器
---------------------- 27 - 标签渲染器
----------------- -1 - DefaultRenderer
---------------------- -1 - DefaultRenderer
------------------------ -1 - DefaultRenderer
-------------------------- 33 - 标签渲染器
-------------------------- 34 - 标签渲染器
-------------------------- 35 - 标签渲染器
------------------ -1 - DefaultRenderer
----------------- -1 - CustomSwitchRenderer
---------------------- 28 - 开关
-------------------- 29 - 标签渲染器
----------------- -1 - DefaultRenderer
---------------------- 36 - 图像渲染器
------------------ -1 - DefaultRenderer
----------------- -1 - CustomSwitchRenderer
---------------------- 30 - 开关
-------------------- 31 - 标签渲染器
------------------ -1 - DefaultRenderer
-------------------- 37 - 图像渲染器
----------------- -1 - 自定义按钮渲染器
---------------------- 32 - 按钮
-------- 44 - 模态容器
---------- -1 - 查看
---------- 38 - 页面渲染器
------------ -1 - DefaultRenderer
-------------- -1 - DefaultRenderer
---------------- -1 - DefaultRenderer
------------------ 39 - 标签渲染器
------------------ -1 - DefaultRenderer
-------------------- 45 - 图像渲染器
---------------- -1 - SearchBarRenderer
------------------ 40 - 搜索视图
-------------------- 16909226 - 线性布局
---------------------- 16909225 - AppCompatTextView
---------------------- 16909227 - AppCompatImageView
---------------------- 16909229 - 线性布局
------------------------ 16909231 - AppCompatImageView
---------------------- 16909232 - 线性布局
-------------------------- 16909233 - 自动完成文本视图
-------------------------- 16909228 - AppCompatImageView
------------------------ 16909321 - 线性布局
-------------------------- 16909230 - AppCompatImageView
-------------------------- 16909235 - AppCompatImageView
-------------- -1 - DefaultRenderer
---------------- -1 - ListViewRenderer
------------------ -1 - SwipeRefreshLayout
-------------------- 41 - 列表视图
---------------------- -1 - 容器
---------------------- -1 - 容器
------------------------ -1 - DefaultRenderer
----------------- -1 - 图像视图
-------------- -1 - DefaultRenderer
---------------- -1 - DefaultRenderer
------------------ -1 - CustomSwitchRenderer
-------------------- 42 - 开关
------------------ 43 - 标签渲染器
  1. 后来我想可能是Xamarin的id生成机制在状态恢复后失效了。但我检查了它,恢复后它被适当地增加了。我什至检查了 Xamarin.Forms/Platform.cs 中的源代码:
内部静态 int GenerateViewId()
{
    if ((int)Build.VERSION.SdkInt >= 17)
        返回全局::Android.Views.View.GenerateViewId();
    如果(s_id >= 0x00ffffff)
        s_id = 0x00000400;
    返回 s_id++;
}

静态 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。在另一个娱乐之后(例如在旋转屏幕之后),它将 ids 从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

【问题讨论】:

  • 您是否在多个布局文件中使用了相同的 id?您可以参考:stackoverflow.com/questions/28961176/…
  • 如帖子中所述,我没有自定义布局,我使用 xaml。此外,正如在更新 Xamarin 中调查的那样,似乎正确设置了 id。我发现在我的设备和模拟器上,我的子类开关没有调用 OnRestoreSavedInstance,即使 MainActivity 调用了它。看来这个问题主要发生在联想智能手机上。可能是他们的操作系统出了问题。

标签: android xamarin xamarin.forms


【解决方案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 只会在 super 调用没有保存它需要的所有内容的情况下返回它自己的自定义 SavedState 类(即当 TextView 被要求冻结其文本或当它有一个选择时)。在所有其他情况下,它只是委托给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 类型,所以我们不必执行任何条件逻辑。我们可以直接施放就走。


希望这个答案为其他回答者提供了一个基础,并可能最终回答您的主要问题。

【讨论】:

  • 非常感谢。这是有道理的,至少我对这个地方的担忧消失了:)
【解决方案2】:

毕竟看起来必须有重复的 id 处于保留状态,但是我没有看到任何合理的解释。我也无法在我的设备上重现它。正如我上面描述的:

Xamarin.Forms 通过增加计数器自动设置 ID。因此,在创建页面后,它会将 id 从1 设置为n。在另一个娱乐之后(例如在旋转屏幕之后),它将 ids 从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。当然,您需要为每个 Switch 设置不同的 ID,因此您可能需要创建自定义 Switch 并添加一些属性,例如 AndroidId。请注意,id 应低于 0x00ffffff 并足够大以避免与 Xamarin 自动生成的 id 发生冲突。

【讨论】:

    猜你喜欢
    • 2023-03-03
    • 1970-01-01
    • 1970-01-01
    • 2010-10-25
    • 1970-01-01
    • 2023-03-24
    • 2020-12-29
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多