ViewPager2引起的Wrong state class, expecting View State but received class

在使用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)

报错源码分析

ViewrestoreInstanceState

@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,然后重写dispatchSaveInstanceStatedispatchRestoreInstanceState 为空实现。

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相同,产生了崩溃。

解决方案参考上面的。

  • 22
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值