Activity 中的 Fragment 页面重叠异常

注意,这里介绍的是 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(R.id.container, targetFragment, targetFragment.getClass().getName())
                .add(R.id,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;
    }

参考:

https://blog.csdn.net/whitley_gong/article/details/51987911

https://www.jianshu.com/p/d9143a92ad94

文章只是作为个人记录学习使用,如有不妥之处请指正,谢谢。

猜你喜欢

转载自blog.csdn.net/modurookie/article/details/80746828