編輯:關於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。
@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的相關數據。
android:background="@android:color/white"
android:clickable="true"
Fragment背景默認是透明的,所以我們能看到兩個Fragment重疊在一起。當我們為每個Fragment添加背景之後,即使兩個Fragment疊加在一起,我們也只看到一個。至於為什麼要設置clickable=”true”,是因為兩個Fragment疊加在一起,雖然我們只能看到上面那個,但是下面那個仍然能接收到事件。設置clickable=”true”時,上面的Fragment會攔截掉所有事件。
這樣處理,能從視覺上解決問題,但是Activity異常銷毀時,同一個Fragment同時出現兩個實例的客觀事實沒有改變。有時你會發現Fragment中的某個網絡接口明明應該只調用一次,Log卻打印調用兩次,其實是Fragment創建了兩個實例。
用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
不知道你有麼有發現,來自菜鳥的成長史:http://blog.csdn.net/zjbpku/article/details/25161131, KitKat之後的版本不
對話框就是一個AlertDialog,但是一個簡單的AlertDialog,我們卻可以將它玩出許多花樣來,下面我們就來一起總結一下AlertDialog的用法。看看各位童
在android上導入zxing.jar包,總是報錯:Could not find class com.google.zxing.MultiFormatWriter, r
傲不可長,欲不可縱,樂不可極,志不可滿。—— 魏 徵 本講內容:SD卡 上一講中我們學習了Android的數據存儲采用File,但是這樣的數據是存儲在應用程序內