最近使用Fragment时遇到的一个问题,记录下来。该问题出现的概率很小,也就是非必现。但是因为很多人也会这样使用Fragment,所以很可能你的项目中也隐藏着这样的一个bug,只是发生概率太小无法发现。
问题主要跟Activity的数据恢复有关,其可能产生的Exception:
android.support.v4.app.Fragment$InstantiationException: Unable to instantiate fragment com.example.fragmenttest.ExampleFragment: make sure class name exists, is public, and has an empty constructor that is public
1、重现问题
先来看看我简化的源码,超级简单:
public class MainActivity extends FragmentActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Fragment的使用
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
ExampleFragment fragment = new ExampleFragment(1);
fragmentTransaction.add(R.id.fragment_container, fragment);
fragmentTransaction.commit();
}
}
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/fragment_container">
LinearLayout>
public class ExampleFragment extends Fragment {
public ExampleFragment(int test) {
super();
Log.d("ExampleFragment", "test : " + test);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.example_fragment, container, false);
}
}
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center">
<TextView
android:text="Fragment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20dp"
/>
LinearLayout>
全部代码就上面那么多,十分简单,然后bug就在这么简短的代码里!!
为了使这个bug能够百分百重现,我们需要把手机开发者选项中的“Don’t keep activities”勾选上,如下图:
*勾选上这个开发功能的后果是,只要你离开某个Activity,即使是按Home键推出,这个Activity都将直接销毁掉,而不会先挂起在后台。如果不使用这个开发者功能,实际情况也有可能会有这种情况发生,例如我想到以下两种情况:
1、用户按Home键离开Activity,并且长时间不返回到该Activity,那么系统就会把这个Activity销毁掉,下次进入的时候再重新创建Activity的实例;
2、当手机内存消耗很大,后台挂起的Activity极有可能被销毁,那么下次进入该Activity的时候又会重新创建Activity的实例;*
这次的bug也是在以上的两种情况时发生,所以发生概率极低,勾选该开发者功能是为了让以上情况更容易发生,更容易发现问题。
那么要如何触发bug呢?很容易,只要跑起程序后,按home键退出(也有其他方法),然后再进入该程序就会报错,错误栈如下:
10-09 11:33:24.564: E/AndroidRuntime(17821): java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.fragmenttest/com.example.fragmenttest.MainActivity}: android.support.v4.app.Fragment$InstantiationException: Unable to instantiate fragment com.example.fragmenttest.ExampleFragment: make sure class name exists, is public, and has an empty constructor that is public
10-09 11:33:24.564: E/AndroidRuntime(17821): at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2198)
10-09 11:33:24.564: E/AndroidRuntime(17821): at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2257)
10-09 11:33:24.564: E/AndroidRuntime(17821): at android.app.ActivityThread.access$800(ActivityThread.java:139)
10-09 11:33:24.564: E/AndroidRuntime(17821): at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1210)
10-09 11:33:24.564: E/AndroidRuntime(17821): at android.os.Handler.dispatchMessage(Handler.java:102)
10-09 11:33:24.564: E/AndroidRuntime(17821): at android.os.Looper.loop(Looper.java:136)
10-09 11:33:24.564: E/AndroidRuntime(17821): at android.app.ActivityThread.main(ActivityThread.java:5086)
10-09 11:33:24.564: E/AndroidRuntime(17821): at java.lang.reflect.Method.invokeNative(Native Method)
10-09 11:33:24.564: E/AndroidRuntime(17821): at java.lang.reflect.Method.invoke(Method.java:515)
10-09 11:33:24.564: E/AndroidRuntime(17821): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:785)
10-09 11:33:24.564: E/AndroidRuntime(17821): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:601)
10-09 11:33:24.564: E/AndroidRuntime(17821): at dalvik.system.NativeStart.main(Native Method)
10-09 11:33:24.564: E/AndroidRuntime(17821): Caused by: android.support.v4.app.Fragment$InstantiationException: Unable to instantiate fragment com.example.fragmenttest.ExampleFragment: make sure class name exists, is public, and has an empty constructor that is public
10-09 11:33:24.564: E/AndroidRuntime(17821): at android.support.v4.app.Fragment.instantiate(Fragment.java:415)
10-09 11:33:24.564: E/AndroidRuntime(17821): at android.support.v4.app.FragmentState.instantiate(Fragment.java:99)
10-09 11:33:24.564: E/AndroidRuntime(17821): at android.support.v4.app.FragmentManagerImpl.restoreAllState(FragmentManager.java:1807)
10-09 11:33:24.564: E/AndroidRuntime(17821): at android.support.v4.app.FragmentActivity.onCreate(FragmentActivity.java:214)
10-09 11:33:24.564: E/AndroidRuntime(17821): at com.example.fragmenttest.MainActivity.onCreate(MainActivity.java:11)
10-09 11:33:24.564: E/AndroidRuntime(17821): at android.app.Activity.performCreate(Activity.java:5248)
10-09 11:33:24.564: E/AndroidRuntime(17821): at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1110)
10-09 11:33:24.564: E/AndroidRuntime(17821): at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2162)
10-09 11:33:24.564: E/AndroidRuntime(17821): ... 11 more
10-09 11:33:24.564: E/AndroidRuntime(17821): Caused by: java.lang.InstantiationException: can't instantiate class com.example.fragmenttest.ExampleFragment; no empty constructor
10-09 11:33:24.564: E/AndroidRuntime(17821): at java.lang.Class.newInstanceImpl(Native Method)
10-09 11:33:24.564: E/AndroidRuntime(17821): at java.lang.Class.newInstance(Class.java:1208)
10-09 11:33:24.564: E/AndroidRuntime(17821): at android.support.v4.app.Fragment.instantiate(Fragment.java:404)
10-09 11:33:24.564: E/AndroidRuntime(17821): ... 18 more
2、找到问题源头
就几行代码,为啥会报错呢?现在我们找找原因。
先看到错误栈中,起报错的源头的是Activity的Oncreate方法,并且指向了这一行:
super.onCreate(savedInstanceState);
如果有留意的话,Activity的onCreate方法是有一个Bundle参数的,这个参数有什么作用呢?如果Activity在被系统杀死的时候,再回到Activity时,这个参数保存在被杀死之前Activity的状态,所以这个参数是用来恢复Activity的数据。
因为之前我们勾选了“Don’t keep activities”的选项来模拟Activity被系统杀死的情况,所以这里会触发到onCreate的参数savedInstanceState不为null,因为要进行恢复。
我们根据源码进去看看:
FragmentActivity.java:
@Override
protected void onCreate(Bundle savedInstanceState) {
mFragments.attachActivity(this, mContainer, null);
// Old versions of the platform didn't do this!
if (getLayoutInflater().getFactory() == null) {
getLayoutInflater().setFactory(this);
}
super.onCreate(savedInstanceState);
NonConfigurationInstances nc = (NonConfigurationInstances)
getLastNonConfigurationInstance();
if (nc != null) {
mAllLoaderManagers = nc.loaders;
}
if (savedInstanceState != null) {
Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
mFragments.restoreAllState(p, nc != null ? nc.fragments : null);
}
mFragments.dispatchCreate();
}
这里调用了mFragments.restoreAllState,mFragments是FragmentManager的实现,其源码FragmentManager.java:
void restoreAllState(Parcelable state, ArrayList nonConfig) {
// If there is no saved state at all, then there can not be
// any nonConfig fragments either, so that is that.
if (state == null)
return;
FragmentManagerState fms = (FragmentManagerState) state;
if (fms.mActive == null)
return;
// First re-attach any non-config instances we are retaining back
// to their saved state, so we don't try to instantiate them again.
...
// Build the full list of active fragments, instantiating them from
// their saved state.
mActive = new ArrayList(fms.mActive.length);
if (mAvailIndices != null) {
mAvailIndices.clear();
}
for (int i = 0; i < fms.mActive.length; i++) {
FragmentState fs = fms.mActive[i];
if (fs != null) {
Fragment f = fs.instantiate(mActivity, mParent);
if (DEBUG)
Log.v(TAG, "restoreAllState: active #" + i + ": " + f);
mActive.add(f);
// Now that the fragment is instantiated (or came from being
// retained above), clear mInstance in case we end up
// re-restoring
// from this FragmentState again.
fs.mInstance = null;
} else {
mActive.add(null);
if (mAvailIndices == null) {
mAvailIndices = new ArrayList();
}
if (DEBUG)
Log.v(TAG, "restoreAllState: avail #" + i);
mAvailIndices.add(i);
}
}
// Update the target of all retained fragments.
...
// Build the back stack.
...
我把主要代码显示了出来,这里可以看到,恢复Fragment的时候,会通过下面代码创建Fragment的实例:
Fragment f = fs.instantiate(mActivity, mParent);
再跟进去,Fragment.java:
public Fragment instantiate(FragmentActivity activity, Fragment parent) {
if (mInstance != null) {
return mInstance;
}
...
mInstance = Fragment.instantiate(activity, mClassName, mArguments);
...
return mInstance;
}
/**
* Create a new instance of a Fragment with the given class name. This is
* the same as calling its empty constructor.
*/
public static Fragment instantiate(Context context, String fname, Bundle args) {
try {
Class> clazz = sClassMap.get(fname);
if (clazz == null) {
// Class not found in the cache, see if it's real, and try to add it
clazz = context.getClassLoader().loadClass(fname);
sClassMap.put(fname, clazz);
}
Fragment f = (Fragment)clazz.newInstance();
if (args != null) {
args.setClassLoader(f.getClass().getClassLoader());
f.mArguments = args;
}
return f;
} catch (ClassNotFoundException e) {
throw new InstantiationException("Unable to instantiate fragment " + fname
+ ": make sure class name exists, is public, and has an"
+ " empty constructor that is public", e);
} catch (java.lang.InstantiationException e) {
throw new InstantiationException("Unable to instantiate fragment " + fname
+ ": make sure class name exists, is public, and has an"
+ " empty constructor that is public", e);
} catch (IllegalAccessException e) {
throw new InstantiationException("Unable to instantiate fragment " + fname
+ ": make sure class name exists, is public, and has an"
+ " empty constructor that is public", e);
}
}
从上面的代码可以看到,恢复Fragment的时候,是通过反射来创建Fragment的实例,从源码的注释可以看出这样创建实例等同于调用Fragment的无参数的构造方法!
终于找到问题的源头了,我那个ExampleFragment里重写了构造方法,导致这里恢复创建实例的时候,由于找不到无参的构造方法,所以反射报错了。那么是不是补上一个无参的构造方法,这个问题就能解决呢?答案是能解决一半,还会有其他不会报错的问题。
3、解决方法
上面为什么我说能解决一半呢?来回想一下整个流程,如果你加上一个无参的构造方法,那么代码会正常跑下来,也就是
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Fragment
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
ExampleFragment fragment = new ExampleFragment();
fragmentTransaction.add(R.id.fragment_container, fragment);
fragmentTransaction.commit();
}
上面代码会跑完整,我们已经知道super.onCreate(savedInstanceState);这里面如果进行数据恢复的时候,会恢复创建一个ExampleFragment的实例,但是创建之后,我们又调用了setContentView(R.layout.activity_main);以及下面的代码,这里又会重新创建ExampleFragment实例,并且执行后面的代码。这样就等于重复创建了,消耗内存,消耗资源。
那么要如何解决呢?
方法1:
按照标准来写恢复数据的,onSaveInstanceState、onRestoreInstanceState都是可能用到的方法,并且在onCreate里判断savedInstanceState:
if (savedInstanceState != null) {
// 进行数据恢复
}
这里要注意的是你要管理好你要恢复的数据,并且把数据及时传递给你的Fragment,否则也会因为数据问题而报错。
方法2:
无视数据的恢复,强行重新执行一遍代码。也就是:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(null);
setContentView(R.layout.activity_main);
// Fragment
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
ExampleFragment fragment = new ExampleFragment();
fragmentTransaction.add(R.id.fragment_container, fragment);
fragmentTransaction.commit();
}
super.onCreate传入null,那么就会忽略掉之前要恢复的数据。这样的方法简单粗暴,不过用户体验会稍差,因为之前用户的操作全部丢失了。举个例子:如果当前Activity是个注册页面,点击注册后跳转后另一个等待页面,因为特殊原因,原本后台挂起的注册Activity被系统杀掉,此时注册失败再次返回注册Activity时,用户之前所输入的所有信息都会丢失。
4、一些注意的地方:
/**
* Default constructor. Every fragment must have an
* empty constructor, so it can be instantiated when restoring its
* activity's state. It is strongly recommended that subclasses do not
* have other constructors with parameters, since these constructors
* will not be called when the fragment is re-instantiated; instead,
* arguments can be supplied by the caller with {@link #setArguments}
* and later retrieved by the Fragment with {@link #getArguments}.
*
* Applications should generally not implement a constructor. The
* first place application code an run where the fragment is ready to
* be used is in {@link #onAttach(Activity)}, the point where the fragment
* is actually associated with its activity. Some applications may also
* want to implement {@link #onInflate} to retrieve attributes from a
* layout resource, though should take care here because this happens for
* the fragment is attached to its activity.
*/
public Fragment() {
}
其实这里的注释,android官方也说得很清楚了,它让我们没事别写一个有参数构造方法给Fragment,并且一定要有一个无参数的构造方法,并且通过setArguments和getArguments来提供数据参数。