Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Fragment重疊問題引發的思考

Fragment重疊問題引發的思考

編輯:關於Android編程

Fragment重疊問題相信很多開發者都遇到個這個問題,也解決個這個問題,前段時間偶然發現,公司項目偶然出現了Fragment重疊的Bug,心裡不由一緊,趕緊去stackoverflow搜索了一番,找到了好幾種解決方案,最終問題是解決了,不過心裡留下了很多疑問(為什麼會出現重疊?為什麼這麼處理之後可以解決問題?這樣寫會不會引發其他問題?),帶著我決定寫個Demo去分析下每種解決方法的原理以及可能帶來的負面影響。

一、問題重現

Demo是常見的用Fragment實現的Tab切換,拿了網上現成的Demo改了一下,先給個Fragment重疊的效果圖:

這裡寫圖片描述

在Fragment切換時,采用的show/hide的方式,原理是顯示某個Fragment時,先把其他幾個Fragment先隱藏掉:

private void setTabSelection(int index) {
        clearSelection();
        FragmentTransaction transaction = fragmentManager.beginTransaction();
        hideFragments(transaction);
        switch (index) {
            case 0:
                messageImage.setImageResource(R.drawable.message_selected);
                messageText.setTextColor(Color.WHITE);
                if (messageFragment == null) {
                    messageFragment = new NormalListFragment();
                    transaction.add(R.id.content, messageFragment);
                } else {
                    transaction.show(messageFragment);
                }
                break;
            case 1:
                contactsImage.setImageResource(R.drawable.contacts_selected);
                contactsText.setTextColor(Color.WHITE);
                if (contactsFragment == null) {
                    contactsFragment = new ContactsFragment();
                    transaction.add(R.id.content, contactsFragment);
                } else {
                    transaction.show(contactsFragment);
                }
                break;
            case 2:
                newsImage.setImageResource(R.drawable.news_selected);
                newsText.setTextColor(Color.WHITE);
                if (newsFragment == null) {
                    newsFragment = new NewsFragment();
                    transaction.add(R.id.content, newsFragment);
                } else {
                    transaction.show(newsFragment);
                }
                break;
            case 3:
            default:
                settingImage.setImageResource(R.drawable.setting_selected);
                settingText.setTextColor(Color.WHITE);
                if (settingFragment == null) {
                    settingFragment = new SettingFragment();
                    transaction.add(R.id.content, settingFragment);
                } else {
                    transaction.show(settingFragment);
                }
                break;
        }
        transaction.commit();
    }

由於Fragment重疊問題是發生在某種特定的情況下,所以在常規環境下很難復現,所以需要在Android 手機開發者選項中把不保留活動這個選項打開,這樣每次進入新的Activity,舊的Activity就會馬上銷毀。

這裡寫圖片描述

問題分析

假設現在處在第一個Tab 圖片列表(NormalListFragment),然後點擊某個Item進入詳情頁,由於不保留活動,Fragment所在的Activity會銷毀掉。然後,我們從詳情頁返回到圖片列表,Activity會重建,Fragment會重新綁定, 整個過程Activity和Fragment的生命周期方法調用Log如下:

06-18 21:35:36.479 3004-3004/com.jx.androiddemos I/FragmentLearn: NormalListFragment  onPause
06-18 21:35:36.479 3004-3004/com.jx.androiddemos I/FragmentLearn: FragmentMainActivity  onPause
06-18 21:35:36.893 3004-3004/com.jx.androiddemos I/FragmentLearn: FragmentMainActivity  onSaveInstanceState   Bundle[{android:viewHierarchyState=Bundle[{android:views={16908290=android.view.AbsSavedState$1@6962f3e, 2131624006=android.view.AbsSavedState$1@6962f3e, 2131624007=android.view.AbsSavedState$1@6962f3e, 2131624008=android.support.v7.widget.Toolbar$SavedState@bda98cc, 2131624009=android.view.AbsSavedState$1@6962f3e, 2131624052=android.view.AbsSavedState$1@6962f3e, 2131624053=android.view.AbsSavedState$1@6962f3e, 2131624054=android.view.AbsSavedState$1@6962f3e, 2131624055=android.view.AbsSavedState$1@6962f3e, 2131624056=android.view.AbsSavedState$1@6962f3e, 2131624057=android.view.AbsSavedState$1@6962f3e, 2131624058=android.view.AbsSavedState$1@6962f3e, 2131624059=android.view.AbsSavedState$1@6962f3e, 2131624060=android.view.AbsSavedState$1@6962f3e, 2131624061=android.view.AbsSavedState$1@6962f3e, 2131624062=android.view.AbsSavedState$1@6962f3e, 2131624063=android.view.AbsSavedState$1@6962f3e, 2131624064=android.view.AbsSavedState$1@6962f3e}, android:focusedViewId=2131624102}], android:support:fragments=android.support.v4.app.FragmentManagerState@696715}]
06-18 21:35:36.893 3004-3004/com.jx.androiddemos I/FragmentLearn: NormalListFragment  onStop
06-18 21:35:36.893 3004-3004/com.jx.androiddemos I/FragmentLearn: FragmentMainActivity  onStop
06-18 21:35:36.910 3004-3004/com.jx.androiddemos I/FragmentLearn: NormalListFragment  onDestroyView
06-18 21:35:36.914 3004-3004/com.jx.androiddemos I/FragmentLearn: NormalListFragment  onDestroy
06-18 21:35:36.914 3004-3004/com.jx.androiddemos I/FragmentLearn: NormalListFragment  onDetach
06-18 21:35:36.914 3004-3004/com.jx.androiddemos I/FragmentLearn: FragmentMainActivity  onDestroy
06-18 21:35:39.527 3004-3004/com.jx.androiddemos I/FragmentLearn: NormalListFragment  onAttach
06-18 21:35:39.527 3004-3004/com.jx.androiddemos I/FragmentLearn: NormalListFragment  onCreate
06-18 21:35:39.527 3004-3004/com.jx.androiddemos I/FragmentLearn: FragmentMainActivity   onCreate   Bundle[{android:viewHierarchyState=Bundle[mParcelledData.dataSize=1512], android:support:fragments=android.support.v4.app.FragmentManagerState@e7e75b8}]
06-18 21:35:39.666 3004-3004/com.jx.androiddemos I/FragmentLearn: NormalListFragment   onCreateView   Bundle[{android:view_state={2131624102=AbsListView.SavedState{402bf7e selectedId=-9223372036854775808 firstId=-1 viewTop=0 position=0 height=1557 filter=null checkState=null}}}]
06-18 21:35:39.672 3004-3004/com.jx.androiddemos I/FragmentLearn: NormalListFragment   onActivityCreated   Bundle[{android:view_state={2131624102=AbsListView.SavedState{402bf7e selectedId=-9223372036854775808 firstId=-1 viewTop=0 position=0 height=1557 filter=null checkState=null}}}]
06-18 21:35:39.672 3004-3004/com.jx.androiddemos I/FragmentLearn: NormalListFragment  onAttach
06-18 21:35:39.672 3004-3004/com.jx.androiddemos I/FragmentLearn: NormalListFragment  onCreate
06-18 21:35:39.673 3004-3004/com.jx.androiddemos I/FragmentLearn: NormalListFragment   onCreateView   null
06-18 21:35:39.675 3004-3004/com.jx.androiddemos I/FragmentLearn: NormalListFragment   onActivityCreated   null
06-18 21:35:39.676 3004-3004/com.jx.androiddemos I/FragmentLearn: NormalListFragment  onStart
06-18 21:35:39.676 3004-3004/com.jx.androiddemos I/FragmentLearn: NormalListFragment  onStart
06-18 21:35:39.676 3004-3004/com.jx.androiddemos I/FragmentLearn: FragmentMainActivity  onStart
06-18 21:35:39.679 3004-3004/com.jx.androiddemos I/FragmentLearn: FragmentMainActivity  onRestoreInstanceState  Bundle[{android:viewHierarchyState=Bundle[{android:views={16908290=android.view.AbsSavedState$1@6962f3e, 2131624006=android.view.AbsSavedState$1@6962f3e, 2131624007=android.view.AbsSavedState$1@6962f3e, 2131624008=android.support.v7.widget.Toolbar$SavedState@80d4056, 2131624009=android.view.AbsSavedState$1@6962f3e, 2131624052=android.view.AbsSavedState$1@6962f3e, 2131624053=android.view.AbsSavedState$1@6962f3e, 2131624054=android.view.AbsSavedState$1@6962f3e, 2131624055=android.view.AbsSavedState$1@6962f3e, 2131624056=android.view.AbsSavedState$1@6962f3e, 2131624057=android.view.AbsSavedState$1@6962f3e, 2131624058=android.view.AbsSavedState$1@6962f3e, 2131624059=android.view.AbsSavedState$1@6962f3e, 2131624060=android.view.AbsSavedState$1@6962f3e, 2131624061=android.view.AbsSavedState$1@6962f3e, 2131624062=android.view.AbsSavedState$1@6962f3e, 2131624063=android.view.AbsSavedState$1@6962f3e, 2131624064=android.view.AbsSavedState$1@6962f3e}, android:focusedViewId=2131624102}], android:support:fragments=android.support.v4.app.FragmentManagerState@e7e75b8}]
06-18 21:35:39.680 3004-3004/com.jx.androiddemos I/FragmentLearn: FragmentMainActivity  onResume
06-18 21:35:39.680 3004-3004/com.jx.androiddemos I/FragmentLearn: NormalListFragment  onResume
06-18 21:35:39.680 3004-3004/com.jx.androiddemos I/FragmentLearn: NormalListFragment  onResume

從上面的Log可以發現,重新創建Activity時,NormalListFragment每個周期方法都走了兩遍。這意味著同時創建了兩個NormalListFragment實例,這個兩個NormalListFragment一個是我代碼裡面主動創建的,另外一個則是上次Activity異常銷毀時保存的,因為恢復的這個Fragment沒有拿到引用,所以無法去做操作的(隱藏顯示),這意味著我切換到其他tab時,這個Fragment會一直顯示,這正是Fragment重疊問題的根源所在。

下面從源碼角度證實Activity在異常情況下銷毀時,會保存Fragment。

從Log第三行打印的Bundle的值我們可以發現,Activity在異常銷毀時會調用onSaveInstanceState方法,系統會默認保存一些數據,包括Fragment

06-18 21:35:36.893 3004-3004/com.jx.androiddemos I/FragmentLearn: FragmentMainActivity  onSaveInstanceState   Bundle[{android:viewHierarchyState=Bundle[{android:views={16908290=android.view.AbsSavedState$1@6962f3e, 2131624006=android.view.AbsSavedState$1@6962f3e, 2131624007=android.view.AbsSavedState$1@6962f3e, 2131624008=android.support.v7.widget.Toolbar$SavedState@bda98cc, 2131624009=android.view.AbsSavedState$1@6962f3e, 2131624052=android.view.AbsSavedState$1@6962f3e, 2131624053=android.view.AbsSavedState$1@6962f3e, 2131624054=android.view.AbsSavedState$1@6962f3e, 2131624055=android.view.AbsSavedState$1@6962f3e, 2131624056=android.view.AbsSavedState$1@6962f3e, 2131624057=android.view.AbsSavedState$1@6962f3e, 2131624058=android.view.AbsSavedState$1@6962f3e, 2131624059=android.view.AbsSavedState$1@6962f3e, 2131624060=android.view.AbsSavedState$1@6962f3e, 2131624061=android.view.AbsSavedState$1@6962f3e, 2131624062=android.view.AbsSavedState$1@6962f3e, 2131624063=android.view.AbsSavedState$1@6962f3e, 2131624064=android.view.AbsSavedState$1@6962f3e}, android:focusedViewId=2131624102}], android:support:fragments=android.support.v4.app.FragmentManagerState@696715}]

這個我們從Activity源碼中的onSaveInstanceState方法也能夠確認:

 protected void onSaveInstanceState(Bundle outState) {
        outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState());
        Parcelable p = mFragments.saveAllState();
        if (p != null) {
            outState.putParcelable(FRAGMENTS_TAG, p);
        }
        getApplication().dispatchActivitySaveInstanceState(this, outState);
    }

在onSaveInstanceState方法中,首先調用mFragments(FragmentManager)的saveAllState方法把Fragment數據保存到Parcelable 變量中,然後通過調用outState.putParcelable(FRAGMENTS_TAG, p);
保存到Bundle 中,FRAGMENTS_TAG這個常量

static final String FRAGMENTS_TAG = "android:support:fragments";

也正是上面Log Bundle 數據中 鍵值對的一個鍵:

android:support:fragments=android.support.v4.app.FragmentManagerState@696715}

Activity異常銷毀時保存Fragment已經可以確認,那麼這個保存Fragment在重新創建Activity時,怎麼恢復的了?這就需要研究Activity的onCreate方法了

 protected void onCreate(@Nullable Bundle savedInstanceState) {
        if (DEBUG_LIFECYCLE) Slog.v(TAG, "onCreate " + this + ": " + savedInstanceState);
        if (mLastNonConfigurationInstances != null) {
            mAllLoaderManagers = mLastNonConfigurationInstances.loaders;
        }
        if (mActivityInfo.parentActivityName != null) {
            if (mActionBar == null) {
                mEnableDefaultActionBarUp = true;
            } else {
                mActionBar.setDefaultDisplayHomeAsUpEnabled(true);
            }
        }
        if (savedInstanceState != null) {
            Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
            mFragments.restoreAllState(p, mLastNonConfigurationInstances != null
                    ? mLastNonConfigurationInstances.fragments : null);
        }
        mFragments.dispatchCreate();
        getApplication().dispatchActivityCreated(this, savedInstanceState);
        if (mVoiceInteractor != null) {
            mVoiceInteractor.attachActivity(this);
        }
        mCalled = true;
    }

ActivityonCreate方法中和Fragment相關的應該是這幾句:

 if (savedInstanceState != null) {
            Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
            mFragments.restoreAllState(p, mLastNonConfigurationInstances != null
                    ? mLastNonConfigurationInstances.fragments : null);
        }
        mFragments.dispatchCreate();

當savedInstanceState 不為空時,意味著Activity上次銷毀時保存了數據,會掉用FragmentManager 的 restoreAllState 方法,這個方法比較長,就不貼出來了,這個方法主要作用就是從savedInstanceState 把保存的Fragment都取出來,實例化,綁定到當前Activity。
通過上面的分析,對Fragment的保存和恢復應該有了比較清楚的理解,也找到的Fragment重疊的根源所在,那麼下一步就是如何解決問題。

解決方法

我在網上找到3中比較有代表性的解決方法,當然,實際可能有更多的解決方法,但每種方法的核心思想都是一樣的,就是如何處理Activity異常銷毀時保存的Fragment。

方法一、重寫 onSaveInstanceState方法

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ......

    if (savedInstanceState != null) {
        mCustomVariable = savedInstanceState.getInt("variable", 0);
    }
}

@Override
protected void onSaveInstanceState(Bundle outState) {
    //super.onSaveInstanceState(outState);
    outState.putInt("variable", mCustomVariable);
}

這樣重寫之後,相當於不會調用Activity的onSaveInstanceState的方法保存系統默認數據,只保存自己需要的數據。Activity異常銷毀時不會保存Fragment,當然也就不會再有重疊的問題出現。
不過這樣處理是可能出現問題的,Activity的onSaveInstanceState方法不僅僅只是保存Fragment,還會保存獲取焦點的View的狀態,ActionBar,以及調用View的onSaveInstanceState 保存View的相關數據。

方法二、給每個Fragment根布局設置背景,攔截點擊事件

 android:background="@android:color/white"
 android:clickable="true"

Fragment背景默認是透明的,所以我們能看到兩個Fragment重疊在一起。當我們為每個Fragment添加背景之後,即使兩個Fragment疊加在一起,我們也只看到一個。至於為什麼要設置clickable=”true”,是因為兩個Fragment疊加在一起,雖然我們只能看到上面那個,但是下面那個仍然能接收到事件。設置clickable=”true”時,上面的Fragment會攔截掉所有事件。
這樣處理,能從視覺上解決問題,但是Activity異常銷毀時,同一個Fragment同時出現兩個實例的客觀事實沒有改變。有時你會發現Fragment中的某個網絡接口明明應該只調用一次,Log卻打印調用兩次,其實是Fragment創建了兩個實例。

方法三、用replace替代show/hide

用replace實現Tab切換的寫法:

private void setTabSelection2(int index) {
        clearSelection();
        FragmentTransaction transaction = fragmentManager.beginTransaction();
        switch (index) {
            case 0:
                messageImage.setImageResource(R.drawable.message_selected);
                messageText.setTextColor(Color.WHITE);
                if (messageFragment == null) {
                    messageFragment = new NormalListFragment();
                }
                transaction.replace(R.id.content, messageFragment);
                break;
            case 1:
                contactsImage.setImageResource(R.drawable.contacts_selected);
                contactsText.setTextColor(Color.WHITE);
                if (contactsFragment == null) {
                    contactsFragment = new ContactsFragment();
                }
                transaction.replace(R.id.content, contactsFragment);
                break;
            case 2:
                newsImage.setImageResource(R.drawable.news_selected);
                newsText.setTextColor(Color.WHITE);
                if (newsFragment == null) {
                    newsFragment = new NewsFragment();
                }
                transaction.replace(R.id.content, newsFragment);
                break;
            case 3:
            default:
                settingImage.setImageResource(R.drawable.setting_selected);
                settingText.setTextColor(Color.WHITE);
                if (settingFragment == null) {
                    settingFragment = new SettingFragment();
                }
                transaction.replace(R.id.content, settingFragment);
                break;
        }
        transaction.commit();
    }

replace的做法是每次把Activity(准確的說是Activity添加Fragment的布局)的Fragment先全部移除掉,再添加新的Fragment,這樣操作確保Activity的布局容器每次只會存在一個Fragment。當然不會出現重疊問題。
replace Fragment操作的源碼,很容易看出是先移除容器包含的Fragment,然後再添加:

 case OP_REPLACE: {
                    Fragment f = op.fragment;
                    if (mManager.mAdded != null) {
                        for (int i = 0; i < mManager.mAdded.size(); i++) {
                            Fragment old = mManager.mAdded.get(i);
                            if (FragmentManagerImpl.DEBUG) {
                                Log.v(TAG,
                                        "OP_REPLACE: adding=" + f + " old=" + old);
                            }
                            if (f == null || old.mContainerId == f.mContainerId) {
                                if (old == f) {
                                    op.fragment = f = null;
                                } else {
                                    if (op.removed == null) {
                                        op.removed = new ArrayList();
                                    }
                                    op.removed.add(old);
                                    old.mNextAnim = op.exitAnim;
                                    if (mAddToBackStack) {
                                        old.mBackStackNesting += 1;
                                        if (FragmentManagerImpl.DEBUG) {
                                            Log.v(TAG, "Bump nesting of "
                                                    + old + " to " + old.mBackStackNesting);
                                        }
                                    }
                                    mManager.removeFragment(old, mTransition, mTransitionStyle);
                                }
                            }
                        }
                    }
                    if (f != null) {
                        f.mNextAnim = op.enterAnim;
                        mManager.addFragment(f, false);
                    }
                }
                break;

當然,這樣處理後每次點擊Tab之後,每個Fragment都要重新創建實例,走周期方法,加載數據。具體能不能這樣做,還要看業務上允不允許每次重新加載數據。

參考:

http://stackoverflow.com/questions/16189088/overlapping-hidden-fragments-after-application-gets-killed-and-restored

http://stackoverflow.com/questions/18274732/android-fragments-overlapping-issue?answertab=active

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved