FragmentTransaction.replace() 你不知道的坑
一、起源:
先看效果,在linearLayout中添加了4個Fragment,然後點選替換一次確替換了兩個Fragment,引發了我的研究興趣;
第一次啟動 點選一次 點選兩次 點選三次
程式碼很簡單 activity onCreate 方法中添加了4個Fragment
FragmentTransaction transaction =manager.beginTransaction();
transaction.add(R.id.content,fragment1,"a");
transaction.add(R.id.content,fragment1_2,"b");
transaction.add(R.id.content,fragment1_3,"c");
transaction.add(R.id.content,fragment1_4,"d");
transaction.commit();
replace 按鈕監聽事件中添加了如下程式碼
Fragment2 fragment2_1 =new
FragmentTransaction transaction=manager.beginTransaction();
transaction.replace(R.id.content,fragment2_1,"kk");
transaction.commit();
二、探究transaction.replace到底做了什麼
探究原始碼得知FragmentTransaction 物件是在FragmentManagerImpl 類中的beginTransaction()方法中產生的;
@Override
publicFragmentTransaction beginTransaction() {
returnnewBackStackRecord(this);
}
這才發現BackStackRecord產生的物件才是我們真正使用的FragmentTransaction,那BackStackRecord.replace() 方法究竟做了啥,讓我們一探究竟;
publicFragmentTransactionreplace(intcontainerViewId, Fragment fragment, String tag) {
doAddOp(containerViewId, fragment, tag,OP_REPLACE);
return this;
}
public FragmentTransactionadd(intcontainerViewId, Fragment fragment, String tag) {
doAddOp(containerViewId, fragment, tag,OP_ADD);
return this;
}
可以看到add和 replace 方法都沒有自己去處理而是交給doAddOp處理,doAddOp()簡化程式碼如下
privatevoiddoAddOp(intcontainerViewId, Fragment fragment, String tag,int opcmd){
fragment.mFragmentManager = mManager;
if (tag!=null) {
fragment.mTag = tag;
}
if(containerViewId != 0) {
fragment.mContainerId = fragment.mFragmentId =containerViewId;
}
Op op =new Op();
op.cmd =opcmd;
op.fragment =fragment;
addOp(op);
}
我們發現,add 和replace 方法都是進行了fragment物件的tag、mFragmentId、mContainerId的賦值,mContainerId是容器id,也就是說每一個fragment 都有保留它被新增的容器的id,也就是我們replace傳入的R.id,content;
再看看OP
static final class Op {
Op next;
Op prev;
int cmd;
Fragment fragment;
int enterAnim;
int exitAnim;
int popEnterAnim;
int popExitAnim;
ArrayList<Fragment> removed;
}
Op其實就是儲存我們處理動作的一個物件,經過一系列追蹤發現,最終在BackStackRecord.run()中處理了這個物件;具體的追蹤過程可以參考:https://zhuanlan.zhihu.com/p/20660984
處理原始碼如下:
switch (op.cmd) {
caseOP_ADD: {
Fragment f = op.fragment;
f.mNextAnim = op.enterAnim;
mManager.addFragment(f,false);
} break;
caseOP_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 =newArrayList<Fragment>();
}
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;
好了終於找到replace的真正處理之處,我們精練出關鍵程式碼再看看:
switch (op.cmd) {
caseOP_ADD: {
Fragment f = op.fragment;
mManager.addFragment(f,false);
} break;
caseOP_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 (f == null ||old.mContainerId == f.mContainerId) {
if (old== f) {
op.fragment = f =null;
} else {
mManager.removeFragment(old,mTransition,mTransitionStyle);
}
}
}
}
if (f !=null) {
mManager.addFragment(f,false);
}
} break;
可以看到
1、add方法就是呼叫了fragmentmanager的新增方法;
2、replace 則是先刪除fragmentmanager中所有已新增的fragment中,容器id與當前要新增的fragment的容器id相同的fragment;然後再添加當前fragment;
3、由於新增的時候都是在一個LinearLayout 中,那麼所有的 fragment的容器Id都是一樣的;
得出結論: replace 會刪除LinearLayout中所有fragment ,然後再新增傳入fragment物件;
好,問題來了,最開始的圖片點選第一次刪除的是fragment1和fragment1_3 ,第二次只刪除了fragment1_3,並沒有刪除全部,這又是為什麼;
帶著疑問的態度進行了一次除錯,在除錯中終於找到了原因,問題就在這段程式碼:
for (int i=0; i<mManager.mAdded.size(); i++) {
Fragment old = mManager.mAdded.get(i);
if (f ==null ||old.mContainerId == f.mContainerId) {
mManager.removeFragment(old,mTransition, mTransitionStyle);
}
}
mManager.mAdded 是一個ArrayList<Fragment> 列表,在遍歷的時候呼叫了mManager.removeFragment方法,而該方法呼叫了ArrayList的remove方法;
public void removeFragment(Fragmentfragment, int transition, inttransitionStyle) {
mAdded.remove(fragment);
}
也就是說在用for迴圈遍歷ArrayList列表的時候使用了remove;這是開始懷戀我們的Java老師的了,list遍歷列表要刪除元素我們要用iterator.remove();
For迴圈遍歷過程刪除會造成ArrayList.size()不斷變小,所以造成刪除不完全的問題;你是否也被坑過。。。
筆記建議 Android此處可以將 mManager.mAdded複製一份再遍歷,就不會有這個問題了(親測有效);
ArrayList<Fragment> list=new ArrayList<Fragment>(mManager.mAdded ) ;
for (int i=0; i<list.size();i++) {
Fragment old = list.get(i);
if (f ==null ||old.mContainerId == f.mContainerId) {
mManager.removeFragment(old,mTransition, mTransitionStyle);
}
}
三、總結
用於我們常常使用FrameLayout 做容器fragment都掩蓋了下面其他Fragment,大部分情況下看不到此類問題,看不到不表示不存在,筆者建議,遇到此類還是手動去呼叫remove+add方法,一定要使用replace()可以去修改原始碼,如果你不嫌麻煩的話。。。