在使用ViewPager2时,线上报了一个偶现错误,问题的描述是这样的:
java.lang.IllegalArgumentException
Wrong state class, expecting View State but received class
androidx.recyclerview.widget.RecyclerView$SavedState instead.
This usually happens when two views of different type have the same id in the same hierarchy.
This view's id is id/0x1. Make sure other views do not use the same id.
在文章 Android 组件提供的状态保存(saveInstanceState)与恢复(restoreInstanceState) 中我知道,这个问题是 因为 视图树中存在两个id相同的View,并且在恢复数据时出了问题。
但是页面中我们明明都是指定了id,按道理来讲是不会出现重复的呀?
通过模拟与查看源码发现,我们虽然给ViewPager2指定了Id,像这样:
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
我们都知道ViewPager2是由RecyclerView实现的,其内部还有一个View,就是RecyclerView,她的RecyclerView的id,并不是通过指定的方式确定的,而是通过一个方法自动生成的。
mRecyclerView = new RecyclerViewImpl(context);
mRecyclerView.setId(ViewCompat.generateViewId());
如果在整个视图中,有一个View的id是和ViewCompat.generateViewId()
生成的id一样的话,在数据保存与恢复时就会出问题。
我们模拟一下这种情况:
在MainActivity的布局文件中加入两个View,一个EditText(不指定id),一个ViewPager2(指定id)
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:id="@+id/ll_root"
tools:context=".MainActivity">
<EditText
android:tag="ett"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
在MainActivity中,将EditText的id设置为1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
findViewById<LinearLayout>(R.id.ll_root)?.let {
it.findViewWithTag<EditText>("ett")?.id = 1
}
}
运行项目,旋转手机,发现App崩溃了,报错如下:
![[Pasted image 20240425102747.png]]
报错原文:
Process: com.example.test, PID: 24413
java.lang.IllegalArgumentException: Wrong state class, expecting View State but received class androidx.recyclerview.widget.RecyclerView$SavedState instead. This usually happens when two views of different type have the same id in the same hierarchy. This view's id is id/0x1. Make sure other views do not use the same id.
at android.view.View.onRestoreInstanceState(View.java:19959)
at android.widget.TextView.onRestoreInstanceState(TextView.java:5934)
at android.view.View.dispatchRestoreInstanceState(View.java:19931)
at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3892)
at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3892)
at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3892)
at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3892)
at android.view.View.restoreHierarchyState(View.java:19909)
at com.android.internal.policy.PhoneWindow.restoreHierarchyState(PhoneWindow.java:2162)
at android.app.Activity.onRestoreInstanceState(Activity.java:1602)
at com.example.test.MainActivity.onRestoreInstanceState(MainActivity.kt:88)
at android.app.Activity.performRestoreInstanceState(Activity.java:1557)
at android.app.Instrumentation.callActivityOnRestoreInstanceState(Instrumentation.java:1354)
at android.app.ActivityThread.handleStartActivity(ActivityThread.java:3348)
at android.app.servertransaction.TransactionExecutor.performLifecycleSequence(TransactionExecutor.java:221)
at android.app.servertransaction.TransactionExecutor.cycleToPath(TransactionExecutor.java:201)
at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:173)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2044)
at android.os.Handler.dispatchMessage(Handler.java:107)
at android.os.Looper.loop(Looper.java:224)
at android.app.ActivityThread.main(ActivityThread.java:7562)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:539)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:950)
报错源码分析
View
的restoreInstanceState
@CallSuper
protected void onRestoreInstanceState(Parcelable state) {
mPrivateFlags |= PFLAG_SAVE_STATE_CALLED;
if (state != null && !(state instanceof AbsSavedState)) {
throw new IllegalArgumentException("Wrong state class, expecting View State but "
+ "received " + state.getClass().toString() + " instead. This usually happens "
+ "when two views of different type have the same id in the same hierarchy. "
+ "This view's id is " + ViewDebug.resolveId(mContext, getId()) + ". Make sure "
+ "other views do not use the same id.");
}
// …… 省略代码
}
在此方法中,找到了这个报错。既然存在这个报错,那么state instanceof AbsSavedState
的值是false。而报错中拿到的类是androidx.recyclerview.widget.RecyclerView$SavedState
,也就是说,androidx.recyclerview.widget.RecyclerView$SavedState
并不是 AbsSavedState
的子类。
查看androidx.recyclerview.widget.RecyclerView
源码,看一下onSaveInstanceState
实现。
在RecyclerView
中找到代码:
@Override
protected Parcelable onSaveInstanceState() {
SavedState state = new SavedState(super.onSaveInstanceState());
if (mPendingSavedState != null) {
state.copyFrom(mPendingSavedState);
} else if (mLayout != null) {
state.mLayoutState = mLayout.onSaveInstanceState();
} else {
state.mLayoutState = null;
}
return state;
}
在这里,它创建了自己的SavedState,继续查看SavedStated
这个类,看它是否实现了AbsSavedState
@RestrictTo(LIBRARY)
public static class SavedState extends AbsSavedState {
// …… 内部代码省略
}
我们发现这个SavedState
明明就是AbsSavedState
,继续查看AbsStavedState
发现
package androidx.customview.view;
// 省略无用导包
/**
* A {@link Parcelable} implementation that should be used by inheritance
* hierarchies to ensure the state of all classes along the chain is saved.
*/
@SuppressLint("BanParcelableUsage")
public abstract class AbsSavedState implements Parcelable {
// 省略代码
}
原来RecyclerView
中使用的SavedState
继承的AbsSavedState
完整类限定名是androidx.customview.view.AbsSavedState
而View中在恢复数据时,判断的是:android.view.AbsSavedState
所以这两个类当然不匹配了!于是报错了。
整个执行步骤
在LinearLayout
中,保存数据时的执行步骤是这样
LinearLayout# onSaveInstanceState stateContainer: SparseArray
EditText onSaveInstance key: 1, value: android.view.AbsSavedState
ViewPager2 onSaveInstance
RecyclerView onSaveInstance key: 1, value: androidx.customview.view.AbsSavedState
保存数据后,SparseArray中id=1对应的Value 已经变成了androidx.customview.view.AbsSavedState
恢复数据时,依然是以上步骤,
LinearLayout# onRestoreInstanceState stateContainer: SparseArray
EditText onRestoreInstanceState key: 1, value: androidx.customview.view.AbsSavedState
// 崩溃
在EditText
恢复数据时,拿到的是RecyclerView
保存的数据,也就是androidx.customview.view.AbsSavedState
的子类对象,所以在恢复时就报错了。
如何防止这个报错
最好的方式当然是不要让视图内存在id一样的View,最好View的id都手动指定,包括ViewGroup的内部View的id。
这就需要我们在使用第三方UI组件时,查阅源码,看是否有可能存在,id重复的情况。这里提供几个处理这个崩溃的方案。
1、把ViewPager2改为ViewPager
不再使用ViewPager2,改为使用ViewPager,即可。
2、修改ViewPager2中的RecyclerView的id为手动指定
findViewById<ViewPager2>(R.id.viewPager)?.let {vp ->
vp.children.firstOrNull { it is RecyclerView }?.id = 10086
}
3、不恢复、不保存数据状态
在Android的Activity中,每次重建都会走onCreate,而大多数时候我们都会在onCreate中获取数据的,所以对于一些用于纯展示的页面,完全可以不用保存数据状态。
那么,我们可以直接不保存数据了。
自定义一个View,然后重写dispatchSaveInstanceState
与dispatchRestoreInstanceState
为空实现。
class NotSaveInstanceFrameLayout: FrameLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
)
override fun dispatchSaveInstanceState(container: SparseArray<Parcelable>?) {
}
override fun dispatchRestoreInstanceState(container: SparseArray<Parcelable>?) {
}
}
在布局中引用该布局:
<com.example.test.NotSaveInstanceFrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</com.example.test.NotSaveInstanceFrameLayout>
ViewPager2 与百度地图出现在同一个视图树中出现崩溃
如果你的页面中使用了百度地图,那么在同一个页面中最好不要再使用ViewPager2了,我最近接到的崩溃就是因为ViewPager2与百度地图处于同一个页面,而百度地图中存在一个id为1的ImageView,导致与ViewPager2中的RecyclerView id相同,产生了崩溃。
解决方案参考上面的。