ActivityでのFragmentページ重複異常


なお、ここではv 4-24.0.0以下のバージョンで発生した問題を紹介し、v 4-24.0.0+以降、以下の問題を公式に修正しました.

情景再現


Fragmentを使用する場合、Activityに関連付けます.システムリソースが不足している場合、私たちのアプリケーションリソースが回収されたり、プログラムにエラーが発生した後、システムがページを再ロードしたりすると、インタフェースにFragmentが重なる異常現象が発生します.

原因を分析する


onSaveInstanceState()保存メカニズム


Activityには、ActivityがkillされるときにコールバックするonSaveInstanceState()メソッドがあることを知っています(たとえば、バックグラウンドに入る、画面が回転する前、次の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インタフェースに復元するには,離れたときの可視のtagまたは下付き文字をonSaveInstanceState(Bundle outState)メソッドに保存し,onCreate(Bundle savedInstanceState)でtag/下付き文字を取り出して復元する必要がある.

メソッド2 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
文章は個人記録として勉強するだけで、不適切な点があれば指摘してください.ありがとうございます.