记一次踩坑:onSaveInstanceState & onRestoreInstanceState过程分析

摘要

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;
}

在特定条件下mMultiViewStubinflate,通过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) {
// 1.注意mWindow#saveHierarchyState的调用
outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState());
// 2.mFragments.saveAllState主要保存Fragment相关的数据:
// 2.1 执行一些未完成的事务、退出动画等;
// 2.2 把所有active状态的fragment转化为可序列化的FragmentState对象保存下来
// 2.3 把所有添加到FragmentManager的fragment的index记录下来
// 2.4 把回退栈的信息记录下来
// 3.在Activity#onCreate中恢复数据
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) {
// 1.注意mWindow#restoreHierarchyState的调用
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() {
// 1.new Bundle
Bundle outState = new Bundle();
// 2.mContentParent为id=ID_ANDROID_CONTENT的ViewGroup
if (mContentParent == null) {
return outState;
}
// 3.new SparseArray
SparseArray<Parcelable> states = new SparseArray<Parcelable>();
// 4.调用View#saveHierarchyState保存状态
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) {
// 1.调用View#restoreHierarchyState恢复状态
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) {
// 1.一个View必须有id,SAVE_DISABLED_MASK标志没被打开才保存状态
if (mID != NO_ID && (mViewFlags & SAVE_DISABLED_MASK) == 0) {
mPrivateFlags &= ~PFLAG_SAVE_STATE_CALLED;
// 2.提供让子类重写的方法,以实现自己的保存逻辑
Parcelable state = onSaveInstanceState();
// 3.检测,子类必须要调用父类的onSaveInstanceState()方法,否则抛异常
if ((mPrivateFlags & PFLAG_SAVE_STATE_CALLED) == 0) {
throw new IllegalStateException(
"Derived class did not call super.onSaveInstanceState()");
}
if (state != null) {
// Log.i("View", "Freezing #" + Integer.toHexString(mID)
// + ": " + state);
// 4.将state放进SparseArray中,以view自身的id为key,当key相同的情况下,后面的put会覆盖掉前面put的结果
container.put(mID, state);
}
}
}
protected Parcelable onSaveInstanceState() {
// 1.设置标志位,在dispatchSaveInstanceState会检测
mPrivateFlags |= PFLAG_SAVE_STATE_CALLED;
// 2.默认不保存任何东西
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) {
// 1.通过id拿到state
Parcelable state = container.get(mID);
if (state != null) {
// Log.i("View", "Restoreing #" + Integer.toHexString(mID)
// + ": " + state);
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.");
}
}

4.1 HorizontalScrollView#onSaveInstanceState

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) {
// Some old apps reused IDs in ways they shouldn't have.
// Don't break them, but they don't get scroll state restoration.
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#onSaveInstanceStateActivity#onRestoreInstanceState方法,在6.0(不包含)及以下的版本,不调用super#onSaveInstanceStatesuper#onRestoreInstanceState方法。

总结

当View确定要保存&恢复数据的时候,请确保它们有唯一id。因为Android内部用id作为保存&恢复数据时使用的Key(SparseArray的key),不指定唯一id会造成数据被覆盖。

参考

View的onSaveInstanceState和onRestoreInstanceState过程分析