我从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
的基类,并且我在主页上有两个开关。SwitchRenderer
及其基类的Xamarin.Forms源代码,也没有看到任何状态保存代码。在有关堆栈溢出的许多问题中,都声称该问题可能是由重复的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}}], android:lastAutofillId=1073741825, android:fragments=android.app.FragmentManagerState@969a700}]}
CompoundButtons
(Switch
的基类):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
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();
}
但是,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中的缺陷。但是我仍然看不到状态不匹配以及AbsSavedState
而不是CompoundButton
传递给CompoundButton.SavedState
的可能性。
答案 0 :(得分:1)
这不能解决您的总体问题,但是我相信可以为您的 Update 3 部分提供一些帮助。
首先让我重申您的问题:TextView
和CompoundButton
为什么有两种不同的策略来实施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()); ... }
这样做的原因是TextView
和CompoundButton
有两种不同的实现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发生冲突。