問題
最近在線上bugly看到一個近兩個版本的出現(xiàn)的一個bug:
#5712 java.lang.NoSuchMethodException
<init> []
com.jess.arms.a.c.onCreate(BaseActivity.java:86)
java.lang.RuntimeException:Unable to start activity
ComponentInfo{xxxx.VehicleDetailActivity}:
android.support.v4.app.Fragment$InstantiationException:
Unable to instantiate fragment xxxx.mvp.ui.fragment.c:
could not find Fragment constructor
這個bug是方法找不到異常,因為項目中用了mvparms框架瞧筛,剛開始看到異常指向他的BaseActivity類,還以為是框架出了問題导盅。但是后面注意到是項目的VehicleDetailActivity報的could not find Fragment constructor较幌,即fragment構造器找不到的異常。于是馬上將問題定位到了該類新添加的一個DialogFragment上白翻,先看一下這個類的構造方法:
public MorePlanFragment(String modelId, HashMap<String, Integer> map) {
this.modelId = modelId;
this.posMap = map;
this.tabPos = map.get(POS_MAP_TAB);
}
這個MorePlanFragment使用了有參構造函數(shù)乍炉,而問題就出現(xiàn)在這里。
分析
既然報的找不到構造方法的錯誤滤馍,我們先來看一下Fragment的構造函數(shù):
/**
* 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. Prefer
* {@link #onAttach(Context)} instead. It is the first place application code can run where
* the fragment is ready to be used - the point where the fragment is actually associated with
* its context. Some applications may also want to implement {@link #onInflate} to retrieve
* attributes from a layout resource, although note this happens when the fragment is attached.
*/
public Fragment() {
}
構造函數(shù)上有一段注釋:
默認構造器岛琼。
每一個Fragment必須有一個無參的構造函數(shù),以便當Activity恢復狀態(tài)時fragment可以實例化巢株。
強烈建議fragment的子類不要有其他的有參構造函數(shù)槐瑞,因為當fragment重新實例化時不會調用這些有參構造函數(shù);
如果要傳值應該使用setArguments方法阁苞,在需要獲取這些值時調用getArguments方法困檩。
這一段注釋明確的告訴我們使用有參構造函數(shù)會出問題,建議使用無參構造函數(shù)那槽,但是并沒有告訴我們具體是哪里的問題悼沿。我們在Fragment中搜索could not find Fragment constructor這個異常,發(fā)現(xiàn)是在instantiate方法中拋出的骚灸。
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);
sClassMap.put(fname, clazz);
}
Fragment f = (Fragment) clazz.getConstructor().newInstance();
if (args != null) {
args.setClassLoader(f.getClass().getClassLoader());
f.setArguments(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 (NoSuchMethodException e) {
throw new InstantiationException("Unable to instantiate fragment " + fname
+ ": could not find Fragment constructor", e);
} catch (InvocationTargetException e) {
throw new InstantiationException("Unable to instantiate fragment " + fname
+ ": calling Fragment constructor caused an exception", e);
}
}
看上面的代碼我們可以知道糟趾,F(xiàn)ragment的實例化是通過調用類對象的getConstructor()方法獲取構造器對象并調用其newInstance()方法創(chuàng)建對象的。此時還會將args參數(shù)設置給Fragment∩跎現(xiàn)在找到了具體報錯的地方义郑,但是這個方法是在哪里調用觸發(fā)的呢?在Fragment沒有找到調用的地方鳖藕,由于Fragment是由FragmentManager管理的魔慷,在該類發(fā)現(xiàn)是在restoreAllState方法中調用的伺通。
void restoreAllState(Parcelable state, FragmentManagerNonConfig nonConfig) {
// Build the full list of active fragments, instantiating them from
// their saved state.
mActive = new SparseArray<>(fms.mActive.length);
for (int i=0; i<fms.mActive.length; i++) {
FragmentState fs = fms.mActive[i];
if (fs != null) {
FragmentManagerNonConfig childNonConfig = null;
if (childNonConfigs != null && i < childNonConfigs.size()) {
childNonConfig = childNonConfigs.get(i);
}
ViewModelStore viewModelStore = null;
if (viewModelStores != null && i < viewModelStores.size()) {
viewModelStore = viewModelStores.get(i);
}
Fragment f = fs.instantiate(mHost, mContainer, mParent, childNonConfig,
viewModelStore);
if (DEBUG) Log.v(TAG, "restoreAllState: active #" + i + ": " + f);
mActive.put(f.mIndex, 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;
}
}
...
}
這方法名意為恢復所有的狀態(tài)记舆,而其中注釋為創(chuàng)建激活Fragment的列表,并將他們從保存的狀態(tài)中實例化魁亦。這個方法應該是Fragment重新實例化時調用的方法喉誊。該方法在Fragment的restoreChildFragmentState被調用邀摆。
void restoreChildFragmentState(@Nullable Bundle savedInstanceState) {
if (savedInstanceState != null) {
Parcelable p = savedInstanceState.getParcelable(
FragmentActivity.FRAGMENTS_TAG);
if (p != null) {
if (mChildFragmentManager == null) {
instantiateChildFragmentManager();
}
mChildFragmentManager.restoreAllState(p, mChildNonConfig);
mChildNonConfig = null;
mChildFragmentManager.dispatchCreate();
}
}
}
restoreChildFragmentState方法又在Fragment的onCreate方法中調用,這里將保存的savedInstanceState狀態(tài)又傳遞給了restoreChildFragmentState以完成Fragment的重新實例化伍茄。
@CallSuper
public void onCreate(@Nullable Bundle savedInstanceState) {
mCalled = true;
restoreChildFragmentState(savedInstanceState);
if (mChildFragmentManager != null
&& !mChildFragmentManager.isStateAtLeast(Fragment.CREATED)) {
mChildFragmentManager.dispatchCreate();
}
}
結論
經過以上的分析栋盹,我們就知道了為什么這個錯誤出在了Fragment的有參構造函數(shù)上。因為當Fragment因為某種原因重新創(chuàng)建時敷矫,會調用到onCreate方法傳入之前保存的狀態(tài)例获,在instantiate方法中通過反射無參構造函數(shù)創(chuàng)建一個Fragment汉额,并且為Arguments初始化為原來保存的值,而此時如果沒有無參構造函數(shù)就會拋出異常榨汤,造成程序崩潰蠕搜。
所以Fragment的構造函數(shù)以及參數(shù)傳遞正確使用方式為如下:
public static MorePlanFragment newInstance(String modelId,HashMap<String, Integer> map) {
Bundle args = new Bundle();
args.putString("modelId",modelId);
args.putSerializable("map",map);
MorePlanFragment fragment = new MorePlanFragment();
fragment.setArguments(args);
return fragment;
}