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 nonConfig)方法恢複Fragment。
2. FragmentManagerImpl的restoreAllState(Parcelable state, ArrayList nonConfig)方法
該方法會調用FragmentState的instantiate(Activity activity, Fragment parent)方法。
package android.app;final class FragmentManagerImpl extends FragmentManager implements LayoutInflater.Factory2 { ... 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; ... for (int i=0; i(); } if (DEBUG) Log.v(TAG, "restoreAllState: avail #" + i); mAvailIndices.add(i); } } ... }}
3. FragmentState的instantiate(Activity activity, Fragment parent)方法
最終會調用Fragment的靜態Factory 方法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的靜態Factory 方法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"。
package android.app;public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListener { private static final ArrayMap> sClassMap = new ArrayMap>(); 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。