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;