一、什么是Fragment重叠?
二、什么情况下会发生Fragment重叠?
三、为什么会发生Fragment重叠?
1.重复replace/add Fragment
2.使用show()或hide()控制Fragment
四、跟"重叠"轻松Say Goodbye
1.不重复replace/add Fragment
2.show()或hide() Fragment不重叠
什么是Fragment重叠?
在使用Fragment过程中,在某些情况下可能会发现一直表现正常的Fragment,突然重叠了,其表现为几个Fragment的界面混合重叠在一起了。下面就是一种Fragment重叠异常表现:
(1)正常情况下的效果图:
(2)Fragment重叠:
由以下打印出的生命周期也可以看出,发生Fragment重叠时,Fragment产生了2个实例,导致弹了两个ToastDialog:
10-20 10:52:23.577 1616-1616/com.sankuai.meituan D/Toast: FlightSubmitOrderActivity onCreate()
10-20 10:52:23.781 1616-1616/com.sankuai.meituan D/Toast: FlightSubmitOrderFragment onViewCreated()
10-20 10:52:23.786 1616-1616/com.sankuai.meituan D/Toast: FlightSubmitOrderFragment showToastDialog()
10-20 10:52:23.864 1616-1616/com.sankuai.meituan D/Toast: FlightSubmitOrderFragment onViewCreated()
10-20 10:52:23.864 1616-1616/com.sankuai.meituan D/Toast: FlightSubmitOrderFragment showToastDialog()
什么情况下会发生Fragment重叠?
在“内存重启”后回到前台,页面发生销毁重建(旋转屏幕、内存不足等情况被强杀重启),如果没对页面重启后的Fragment状态做好处理,就容易发生Fragment重叠。
我们知道Activity中有个onSaveInstanceState()
方法,该方法在app进入后台、屏幕旋转前、跳转下一个Activity等情况下会被调用,此时系统帮我们保存一个Bundle类型的数据,我们可以根据自己的需求,手动保存一些例如播放进度等数据,而后如果发生了页面重启,我们可以在onRestoreInstanceState()
或onCreate()
里获取该数据,从而恢复播放进度等状态。下面是FragmentActivity的相关源码
public class FragmentActivity extends ... {
final FragmentController mFragments = FragmentController.createController(new HostCallbacks());
protected void onCreate(@Nullable Bundle savedInstanceState) {
...省略代码
if (savedInstanceState != null) {
Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
mFragments.restoreAllState(p, nc != null ? nc.fragments : null);
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
Parcelable p = mFragments.saveAllState();
...省略代码
}
}
从上可以看出,FragmentActivity确实是帮我们保存了Fragment的状态,并且在页面重启后会帮我们恢复。其中的mFragments是FragmentController,它是一个Controller,内部通过FragmentHostCallback间接控制FragmentManagerImpl。由于FragmentController是间接控制,没有详细保存Fragment状态的内容,所以我们直接看FragmentManagerImpl中的实现
final class FragmentManagerImpl extends FragmentManager {
Parcelable saveAllState() {
...省略 详细保存过程
FragmentManagerState fms = new FragmentManagerState();
fms.mActive = active;
fms.mAdded = added;
fms.mBackStack = backStack;
return fms;
}
void restoreAllState(Parcelable state, List<Fragment> nonConfig) {
// 恢复核心代码
FragmentManagerState fms = (FragmentManagerState)state;
FragmentState fs = fms.mActive[i];
if (fs != null) {
Fragment f = fs.instantiate(mHost, mParent);
}
}
}
我们通过saveAllState()
看到了关键的保存代码,原来是通过FragmentManagerState来保存Fragment的状态、所处Fragment栈下标、回退栈状态等,而在restoreAllState()恢复时,通过FragmentManagerState里的FragmentState的instantiate()方法恢复了Fragment。我们重点看FragmentState,Fragment的状态,类名、下标、id、Tag、ContainerId以及Arguments等数据就保存在里面。Fragment重叠的原因就与这个保存状态的机制有关!
为什么会发生Fragment重叠?
1.重复replace/add Fragment
我们知道加载Fragment有2种方式:replace()和add()。当发生内存重启时,比如屏幕发生旋转,Activity会重新启动,默认Activity中的Fragment也会跟着Activity重新创建,这样就造成了同一个Fragment会重复加载2次:
- 通过
onSaveInstanceState()
保存的Fragment会重新启动; - 当执行Activity的
onCreate()
时,又会再次实例化一个新的Fragment这就是出现重叠的原因。
由以上分析可知,一般情况下,我们会在Activity的onCreate()
里或者Fragment的onCreateView()
里加载根Fragment,如果在这里没有进行页面重启的判断的话,就可能导致重复加载Fragment引起重叠。
2.使用show()或hide()控制Fragment(源码support -v4 24.0.0以下)
由前面介绍可知,发生内存重启时,Fragment的状态会被保存在FragmentState中,但是在源码support-v4 24.0.0以下,FragmentState里没有mHidden
字段,默认情况下mHidden = false
。也就是说发生内存重启时,没有保存Fragment的显示状态,导致页面销毁重建后,Fragment就是默认情况下的show状态,Fragment一次性从栈底向栈顶顺序恢复时发生重叠。support-v424.0.0以下的FragmentState类源码如下,它实现了Parcelable,保存了Fragment的类名、下标、id、Tag、ContainerId以及Arguments等数据,但没有mHidden
字段(24.0.0及以上有该字段):
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;
...
// 在FragmentManagerImpl的restoreAllState()里被调用
public Fragment instantiate(FragmentHostCallback host, Fragment parent) {
...省略
mInstance = Fragment.instantiate(context, mClassName, mArguments);
}
}
注:show()
、hide()
最终是让Fragment的ViewsetVisibility(true或false)
,不会调用生命周期;当使用add()
+show()
,hide()
跳转新的Fragment时,旧的Fragment回调onHiddenChanged()
,不会回调onStop()
等生命周期方法,而新的Fragment在创建时是不会回调onHiddenChanged()
。
跟"重叠"轻松Say Goodbye
1.不重复replace/add Fragment
public class MyActivity ... {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
...
// 这里一定要在save为null时才加载Fragment,Fragment中onCreateView等生命周里加载根子Fragment同理
if(saveInstanceState == null){
// 正常情况下去 加载根Fragment
}
}
}
注:replace情况下,如果没有加入回退栈,则不判断也不会造成重叠;若加入回退栈,则也会造成重叠现象,建议统一判断下
2.show()
或hide()
Fragment不重叠(源码support -v4 24.0.0及以上不用考虑)
从源码角度的解决方案:从上面分析的原因,我们知道Fragment重叠的根本原因在于FragmentState没有保存Fragment的显示状态,即mHidden,那我们就自己手动在Fragment中维护一个mSupportHidden,在页面重启后,我们自己来决定Fragment是否显示。只需9行代码!(摘自:9行代码解决App内的Fragment重叠)
public class BaseFragment extends Fragment {
private static final String STATE_SAVE_IS_HIDDEN = "STATE_SAVE_IS_HIDDEN";
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
...
if (savedInstanceState != null) {
boolean isSupportHidden = savedInstanceState.getBoolean(STATE_SAVE_IS_HIDDEN);
FragmentTransaction ft = getFragmentManager().beginTransaction();
if (isSupportHidden) {
ft.hide(this);
} else {
ft.show(this);
}
ft.commit();
}
@Override
public void onSaveInstanceState(Bundle outState) {
...
outState.putBoolean(STATE_SAVE_IS_HIDDEN, isHidden());
}
}
注:在使用show()
、hide()
对多个Fragment的显示进行控制时,在不同场景下如何选择,用findFragmentByTag()
还是用getFragments()
恢复Fragment时(同时防止Fragment重叠),详细分析见Fragment全解析系列(二):正确的使用姿势。