首页 > 代码库 > 浅析Fragment为什么需要Public的empty constructor

浅析Fragment为什么需要Public的empty constructor

最近,在做一个项目。当app启动后,然后使其进入后台进程(按home键),接着使用其它app(用其它app的目的是为了让系统内存不足,然后让系统将我们的app杀死)。当我们的app被系统杀死后,这时候通过任务管理点击我们的app进入应用。这时候问题出现了,app崩溃了,为了不暴露项目,一些项目包名或者类名的信息就省略了,下面就是异常的关键信息:

java.lang.RuntimeException: Unable to start activity 
ComponentInfo{省略}: 
android.support.v4.app.Fragment$InstantiationException: 
Unable to instantiate fragment 省略$3:
make sure class name exists, is public, and has an empty constructor that is
public
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:1750)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:1766)
at android.app.ActivityThread.handleRelaunchActivity(ActivityThread.java:2960)
at android.app.ActivityThread.access$1600(ActivityThread.java:127)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:945)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:130)
at android.app.ActivityThread.main(ActivityThread.java:3818)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:507)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:875)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:633)
at dalvik.system.NativeStart.main(Native Method)
Caused by: android.support.v4.app.Fragment$InstantiationException: 
Unable to instantiate fragment 省略$3: 
make sure class name exists, is public, and has an empty constructor that
is public
at android.support.v4.app.Fragment.instantiate(Fragment.java:399)
at android.support.v4.app.FragmentState.instantiate(Fragment.java:97)
at android.support.v4.app.FragmentManagerImpl.restoreAllState(FragmentManager.java:1760)
at android.support.v4.app.FragmentActivity.onCreate(FragmentActivity.java:200)
at 省略
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1047)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:1710)
... 12 more
Caused by: java.lang.InstantiationException: 
com.redacted.redacted.PreferenceListFragment$3
at java.lang.Class.newInstanceImpl(Native Method)
at java.lang.Class.newInstance(Class.java:1409)
at android.support.v4.app.Fragment.instantiate(Fragment.java:388)
... 18 more
从上面的关键信息可以看出,异常的原因就是因为使用的fragment没有public的empty constructor。事实,也确实如此,我的那个fragment有一个带参数的constructor但是i没有Public的empty constructor。

那么问题来了,为什么Fragment必须要empty constructor?

首先看到这个异常栈,就能发现问题是由于,fragment在还原状态中调用FragmentState#instantitae()->Fragment#instantitae()抛出异常,那么就顺着这个调用点,进入源代码。

首先,看下FragmentState对应的关键代码片

 public Fragment instantiate(FragmentActivity activity, Fragment parent) {
        if (mInstance != null) {
            return mInstance;
        }
        
        if (mArguments != null) {
            mArguments.setClassLoader(activity.getClassLoader());
        }
        
        mInstance = Fragment.instantiate(activity, mClassName, mArguments);
        
        if (mSavedFragmentState != null) {
            mSavedFragmentState.setClassLoader(activity.getClassLoader());
            mInstance.mSavedFragmentState = mSavedFragmentState;
        }
        mInstance.setIndex(mIndex, parent);
        mInstance.mFromLayout = mFromLayout;
        mInstance.mRestored = true;
        mInstance.mFragmentId = mFragmentId;
        mInstance.mContainerId = mContainerId;
        mInstance.mTag = mTag;
        mInstance.mRetainInstance = mRetainInstance;
        mInstance.mDetached = mDetached;
        mInstance.mFragmentManager = activity.mFragments;
        if (FragmentManagerImpl.DEBUG) Log.v(FragmentManagerImpl.TAG,
                "Instantiated fragment " + mInstance);
        return mInstance;
    }
很明显,在上述代码片中,一眼就找到了方法调用mInstance = Fragment.instantiate(activity, mClassName, mArguments),你猜的没错,它就是fragment的实例对象赋值。顺着这个点进入Fragment#instantiate(),

 /**
     * Create a new instance of a Fragment with the given class name.  This is
     * the same as calling its empty constructor.
     *
     * @param context The calling context being used to instantiate the fragment.
     * This is currently just used to get its ClassLoader.
     * @param fname The class name of the fragment to instantiate.
     * @param args Bundle of arguments to supply to the fragment, which it
     * can retrieve with {@link #getArguments()}.  May be null.
     * @return Returns a new fragment instance.
     * @throws InstantiationException If there is a failure in instantiating
     * the given fragment class.  This is a runtime exception; it is not
     * normally expected to happen.
     */
    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);
        }
    }
从它的catch语句块,也能看出,是不是与之前说的异常信息一模一样。异常抛出确实是来源于此,上述代码片的关键,其实就是通过java的反射机制进行实例化Fragment。好,即使对Java反射机制不太了解也没什么关系,顺着思路来。其中Fragment f = (Fragment)clazz.newInstance()就是关键中的关键,先来看下Class#newInstance()的java doc吧,
public T newInstance()
              throws InstantiationException,
                     IllegalAccessException
       Creates a new instance of the class represented by this Class object. The class is instantiated as if by a new expression with an empty argument list. The class is initialized if it has not already been initialized.
       Note that this method propagates any exception thrown by the nullary constructor, including a checked exception. Use of this method effectively bypasses the compile-time exception checking that would otherwise be performed by the compiler. The Constructor.newInstance method avoids this problem by wrapping any exception thrown by the constructor in a (checked) InvocationTargetException.

Returns:
       a newly allocated instance of the class represented by this object.
Throws:
       IllegalAccessException - if the class or its nullary constructor is not accessible.
       InstantiationException - if this Class represents an abstract class, an interface, an array class, a primitive type, or void; or if the class has no nullary constructor; or if the instantiation fails for some other reason.
       ExceptionInInitializerError - if the initialization provoked by this method fails.
       SecurityException - If a security manager, s, is present and any of the following conditions is met:
invocation of s.checkMemberAccess(this, Member.PUBLIC) denies creation of new instances of this class
the caller's class loader is not the same as or an ancestor of the class loader for the current class and invocation of s.checkPackageAccess() denies access to the package of this class

主要看下InstantiationException和IllegalAccessException,对于我们来说其关键信息就是:

  • InstantiationException,如果类没有empty constructor,该异常就会抛出。
  • IllegalAccessException,如果类没有public的empty constructor,该异常就会抛出。

至此,Fragment为什么需要Public的empty constructor,答案跃然于上。为了佐证上述分析,找到了Fragment API文档上的一段描述,

public Fragment ()

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 setArguments(Bundle) and later retrieved by the Fragment with 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 onAttach(Activity), the point where the fragment is actually associated with its activity. Some applications may also want to implement onInflate(Activity, AttributeSet, Bundle) to retrieve attributes from a layout resource, though should take care here because this happens for the fragment is attached to its activity.

浅析Fragment为什么需要Public的empty constructor