今天跟一个bug花了差不多1个小时左右的时间.
现象是这样的:
同事在主程序中新加了一个模块,只要一运行这个新加的模块,再运行我的模块就会崩溃。如果不运行同时的模块,运行我的模块就不会崩溃。 堆栈信息:Fragment xxx did not create a view。我的模块会通过LayoutInflator去动态加载一个带fragment标签的界面,但是我的xxxFragment在onCreateView中返回了View啊。通过加Log日志发现,框架根本没有回调到xxxFragment的onCreateView函数里来,这就奇怪了。
调用以下代码会抛出 Fragment xxx did not create a view的异常:
View content = mContext.getLayoutInflater().inflate(
R.layout.transfer_search_popup, null);
R.layout.transfer_search_popup.xml 的源代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<fragment class="com.qvod.player.activity.transfer.TransferSearchFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:tag="TransferSearchFragment"/>
</LinearLayout>
TransferSearchFragment 的onCreateView是有返回View的,而且在不进入同事的模块下一切正常。
遂开始研究源代码: LayoutInflator 在 inflate 的时候,会调用到 FragmentActivity的onCreateView里面来。
Fragment fragment = id != View.NO_ID ? mFragments.findFragmentById(id) : null;
if (fragment == null && tag != null) {
fragment = mFragments.findFragmentByTag(tag);
}
if (fragment == null && containerId != View.NO_ID) {
fragment = mFragments.findFragmentById(containerId);
}
if (FragmentManagerImpl.DEBUG) Log.v(TAG, "onCreateView: id=0x"
+ Integer.toHexString(id) + " fname=" + fname
+ " existing=" + fragment);
if (fragment == null) {
fragment = Fragment.instantiate(this, fname);
fragment.mFromLayout = true;
fragment.mFragmentId = id != 0 ? id : containerId;
fragment.mContainerId = containerId;
fragment.mTag = tag;
fragment.mInLayout = true;
fragment.mFragmentManager = mFragments;
fragment.onInflate(this, attrs, fragment.mSavedFragmentState);
mFragments.addFragment(fragment, true);
} else if (fragment.mInLayout) {
// A fragment already exists and it is not one we restored from
// previous state.
throw new IllegalArgumentException(attrs.getPositionDescription()
+ ": Duplicate id 0x" + Integer.toHexString(id)
+ ", tag " + tag + ", or parent id 0x" + Integer.toHexString(containerId)
+ " with another fragment for " + fname);
} else {
// This fragment was retained from a previous instance; get it
// going now.
fragment.mInLayout = true;
// If this fragment is newly instantiated (either right now, or
// from last saved state), then give it the attributes to
// initialize itself.
if (!fragment.mRetaining) {
fragment.onInflate(this, attrs, fragment.mSavedFragmentState);
}
mFragments.moveToState(fragment);
}
if (fragment.mView == null) {
throw new IllegalStateException("Fragment " + fname
+ " did not create a view.");
}
以上是onCreateView的关键代码。可以看到当判断到 mView == null 时便会抛出这个异常。
在onCreateView 中,会首先会根据id和tag 从mFragments中获取到之前的Fragment,如果有就直接返回,如果没有就通过Fragment.instantiate(this, fname)来创建一个。
因为在崩溃的过程中,框架没有回调到xxxFragment的onCreateView,但是却走到了判断 mView == null 的这步,说明在第一次初始化xxxFragment的时候,下面的代码却给fragment赋值了。可是奇怪的时,xxxFragment是第一次创建啊,怎么会已经存在了他的实例呢? 难道是框架把其他的Fragment实例返回给我了?
Fragment fragment = id != View.NO_ID ? mFragments.findFragmentById(id) : null;
if (fragment == null && tag != null) {
fragment = mFragments.findFragmentByTag(tag);
}
if (fragment == null && containerId != View.NO_ID) {
fragment = mFragments.findFragmentById(containerId);
}
检查同事的模块发现他在初始化的时候会调用下面的代码,添加一个Fragment到mFragments里去。
mRetainFragment = new RetainFragment();
fm.beginTransaction().add(mRetainFragment, TAG).commitAllowingStateLoss();
回头在看我的R.layout.transfer_search_popup.xml文件,发现我的fragment没有指定id. 再回头看看这句代码:
Fragment fragment = id != View.NO_ID ? mFragments.findFragmentById(id) : null;
View.NO_ID 的值是-1,同事添加的Fragment没有给id,那么其Fragment id为0,那么上面这句代码就变成了:
Fragment fragment = 0 != -1 ? mFragments.findFragmentById(0) : null;
恍然大悟了吧。我inflate我的layout,调用到FragmentActivity的onCreateView的时候,却把同事的另外的Fragment返回给我了。
这是FragmentActivity的bug,他使用了VIEW.NO_ID这个常量去判断,但是默认的Fragment id却是0.
解决办法很简单,在下面这句代码里加一个id就可以了,这样就不会使用默认的id 0 了
mRetainFragment = new RetainFragment();
fm.beginTransaction().add(xxxID, mRetainFragment, TAG).commitAllowingStateLoss();