Android 视图树状态保存
FragmentTabHost引起的思考
公司的项目是一个标准的FragmentTabHost
与Fragment
构成的四TAB布局。其中三个TAB中都包含有ListView
来展现一个列表。用户在切换TAB时,ListView
的当前位置会自动被保存,切换回来之后会自动滚动到上次的位置。
我们知道FragmentTabHost
内部对于Fragment
的切换使用的是attach
和dettach,此时必然会走Fragment的视图树的重建,也就是在切换tab的时候,fragment的UI元素会进行重建,当然也包括重建其中的ListView。那为什么在重建之后Listview能找到正确位置?为了找到解释,我开始查阅代码。
第一个想法是否是我们通过Fragment
的onSaveInstanceState
和Bundle
来保存的呢?但是看Fragment
中的方法并没有任何代码进行保存,也没有进行恢复。
有人告诉我是通过Adapter
来进行保存的,因为我们的Fragment
会持有listview
使用的ListAdapter
,所以在重建的时候listadapter
实际上复用之前的adapter
,但是,通查adapter的代码,发现并没有任何保存当前位置的代码。
这样不科学的事情怎么会发生?一个新创建出来的ListView居然会保存之前一个ListView的状态?
Android View状态保存
我们经常有这样的经验,在一个EditText
中我们输入一些文字,在屏幕翻转时,EditText
进行重建,其中的文字还会保留(前提是你为这个EditText
指定了ID)。
解释这个问题,我们需要从Android
的View
状态保存机制说起。
在View
中Android
定义了saveHierarchyState
,它用来保存这个View
所在的视图树的状态,传进来的参数是父view
保存的状态,看到这里,你应该已经意识到了这又是android View
中常用的递归伎俩。
public void saveHierarchyState(SparseArray<Parcelable> container) {
dispatchSaveInstanceState(container);
}
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
//这里可以看到如果你不指定view的ID,系统是不会给你保存view的状态的
if (mID != NO_ID && (mViewFlags & SAVE_DISABLED_MASK) == 0) {
mPrivateFlags &= ~PFLAG_SAVE_STATE_CALLED;
//获取当前View需要保存的Parcelable
Parcelable state = 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);
//妈蛋,直接是简单粗暴的 viewId ----> Parcelable的映射
container.put(mID, state);
}
}
}
从上面的代码,我们可以大致想象一下saveHierarchySate
的过程,顶层的view
调用saveHierarchyState
,然后dispatchSaveInstanceState
保存自己,为了让递归走下去,我们都能想象到在ViewGroup
中的dispatchSaveInstanceState
会调用子View
的saveHierachySate
,是不是这样呢,搂一眼ViewGroup
中的代码:
@Override
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
//先把自己放到SparseArray中
super.dispatchSaveInstanceState(container);
final int count = mChildrenCount;
final View[] children = mChildren;
//保存孩儿们的状态
for (int i = 0; i < count; i++) {
View c = children[i];
if ((c.mViewFlags & PARENT_SAVE_DISABLED_MASK) != PARENT_SAVE_DISABLED) {
c.dispatchSaveInstanceState(container);
}
}
}
看到这里,相信你已经迫不及待想要看顶层View
是在什么时机保存视图树信息的,Activity中的onSaveInstanceState
有如下语句:
outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState());
这里调用了跟到PhoneWindow.saveHierarchyState()
,
SparseArray<Parcelable> states = new SparseArray<Parcelable>();
mContentParent.saveHierarchyState(states);
这里交代地很清楚明白。ok,总结一下这个流程
Acitvity
在状态需要保存时,直接new了一个SparseArray
,建立movieId
和Parcelable
的映射,其中每一个View
都会使用一个Parcelable
来序列化需要保存的内容,ViewGroup
在保存了自己的同时,也会去调用子类的保存状态的方法,并把上面传下来的SparseArray
一直往下传。
视图树状态恢复
视图树保存下来的SparsesArray不是被ViewGroup自己持有(避免View被重新new出来,这些信息丢失,这也是前面疑惑的根源)。以Activity
为例,我们可以看到整个视图树状态保存之后会放到一个Bundle
中,那我们来看看视图树是如何恢复的。
在Activity
的onRestoreInstanceState
中
protected void onRestoreInstanceState(Bundle savedInstanceState) {
if (mWindow != null) {
Bundle windowState = savedInstanceState.getBundle(WINDOW_HIERARCHY_TAG);
if (windowState != null) {
mWindow.restoreHierarchyState(windowState);
}
}
}
跟到PhoneWindow
中的restoreHierarchyState
SparseArray<Parcelable> savedStates = savedInstanceState.getSparseParcelableArray(VIEWS_TAG);
if (savedStates != null) {
mContentParent.restoreHierarchyState(savedStates);
}
从Bundle中拿出之前保存的SparseArray,丢给顶层View的restoreHierarchyState
public void restoreHierarchyState(SparseArray<Parcelable> container) {
dispatchRestoreInstanceState(container);
}
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
if (mID != NO_ID) {
//拿到当前View的id对应的Parcelable,注意,这个Parcelable很有可能是之前id相同的View保存的
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()");
}
}
}
}
调用到View的onRestoreInstanceState
,这里让子类去处理拿出的信息。
到这里,可以猜到在ViewGroup的dispatchRestoreInstanceState
调用super方法(恢复自己),同时应该会去恢复所有子View,这里不上代码了。
回到Fragment状态保存的问题
虽然attach
和dettach
并不会引起Activity的状态保存,但是由于视图树保存状态的机制可以知道,Fragment也可以只保留自己所持有的View,然后恢复Fragment的视图树。
在FragmentManager
的moveToState
方法中,调用了saveFragmentViewState
void saveFragmentViewState(Fragment f) {
if (f.mView == null) {
return;
}
if (mStateArray == null) {
mStateArray = new SparseArray<Parcelable>();
} else {
mStateArray.clear();
}
//f.mView是Fragment的顶层View
f.mView.saveHierarchyState(mStateArray);
if (mStateArray.size() > 0) {
f.mSavedViewState = mStateArray;
mStateArray = null;
}
}
往下走和Activity恢复视图树的过程一样鸟!
这样看起来,ListView能够恢复到上次保存的位置,可以看到AbsListView中的onSaveInstanceState
和onRestoreInstanceState
进行了保存和恢复,其中保存一个变量 mSyncPosition = ss.position;