2019独角兽企业重金招聘Python工程师标准>>>
情景说明
Android开发中,如果存在多个Fragment,经常能遇到如下Fragment异常,意味着该fragment 被重复add。
java.lang.IllegalStateException: Fragment already added:xxxFragment
代码如下
public Fragment showFragment(int position, Bundle bundle) {
try {
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
String fragmentTag = createFragmentName(mViewContainer.getId(), position);
Fragment fragment = mFragmentManager.findFragmentByTag(fragmentTag);
if (fragment == null) {
fragment = instantiateItem(position);
fragment.setUserVisibleHint(false);
}
if (mCurrentPrimaryItem != fragment) { //防止重复add
if (mCurrentPrimaryItem != null) {
mCurTransaction.hide(mCurrentPrimaryItem);
mCurrentPrimaryItem.setUserVisibleHint(false);
}
if (fragment.isAdded()) {
mCurTransaction.show(fragment);
} else{
mCurTransaction.add(mViewContainer.getId(), fragment, fragmentTag);
}
mCurrentPrimaryItem = fragment;
}
if(bundle!=null)
setArgs(bundle);
if (!mCurrentPrimaryItem.getUserVisibleHint()) {
mCurrentPrimaryItem.setUserVisibleHint(true);
}
mCurTransaction.commit();
return fragment;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
思路分析
解决问题的前提往往是分析问题,那么这种问题是如何导致的呢?
在FragmentTransaction中,我们常用的是add和attach方法来添加fragment,这2个方法中的动作并不会立即执行,而是将OP任务加入了自己的队列。OP任务在等待commit系列方法提交事务之后执行,但同时commit 方法提交的任务加入到了主线程Looper中,如果Looper阻塞,add OP可能会延迟。
因此,这种情况下导致多次点击tab切换不相邻的fragment的时候if (mCurrentPrimaryItem != fragment) 条件可能成立,那么接下来就会调用isAdd()方法 ,但isAdd()方法可能返回的是false ,因为只有Fragment被真正add之后才返回true,但同时findFragmentByTag却能返回当前的fragment示例。
解决方法
方法一,commitNow系列
新版FragmentTransaction提供了commitNow方法,这个方法被调用之后,任务不会被加入主线Looper,可以立即执行
使用场景:解决数量较小(数量在4以内)和UI和Work相对简单Fragment的add问题,如果是复杂Fragment或者数量较多的Fragment被add,有可能导致卡顿、ANR问题.
mCurTransaction.commitNow();
此外,commitNow无法与addToBackStack并用,因为该方法内部使用了disallowAddToBackStack(),如果调用addToBackStack()会发生异常
方法二、使用Looper队列
commit把OP ADD任务加入到Looper队列中,并且是MainLooper队列,由于队列是先进先出的关系,因此,我们在OP ADD为完成之前,进行拦截。完成之后删除缓存
final Map pendingRequestManagerFragments =
new HashMap<>();
private BaseFragment getFragment(@NonNull FragmentManager fm,String FragmentTAG) {
BaseFragment current = (BaseFragment) fm.findFragmentByTag(FragmentTAG);
if (current == null) {
current = pendingRequestManagerFragments.get(fm); //如果存在,则表示OP ADD未完成
if (current == null) {
current = new MyBaseFragment();
pendingRequestManagerFragments.put(fm, current);
fm.beginTransaction().add(current, FRAGMENT_TAG).commitAllowingStateLoss();
handler.obtainMessage(ID_REMOVE_FRAGMENT_MANAGER, fm).sendToTarget();
}
}
return current;
}
private final Handler handler = new Handler(Looper.getMainLooper(),new Callback(){
@Override
public boolean handleMessage(Message message) {
boolean handled = true;
Object removed = null;
Object key = null;
switch (message.what) {
case ID_REMOVE_FRAGMENT_MANAGER:
android.app.FragmentManager fm = (android.app.FragmentManager) message.obj;
key = fm;
removed = pendingRequestManagerFragments.remove(fm);
break;
case ID_REMOVE_SUPPORT_FRAGMENT_MANAGER:
FragmentManager supportFm = (FragmentManager) message.obj;
key = supportFm;
removed = pendingSupportRequestManagerFragments.remove(supportFm);
break;
default:
handled = false;
break;
}
if (handled && removed == null && Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "Failed to remove expected request manager fragment, manager: " + key);
}
return handled;
}
});
使用场景:非常适合数量多,功能复杂的Fragment的事务操作
方法三、改造流程控制方式
fragment在FragmentTransaction的add(fragment) 方法被调用之后,会立即赋值fragment.mFragmentManager赋值为当前的FragmentManager,因此改造方式可以如下:
if (fragment.isAdded()) {
mCurTransaction.show(fragment);
} else if(fragment.getFragmentManager()!=null){
//防止fragment被多次加载
mCurTransaction.show(fragment);
}else{
mCurTransaction.add(mViewContainer.getId(), fragment, fragmentTag);
}
使用场景:比较适合fragment数量为2个的fragment切换
但是这种方式也存在一个问题,如果涉及频繁的show与hide的切换不同步问题,在MinLooper中OP hide之后可能OP show了。