摘要
app灰度中,发现如下crash一枚。而且全部集中在Android 4.0和5.0的手机上。
1 2
| Caused by java.lang.IllegalArgumentException Wrong state class, expecting View State but received class android.widget.HorizontalScrollView$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/0x0. Make sure other views do not use the same id.
|
分析步骤
1.查看异常大意
异常大意:同一个层级中存在两个不同类型的HorizontalScrollView
,但却使用了同样的id
。
1.看到HorizontalScrollView
,第一个是联想到商详页多件装使用HorizontalScrollView
控件,查看代码,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| mMultiViewStub = new ViewStub(getContext(), R.layout.goods_detail_multi_set); <HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/horizontal_scroll_mult_layout" android:layout_width="wrap_content" android:layout_height="wrap_content" > ... </HorizontalScrollView> if (mMultiViewStub != null) { mHorizontalScrollView = (HorizontalScrollView) mMultiViewStub.inflate(); mMultiViewStub = null; }
|
在特定条件下mMultiViewStub
被inflate
,通过AS自带的Tools-Layout Inspector工具查看此处mID
内容为0x0;
2.发现通用组件标题栏中使用SmartTabLayout
,它继承自HorizontalScrollView
,通过AS自带的Tools-Layout Inspector工具查看此处mID
内容为NO_ID
,NO_ID
是一个常量,值为-1。
很明显,这两个id
并不相等,所以不是因为使用了同样的id
导致此异常。
2.搜索异常关键字
搜索异常关键字when two views of different type have the same id in the same hierarchy
,发现抛出异常的地方和保存&恢复数据相关。
3.尝试复现crash
找了一台5.1的测试机,打开开发者选项-不保留活动,进入一个包含多件装的商品详情页,从多件装A切换到多件装B后,将app切到后台,再切回前台,即crash。
分析原因
那么onSaveInstanceState
& onRestoreInstanceState
到底做了什么。来扒一扒源码。
以下基于5.1.1源码分析:
1.1 Activity#onSaveInstanceState
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| protected void onSaveInstanceState(Bundle outState) { outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState()); Parcelable p = mFragments.saveAllState(); if (p != null) { outState.putParcelable(FRAGMENTS_TAG, p); } getApplication().dispatchActivitySaveInstanceState(this, outState); }
|
1.2 Activity#onRestoreInstanceState
1 2 3 4 5 6 7 8 9
| protected void onRestoreInstanceState(Bundle savedInstanceState) { if (mWindow != null) { Bundle windowState = savedInstanceState.getBundle(WINDOW_HIERARCHY_TAG); if (windowState != null) { mWindow.restoreHierarchyState(windowState); } } }
|
2.1 PhoneWindow#saveHierarchyState
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public Bundle saveHierarchyState() { Bundle outState = new Bundle(); if (mContentParent == null) { return outState; } SparseArray<Parcelable> states = new SparseArray<Parcelable>(); mContentParent.saveHierarchyState(states); outState.putSparseParcelableArray(VIEWS_TAG, states); ... return outState; }
|
2.2 PhoneWindow#restoreHierarchyState
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public void restoreHierarchyState(Bundle savedInstanceState) { if (mContentParent == null) { return; } SparseArray<Parcelable> savedStates = savedInstanceState.getSparseParcelableArray(VIEWS_TAG); if (savedStates != null) { mContentParent.restoreHierarchyState(savedStates); } ... }
|
3.1 View#dispatchSaveInstanceState
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) { if (mID != NO_ID && (mViewFlags & SAVE_DISABLED_MASK) == 0) { mPrivateFlags &= ~PFLAG_SAVE_STATE_CALLED; Parcelable state = onSaveInstanceState(); if ((mPrivateFlags & PFLAG_SAVE_STATE_CALLED) == 0) { throw new IllegalStateException( "Derived class did not call super.onSaveInstanceState()"); } if (state != null) { container.put(mID, state); } } } protected Parcelable onSaveInstanceState() { mPrivateFlags |= PFLAG_SAVE_STATE_CALLED; return BaseSavedState.EMPTY_STATE; }
|
3.2 View#dispatchRestoreInstanceState
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) { if (mID != NO_ID) { Parcelable state = container.get(mID); if (state != null) { mPrivateFlags &= ~PFLAG_SAVE_STATE_CALLED; onRestoreInstanceState(state); if ((mPrivateFlags & PFLAG_SAVE_STATE_CALLED) == 0) { throw new IllegalStateException( "Derived class did not call super.onRestoreInstanceState()"); } } } } protected void onRestoreInstanceState(Parcelable state) { mPrivateFlags |= PFLAG_SAVE_STATE_CALLED; if (state != BaseSavedState.EMPTY_STATE && state != null) { 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."); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Override protected Parcelable onSaveInstanceState() { if (mContext.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR2) { return super.onSaveInstanceState(); } Parcelable superState = super.onSaveInstanceState(); SavedState ss = new SavedState(superState); ss.scrollPosition = mScrollX; ss.isLayoutRtl = isLayoutRtl(); return ss; }
|
可以看到,HorizontalScrollView
复写了onSaveInstanceState
方法,所以符合条件state != BaseSavedState.EMPTY_STATE && state != null
,所以在恢复数据的时候抛出异常。
6.0View#onRestoreInstanceState
对比
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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."); } if (state != null && state instanceof BaseSavedState) { mStartActivityRequestWho = ((BaseSavedState) state).mStartActivityRequestWhoSaved; } }
|
对比发现,条件从state != BaseSavedState.EMPTY_STATE && state != null
变成了state != null && !(state instanceof AbsSavedState)
,在6.0中HorizontalScrollView#onSaveInstanceState
对比5.0并没有太大变化,SavedState
继承自AbsSavedState
,所以state instanceof AbsSavedState
为true,在6.0机型上并不会抛出该异常。
解决办法
复写Activity#onSaveInstanceState
和Activity#onRestoreInstanceState
方法,在6.0(不包含)及以下的版本,不调用super#onSaveInstanceState
和super#onRestoreInstanceState
方法。
总结
当View确定要保存&恢复数据的时候,请确保它们有唯一id。因为Android内部用id作为保存&恢复数据时使用的Key(SparseArray的key),不指定唯一id会造成数据被覆盖。
参考
View的onSaveInstanceState和onRestoreInstanceState过程分析