Fragment恢复时注意事项:InstantiationException,别在Fragment写有参数的构造方法

最近使用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、重现问题
先来看看我简化的源码,超级简单:

  • 主MainActivity,继承了FragmentActivity
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();
    }
}
  • MainActivity的布局activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<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>
  • ExampleFragment.java,其布局里只有一个TextView,然后重写了构造方法
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);
    }
}
  • ExampleFragment的布局example_fragment.xml
<?xml version="1.0" encoding="utf-8"?>
<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”勾选上,如下图:
勾选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<Fragment> 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<Fragment>(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<Integer>();
                }
                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、一些注意的地方:

  • 来看看Fragment的源码,里面默认有个无参数的构造方法,这个方法有很长的一段注释,Fragment.java:
    /**
     * Default constructor.  <strong>Every</strong> 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}.
     * 
     * <p>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来提供数据参数。

  • 细节很重要
    这次这个bug是很难出现的bug,因为后台Activity被系统杀死的情况,所以是个极低概率的bug。但这不代表我们可以忽略这个bug,反而正正因为这么低概率才要更加重视,这种非必现的bug可遇不可求。可能1000个用户里也不会有一个用户出现这个bug,但是一旦用户数量级上去了,这个bug出现的个数就会越来越多。细节很重要!对于产品如此,对于代码也如此。细节很重要!细节很重要!细节很重要!
  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
这个异常通常是由于Fragment类没有默认的构造函数而引起的。当你在创建Fragment实例,系统会调用Fragment的默认构造函数进行实例化。如果你的Fragment类中没有默认构造函数,就会抛出上述异常。 为了解决这个问题,有以下两种方法: 1. 在Fragment类中添加默认构造函数 你可以在Fragment类中手动添加一个无参构造函数,如下所示: ```kotlin class MainFragment : Fragment() { // 添加一个无参构造函数 constructor() // 其他代码 } ``` 这样就可以保证系统在创建Fragment实例能够正常地进行实例化。 2. 使用newInstance方法传递参数 如果你的Fragment需要传递参数,可以使用静态的newInstance方法来创建Fragment实例,并在newInstance方法中传递参数。例如: ```kotlin class MainFragment : Fragment() { companion object { fun newInstance(param1: String, param2: Int): MainFragment { val fragment = MainFragment() val args = Bundle() args.putString("param1", param1) args.putInt("param2", param2) fragment.arguments = args return fragment } } // 其他代码 } ``` 在上述代码中,我们添加了一个静态的newInstance方法,该方法接收两个参数,将这些参数存储在Bundle中,并将Bundle设置为Fragment的arguments属性。在创建Fragment实例,我们可以使用该静态方法来传递参数,例如: ```kotlin val fragment = MainFragment.newInstance("Hello", 123) ``` 这样就可以保证系统在创建Fragment实例能够正常地进行实例化,并且能够传递参数

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值