android - 恢复 Android 状态时崩溃 - 无法强制转换 AbsSavedState
问题描述
我从 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)
- 不幸的是,我无法重现它。
- 我检查了这
CompoundButton
是一个基类,Switch
我的主页上有两个开关。 - 我只有一项主要活动。
- 我在 Xamarin.Android 中使用没有任何自定义布局的 Xamarin.Forms。
- 我没有任何关于状态保存/恢复的自定义操作。
- 我检查了 Xamarin.Forms 源代码
SwitchRenderer
及其基类,也没有看到任何状态保存代码。
在有关 Stack Overflow 的许多问题中,据称该问题可能是由 duplicated 引起的android:id
,但是正如我上面提到的,我没有自定义布局。
更新
我决定深入调查,并开始验证整个状态保存机制。以下是我的发现:
- 我发现整个视图层次结构是成对存储的
(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}]}
- 我在两个页面上有
CompoundButtons
(基类):和模式页面。毕竟我认为在恢复状态时这种可能的不匹配可能是由重复的 id 造成的。我决定编写一段代码来打印带有 id 的整个层次结构。下面你可以看到模态页面,一共3个开关。但是,这里没有重复。Switch
MainPage
MainPage
-- 16908290 - 内容框架布局 ---- -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 - 标签渲染器
- 后来想可能是状态恢复后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
)。
我找到了它以这种方式运行的原因。请看一下Android的实现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 通过增加计数器自动设置 id。因此,在创建页面后,它将 ids 设置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();
}
但是,TextView
(CompoundButton
的祖先) 有这个实现:
@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的缺陷。但是我仍然不明白 state 怎么可能不匹配AbsSavedState
并被传递给CompoundButton
而不是CompoundButton.SavedState
.
解决方案
毕竟看起来必须有重复的 id 在保留状态,但是我看不到任何合理的解释。我也无法在我的设备上重现它。正如我上面描述的:
Xamarin.Forms 通过增加计数器自动设置 id。因此,在创建页面后,它将 ids 设置
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。当然,您需要为每个设置不同的 ID,Switch
因此您可能需要创建自定义Switch
并添加一些属性,例如AndroidId
. 请注意,id 应小于0x00ffffff
且足够大,以避免与 Xamarin 自动生成的 id 发生冲突。
推荐阅读
- reactjs - 如何在 reactJS map 函数中添加条件语句
- javascript - 在 React 中导出 2 个不同的函数
- c# - ASP.NET MVC - 如果发生错误并返回视图,则不返回上传的文件
- sql-server - 将 SELECT 嵌套到 #temp 表中
- html - window.open() 打开一个新的电子浏览器窗口而不是原生浏览器窗口
- javascript - 随机密码生成器javascript不返回密码
- mysql - 如何将硬编码值包含在 mysql 查询的输出中?
- delphi - 从资源文件加载的透明 PNG 图像,使用 Grapics32 调整大小并在 Canvas 上绘制
- java - 使用 java 上传与 selenium 相同的图像时图像质量差
- jquery - 是否有适用于 jQuery 3.x 的 ImageMapster 的替代品?