[置顶] android.app.Fragment$InstantiationException的原因分析

1. Fragment$InstantiationException的原因分析

在编写Fragment类的代码时候,Android Lint有时会提示如下error:


Avoid not-default constructors in fragments: use a default constructor plus Fragment$setArguments(Bundle) instead

From the Fragment documentation:
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().

每个Fragment必须要有一个无参构造方法,这样该Fragment在Activity恢复状态的时候才可以被实例化。强烈建议,Fragment的子类不要有其他含参构造方法,因为这些构造方法在Fragment重新实例化时不会被调用。取而代之的方式是,通过setArguments(Bundle)设置参数,然后通过getArguments获得参数。


如果的Fragment没有无参构造方法,app在恢复Activity时(例如旋转设备),会出现crash。

 

Q: 为什么必须要有一个无参构造方法,而且含参构造方法在Fragment重新实例化时不会调用?

接下来分析一下Activity恢复状态的过程。

1. Activity的onCreate(Bundle savedInstanceState)的方法

package android.app;

public class Activity extends ContextThemeWrapper
        implements LayoutInflater.Factory2,
        Window.Callback, KeyEvent.Callback,
        OnCreateContextMenuListener, ComponentCallbacks2,
        Window.OnWindowDismissedCallback {

    final FragmentManagerImpl mFragments = new FragmentManagerImpl();

    protected void onCreate(@Nullable Bundle savedInstanceState) {
        ...
        if (savedInstanceState != null) {
            Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
            mFragments.restoreAllState(p, mLastNonConfigurationInstances != null
                    ? mLastNonConfigurationInstances.fragments : null);
        }
        mFragments.dispatchCreate();
        ...
    }
    ...
}
当savedInstanceState不为null的时候,会调用FragmentManagerImpl的restoreAllState(Parcelable state, ArrayList<Fragment> nonConfig)方法恢复Fragment。

2. FragmentManagerImpl的restoreAllState(Parcelable state, ArrayList<Fragment> nonConfig)方法

该方法会调用FragmentState的instantiate(Activity activity, Fragment parent)方法。

package android.app;

final class FragmentManagerImpl extends FragmentManager implements LayoutInflater.Factory2 {
    ...
    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;
        
        ...
        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);
            }
        }
        ...
    }
}
3. FragmentState的instantiate(Activity activity, Fragment parent)方法

最终会调用Fragment的静态工厂方法instantiate(Context context, String fname, @Nullable Bundle args)。

package android.app;

final class FragmentState implements Parcelable { 
    ...
    public Fragment instantiate(Activity 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;
    }
}

 
 4. Fragment的静态工厂方法instantiate(Context context, String fname, @Nullable Bundle args) 
 

Fragment的重新实例化是利用Java反射机制,并且调用的是Fragment的无参构造方法,所以这一步是不会调用其他有参构造方法的。若Fragment没有无参构造方法,则clazz.newInstance()会抛出InstantiationExecption,然后就会打印出常见的一个Exception,"Unable to instantiate fragment " + fname + ": make sure class name exists, is public, and has an" + " empty constructor that is public"。

</pre><p><pre name="code" class="java">package android.app;

public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListener { 
    private static final ArrayMap<String, Class<?>> sClassMap = new ArrayMap<String, Class<?>>(); 

    public static Fragment instantiate(Context context, String fname, @Nullable 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);
                if (!Fragment.class.isAssignableFrom(clazz)) {
                    throw new InstantiationException("Trying to instantiate a class " + fname
                            + " that is not a Fragment", new ClassCastException());
                }
                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);
        }
    }
}

Q: 实际中还会遇到另一种情况,该Fragment有无参构造方法,依然抛出了InstantiationException。这又是为什么呢?
当你的Fragment是作为的一个非静态内部类的时候,这个时候利用Java反射机制也是无法实例化该Fragment的。我在实际项目中就是将一个DialogFragment定义为Activity的内部类,导致了这个Exception。因为非静态内部类对象的实例化需要先实例化外部类对象,仅仅实例化非静态内部类必然抛出InstantiationException。

你可能感兴趣的:(java,android,Fragment,Instantiation)