本文基于Support-V4包下的Fragment、FragmentManager类,SDK版本为27
背景描述
测试提了bug,页面输入框在录入数据后,切到其他页面再返回,输入框值会被最后一项覆盖。
每行控件都是通过LayoutInflater
生成,添加进容器。
复现步骤
只要滑动页数只要超过ViewPager的OffscreenPageLimit()
默认值(1),滑回时就会复现
先说结论
控件保存数据时,把当前状态序列化,保存到变量(SparseArray
),Key是控件ID。
恢复数据时,根据ID,从mStateArray
里面取序列化对象。
由于每个输入框的ID一样,导致mStateArray
里该ID对应的值,就是最后一个输入框的序列化对象;
因此在恢复数据时,相同ID的控件,数据都会被最后一项覆盖。
所以禁用数据保存、恢复功能即可,下述操作任选一个
把View的id设置成View.NO_ID
或者 .setSaveEnabled(false);
问题排查
排除业务代码问题后,我们给输入框添加了TextChanged监听事件,断点。
执行复现步骤,发现此时在生命周期的恢复步骤,editText被重新赋值。
原因分析
这个bug由View的状态保存、恢复引起,所以要彻底解决它,我们需要弄明白
- View是如何保存、恢复状态的
- fragment在ViewPager里何时调用View的保存、恢复事件
View是如何保存、恢复状态的
系统提供的控件,都实现了自己的保存、恢复操作,定义在各自的onSaveInstanceState
和onRestoreInstanceState
/**
* 保存状态,返回一个序列化对象
*
* @return
*/
@Nullable
@Override
protected Parcelable onSaveInstanceState() {
return super.onSaveInstanceState();
}
/**
* 还原状态,根据序列化入参
*
* @param state
*/
@Override
protected void onRestoreInstanceState(Parcelable state) {
super.onRestoreInstanceState(state);
}
这俩函数解决了View如何把当前的状态序列化成对象;以及根据序列化对象来恢复当前状态。
那么这个序列化对象是如何被赋值、使用的呢?我们主要关注以下两个函数
dispatchSaveInstanceState(...)
dispatchRestoreInstanceState(...)
dispatchSaveInstanceState
通过调用onSaveInstanceState
得到View序列化的状态,然后存到了入参中container.put(mID, state);
,key为控件ID。
protected void dispatchSaveInstanceState(SparseArray 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) {
// Log.i("View", "Freezing #" + Integer.toHexString(mID)
// + ": " + state);
container.put(mID, state);
}
}
}
从上文的if
语句我们也可得出,如果一个View需要保存状态,需要以下两个条件:
- 该View有着唯一的一个id。可通过
setId
或xml设置id,如果id不唯一,由于SparseArray
是以id为key保存状态的,那么相同的id的View的数据会覆盖。 - 该View的标志位不是
SAVE_DISABLED_MASK
。可通过setSaveEnabled(boolean)
方法来改变,默认是true,即可保存状态。
dispatchSaveInstanceState
方法在ViewGroup
里被重写,分发保存事件到每个子元素。
dispatchRestoreInstanceState
通过ID得到序列化对象,然后调用onRestoreInstanceState
恢复当前状态
protected void dispatchRestoreInstanceState(SparseArray container) {
if (mID != NO_ID) {
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()");
}
}
}
}
从上文的if
语句我们也可得出,如果一个View需要恢复状态,需要设置id。
onRestoreInstanceState
方法在ViewGroup
里被重写,分发恢复事件到每个子元素。
对外暴露函数
上述两个函数都是protect访问权限的,View提供了代理方法,暴露给外界调用
- dispatchSaveInstanceState
public void saveHierarchyState(SparseArray container) {
dispatchSaveInstanceState(container);
}
- dispatchRestoreInstanceState
public void restoreHierarchyState(SparseArray container) {
dispatchRestoreInstanceState(container);
}
序列化对象管理
本文场景(ViewPager使用Fragment)而言,FragmentManager
管理了Fragment状态的保存、恢复,并提供了SparseArray
变量保存序列化对象。
保存
下面函数,把mStateArray
变量作为参数,通知View保存状态,
然后把mStateArray`保存到Fragment的mSavedViewState变量上
//保存fragment里View的状态
void saveFragmentViewState(Fragment f) {
if (f.mInnerView == null) {
return;
}
if (mStateArray == null) {
mStateArray = new SparseArray();
} else {
mStateArray.clear();
}
f.mInnerView.saveHierarchyState(mStateArray);
if (mStateArray.size() > 0) {
f.mSavedViewState = mStateArray;
mStateArray = null;
}
}
恢复
由于上一步已经把mStateArray
赋值给Fragment的mSavedViewState
。因此下面函数看不到mStateArray
的使用
void moveToState(Fragment f, int newState, int transit, int transitionStyle, boolean keepActive) {
//...一堆代码
switch (f.mState) {
//....一堆代码
case Fragment.CREATED:
if (f.mView != null) {
f.restoreViewState(f.mSavedFragmentState);
}
break;
}
}
在Fragment的restoreViewState
里就调用了restoreHierarchyState
恢复对象
final void restoreViewState(Bundle savedInstanceState) {
if (mSavedViewState != null) {
mInnerView.restoreHierarchyState(mSavedViewState);
mSavedViewState = null;
}
mCalled = false;
onViewStateRestored(savedInstanceState);
if (!mCalled) {
throw new SuperNotCalledException("Fragment " + this
+ " did not call through to super.onViewStateRestored()");
}
}
小结
通过上面的分析,我们知道了
在保存的时候,View
通过onSaveInstanceState()
把自己的状态序列化,保存到FragmentManager
的mStateArray
变量上;
在恢复的时候,View
通过onRestoreInstanceState()
从Fragment
的mSavedViewState
变量里取值恢复。
需要注意的是,View
必须设置ID、标志位不是SAVE_DISABLED_MASK
系统何时调用View保存、恢复事件
我们可以通过给FragmentManager
的saveFragmentViewState()
和moveToState()
断点,来获取函数被执行的上下文环境。
保存
完整上下文
java.lang.Thread.State: RUNNABLE
at android.support.v4.app.FragmentManagerImpl.saveFragmentViewState(FragmentManager.java:2860)
at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1509)
at android.support.v4.app.FragmentManagerImpl.moveFragmentToExpectedState(FragmentManager.java:1759)
at android.support.v4.app.BackStackRecord.executeOps(BackStackRecord.java:792)
at android.support.v4.app.FragmentManagerImpl.executeOps(FragmentManager.java:2596)
at android.support.v4.app.FragmentManagerImpl.executeOpsTogether(FragmentManager.java:2383)
at android.support.v4.app.FragmentManagerImpl.removeRedundantOperationsAndExecute(FragmentManager.java:2338)
at android.support.v4.app.FragmentManagerImpl.execSingleAction(FragmentManager.java:2215)
at android.support.v4.app.BackStackRecord.commitNowAllowingStateLoss(BackStackRecord.java:649)
at android.support.v4.app.FragmentPagerAdapter.finishUpdate(FragmentPagerAdapter.java:145)
at android.support.v4.view.ViewPager.populate(ViewPager.java:1238)
at android.support.v4.view.ViewPager.populate(ViewPager.java:1086)
at android.support.v4.view.ViewPager$3.run(ViewPager.java:267)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:927)
at android.view.Choreographer.doCallbacks(Choreographer.java:702)
at android.view.Choreographer.doFrame(Choreographer.java:635)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:913)
at android.os.Handler.handleCallback(Handler.java:751)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6688)
at java.lang.reflect.Method.invoke(Method.java:-1)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1468)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1358)
恢复
完整上下文
at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1454)
at android.support.v4.app.FragmentManagerImpl.moveFragmentToExpectedState(FragmentManager.java:1759)
at android.support.v4.app.BackStackRecord.executeOps(BackStackRecord.java:792)
at android.support.v4.app.FragmentManagerImpl.executeOps(FragmentManager.java:2596)
at android.support.v4.app.FragmentManagerImpl.executeOpsTogether(FragmentManager.java:2383)
at android.support.v4.app.FragmentManagerImpl.removeRedundantOperationsAndExecute(FragmentManager.java:2338)
at android.support.v4.app.FragmentManagerImpl.execSingleAction(FragmentManager.java:2215)
at android.support.v4.app.BackStackRecord.commitNowAllowingStateLoss(BackStackRecord.java:649)
at android.support.v4.app.FragmentPagerAdapter.finishUpdate(FragmentPagerAdapter.java:145)
at android.support.v4.view.ViewPager.populate(ViewPager.java:1238)
at android.support.v4.view.ViewPager.populate(ViewPager.java:1086)
at android.support.v4.view.ViewPager$3.run(ViewPager.java:267)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:927)
at android.view.Choreographer.doCallbacks(Choreographer.java:702)
at android.view.Choreographer.doFrame(Choreographer.java:635)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:913)
at android.os.Handler.handleCallback(Handler.java:751)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6688)
at java.lang.reflect.Method.invoke(Method.java:-1)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1468)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1358)
延伸阅读
为何TextView也是相同ID,但没有这个bug
我们知道EditText
是继承于TextView
的,所以翻下源码发现默认是不去保存数据。
@Override
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
// Save state if we are forced to
final boolean freezesText = getFreezesText();
boolean hasSelection = false;
int start = -1;
int end = -1;
if (mText != null) {
start = getSelectionStart();
end = getSelectionEnd();
if (start >= 0 || end >= 0) {
// Or save state if there is a selection
hasSelection = true;
}
}
if (freezesText || hasSelection) {
//保存数据操作...
}
}
从上面的if条件,我们可以得出 要保存数据,必须满足下述变量为true
- freezesText
TextView默认false ,在EditText里被重写为true - hasSelection
TextView 默认false,因为getSelectionStart()
和getSelectionStart()
都是-1