注意,这里介绍的是 v4-24.0.0以下的版本出现的问题,在 v4-24.0.0+ 以后,官方修复了下面的问题。
情景再现
我们在使用 Fragment 时,都将它关联到 Activity 中。有时系统资源紧张我们的应用资源被回收,或者程序出现错误后系统重新加载页面,会出现界面中出现了 Fragment 重叠的异常现象。
分析原因
onSaveInstanceState() 保存机制
我们知道 Activity 中有个 onSaveInstanceState()
方法,该方法会在 Activity 将要被 kill 的时候回调(例如进入后台、屏幕旋转前、跳转下一个 Activity 等情况会被调用)。
此时系统帮我们保存一个 Bundle 类型的数据,我们可以根据自己的需求,手动保存一些例如播放进度等数据,而后如果发生页面重启,我们可以在 onRestoreInstanceState()
或 onCreate()
里获取保存的数据,如恢复播放进度等状态。
而产生 Fragment 重叠的原因就与这个保存状态机制有关。大致原因就是系统在页面重启前,帮我们保存了 Fragment 的状态,但是在重启恢复时,视图的可见状态没帮我们保存,而 Fragment 默认的是 show 状态,所以产生了 Fragment 重叠现象。
相关源码
FragmentActivity
在应用的 Activity 的父类 FragmentActivity 中
@SuppressWarnings("deprecation")
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
if (savedInstanceState != null) {
Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
mFragments.restoreAllState(p, nc != null ? nc.fragments : null);
...
/**
* Save all appropriate fragment state.
*/
@Override
protected void onSaveInstanceState(Bundle outState) {
Parcelable p = mFragments.saveAllState();
if (p != null) {
outState.putParcelable(FRAGMENTS_TAG, p);
}
...
FragmentManagerImpl
Parcelable saveAllState() {
...
FragmentManagerState fms = new FragmentManagerState();
fms.mActive = active;
fms.mAdded = added;
fms.mBackStack = backStack;
...
return fms;
}
void restoreAllState(Parcelable state, FragmentManagerNonConfig nonConfig) {
...
FragmentManagerState fms = (FragmentManagerState)state;
...
}
通过 saveAllState()
方法看到了关键的保存代码。而在 restoreAllState()
方法中,通过 FragmentManagerState 得到之前保存的数据。
FragmentState
final class FragmentState implements Parcelable {
final String mClassName;
final int mIndex;
final boolean mFromLayout;
final int mFragmentId;
final int mContainerId;
final String mTag;
final boolean mRetainInstance;
final boolean mDetached;
final Bundle mArguments;
// final boolean mHidden;
Bundle mSavedFragmentState;
Fragment mInstance;
...
public Fragment instantiate(FragmentHostCallback host, FragmentContainer container,
Fragment parent, FragmentManagerNonConfig childNonConfig) {
if (mInstance == null) {
final Context context = host.getContext();
if (mArguments != null) {
mArguments.setClassLoader(context.getClassLoader());
}
if (container != null) {
mInstance = container.instantiate(context, mClassName, mArguments);
} else {
mInstance = Fragment.instantiate(context, mClassName, mArguments);
}
if (mSavedFragmentState != null) {
mSavedFragmentState.setClassLoader(context.getClassLoader());
mInstance.mSavedFragmentState = mSavedFragmentState;
}
mInstance.setIndex(mIndex, parent);
mInstance.mFromLayout = mFromLayout;
mInstance.mRestored = true;
mInstance.mFragmentId = mFragmentId;
mInstance.mContainerId = mContainerId;
mInstance.mTag = mTag;
mInstance.mRetainInstance = mRetainInstance;
mInstance.mDetached = mDetached;
mInstance.mHidden = mHidden;
mInstance.mFragmentManager = host.mFragmentManager;
if (FragmentManagerImpl.DEBUG) Log.v(FragmentManagerImpl.TAG,
"Instantiated fragment " + mInstance);
}
mInstance.mChildNonConfig = childNonConfig;
return mInstance;
}
...
}
帮我们保存的 Fragment 最终以 FragmentState 形式存在。 这样在页面重启后, Activity 会自动根据上次保存的 Fragment 状态,显示之前的 Fragment。同时和当前要显示的 Fragment 发生重叠。
解决方法
方法一 findFragmentByTag
在 Activity 中通过 add()
或 replace()
方法添加 fragment 时,绑定一个 tag,一般我们用 fragment 的类名作为 tag,然后在发生内存回收而页面重载时,通过 findFragmentByTag()
找到对应的 Fragment,并 hide()
需要隐藏的 fragment。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity);
TargetFragment targetFragment;
HideFragment hideFragment;
if (savedInstanceState != null) { // “内存重启”时调用
targetFragment = getSupportFragmentManager().findFragmentByTag(TargetFragment.class.getName);
hideFragment = getSupportFragmentManager().findFragmentByTag(HideFragment.class.getName);
// 解决重叠问题
getFragmentManager().beginTransaction()
.show(targetFragment)
.hide(hideFragment)
.commit();
}else{ // 正常时
targetFragment = TargetFragment.newInstance();
hideFragment = HideFragment.newInstance();
getFragmentManager().beginTransaction()
.add(.container, targetFragment, targetFragment.getClass().getName())
.add(,container,hideFragment,hideFragment.getClass().getName())
.hide(hideFragment)
.commit();
}
}
如果想恢复到用户离开时的那个 Fragment 界面,需要在 onSaveInstanceState(Bundle outState)
方法中保存离开时的那个可见的 tag 或下标,在 onCreate(Bundle savedInstanceState)
中取出 tag/下标,进行恢复。
方法二 onAttachFragment(推荐)
重写 onAttachFragment,让新的 Fragment 指向了原本为被销毁的 fragment。
@Override
public void onAttachFragment(Fragment fragment) {
if (tab1 == null && fragment instanceof Tab1Fragment)
tab1 = fragment;
if (tab2 == null && fragment instanceof Tab2Fragment)
tab2 = fragment;
if (tab3 == null && fragment instanceof Tab3Fragment)
tab3 = fragment;
if (tab4 == null && fragment instanceof Tab4Fragment)
tab4 = fragment;
}