一、問題
先來看兩個Crash Log:
1.
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
at android.support.v4.app.ab.v(FragmentManager.java:1377)
at android.support.v4.app.ab.a(FragmentManager.java:1395)
at android.support.v4.app.h.a(BackStackRecord.java:637)
at android.support.v4.app.h.b(BackStackRecord.java:616)
at android.support.v4.app.DialogFragment.show(DialogFragment.java:139)
at com.sankuai.common.views.ai.a(MaoyanDialogBuilder.java:184)
at com.sankuai.movie.y.handleMessage(MovieMainActivity.java:750)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:137)
at android.app.ActivityThread.main(ActivityThread.java:4424)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:511)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:784)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:551)
at dalvik.system.NativeStart.main(Native Method)
2.
java.lang.IllegalStateException: Can not perform this action inside of onLoadFinished
at android.support.v4.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:1381)
at android.support.v4.app.FragmentManagerImpl.enqueueAction(FragmentManager.java:1395)
at android.support.v4.app.BackStackRecord.commitInternal(BackStackRecord.java:637)
at android.support.v4.app.BackStackRecord.commit(BackStackRecord.java:616)
at android.support.v4.app.DialogFragment.show(DialogFragment.java:139)
at com.sankuai.common.views.MaoyanDialogBuilder.show(MaoyanDialogBuilder.java:185)
at com.sankuai.movie.MovieMainActivity$8$1$1.onLoadFinished(MovieMainActivity.java:609)
at com.sankuai.movie.MovieMainActivity$8$1$1.onLoadFinished(MovieMainActivity.java:590)
at android.support.v4.app.LoaderManagerImpl$LoaderInfo.callOnLoadFinished(LoaderManager.java:427)
at android.support.v4.app.LoaderManagerImpl$LoaderInfo.onLoadComplete(LoaderManager.java:395)
at android.support.v4.content.Loader.deliverResult(Loader.java:104)
二、原因
經(jīng)過查找,發(fā)現(xiàn)這兩個Crash處于由同一個方法觸發(fā):
FragmentManagerImpl#checkStateLoss():
private void checkStateLoss() {
if (mStateSaved) {
throw new IllegalStateException(
"Can not perform this action after onSaveInstanceState");
}
if (mNoTransactionsBecause != null) {
throw new IllegalStateException(
"Can not perform this action inside of " + mNoTransactionsBecause);
}
}
這個方法中的每一個異常分別對應了上述的兩段Crash Log。下面逐一分析。
三、分析
第一部分:mStateSaved:
關于這個問題,可以參考這篇文章,Fragment Transactions & Activity State Loss。
從代碼中可以看出,若這個字段為true,則會拋出異常。檢查代碼中與這個字段相關的內(nèi)容,如下:
Parcelable saveAllState() {
// Make sure all pending operations have now been executed to get
// our state update-to-date.
execPendingActions();
if (HONEYCOMB) {
// As of Honeycomb, we save state after pausing. Prior to that
// it is before pausing. With fragments this is an issue, since
// there are many things you may do after pausing but before
// stopping that change the fragment state. For those older
// devices, we will not at this point say that we have saved
// the state, so we will allow them to continue doing fragment
// transactions. This retains the same semantics as Honeycomb,
// though you do have the risk of losing the very most recent state
// if the process is killed... we'll live with that.
mStateSaved = true;
}
...
}
public void noteStateNotSaved() {
mStateSaved = false;
}
public void dispatchCreate() {
mStateSaved = false;
moveToState(Fragment.CREATED, false);
}
public void dispatchActivityCreated() {
mStateSaved = false;
moveToState(Fragment.ACTIVITY_CREATED, false);
}
public void dispatchStart() {
mStateSaved = false;
moveToState(Fragment.STARTED, false);
}
public void dispatchResume() {
mStateSaved = false;
moveToState(Fragment.RESUMED, false);
}
public void dispatchPause() {
moveToState(Fragment.STARTED, false);
}
public void dispatchStop() {
// See saveAllState() for the explanation of this. We do this for
// all platform versions, to keep our behavior more consistent between
// them.
mStateSaved = true;
moveToState(Fragment.STOPPED, false);
}
可以看到,這個字段的使用與生命周期有關,隨便找一個生命周期的傳遞方法去查看使用:
會有三處使用到,分別研究
1)Fragment#getChildFragmentManager
/**
* Return a private FragmentManager for placing and managing Fragments
* inside of this Fragment.
*/
final public FragmentManager getChildFragmentManager() {
if (mChildFragmentManager == null) {
instantiateChildFragmentManager();
if (mState >= RESUMED) {
mChildFragmentManager.dispatchResume();
} else if (mState >= STARTED) {
mChildFragmentManager.dispatchStart();
} else if (mState >= ACTIVITY_CREATED) {
mChildFragmentManager.dispatchActivityCreated();
} else if (mState >= CREATED) {
mChildFragmentManager.dispatchCreate();
}
}
return mChildFragmentManager;
}
這是在Fragment中獲取嵌套使用Fragment,獲取childFragmentManager時調用,即告知childFragmentManager父Fragment當前的生命周期。此時也會執(zhí)行childFragmentManager的初始化。
2)Fragment#performStart
void performStart() {
if (mChildFragmentManager != null) {
mChildFragmentManager.noteStateNotSaved();
mChildFragmentManager.execPendingActions();
}
mCalled = false;
onStart();
if (!mCalled) {
throw new SuperNotCalledException("Fragment " + this
+ " did not call through to super.onStart()");
}
if (mChildFragmentManager != null) {
mChildFragmentManager.dispatchStart();
}
if (mLoaderManager != null) {
mLoaderManager.doReportStart();
}
}
這是在Fragmen在自己的生命周期變化過程中,通知子Fragment。
3)FragmentActivity#onStart
/**
* Dispatch onStart() to all fragments. Ensure any created loaders are
* now started.
*/
@Override
protected void onStart() {
super.onStart();
mStopped = false;
mReallyStopped = false;
mHandler.removeMessages(MSG_REALLY_STOPPED);
if (!mCreated) {
mCreated = true;
mFragments.dispatchActivityCreated();
}
mFragments.noteStateNotSaved();
mFragments.execPendingActions();
if (!mLoadersStarted) {
mLoadersStarted = true;
if (mLoaderManager != null) {
mLoaderManager.doStart();
} else if (!mCheckedForLoaderManager) {
mLoaderManager = getLoaderManager("(root)", mLoadersStarted, false);
// the returned loader manager may be a new one, so we have to start it
if ((mLoaderManager != null) && (!mLoaderManager.mStarted)) {
mLoaderManager.doStart();
}
}
mCheckedForLoaderManager = true;
}
// NOTE: HC onStart goes here.
mFragments.dispatchStart();
if (mAllLoaderManagers != null) {
final int N = mAllLoaderManagers.size();
LoaderManagerImpl loaders[] = new LoaderManagerImpl[N];
for (int i=N-1; i>=0; i--) {
loaders[i] = mAllLoaderManagers.valueAt(i);
}
for (int i=0; i<N; i++) {
LoaderManagerImpl lm = loaders[i];
lm.finishRetain();
lm.doReportStart();
}
}
}
這是Activity在生命周期發(fā)生變化時,通知Fragment。
現(xiàn)在了解了這個行為是和整個Activity、Fragment的生命周期有關的,回過頭再來看mStateSaved,這個字段是在onPause或者onStop之后就被置為true。在Android中,由于對運行時的生命周期應用能做的實在是很少,用戶可以隨時切換Activity,系統(tǒng)也可以隨時回收處于后臺的Activity內(nèi)存,所以Android為了保證在再次返回Activity時讓它看起來同離開時相似,會使用onSaveInstanceState()來保存一些狀態(tài)。若在onSaveInstanceState()被調用之后調用FragmentTransaction#commit(),那么這個Fragment的狀態(tài)就不會被保存,在之后恢復時也不會恢復這個Fragment,使得恢復時UI發(fā)生一些變化。
那為什么是onPause或者onStop之后被置為true?這和Android的版本發(fā)展有關,在HoneyComb之前,Activity被設計成在onPause之前不會被殺掉,所以onSaveInstanceState()是緊挨著onPause()之前調用的,但是在HoneyComb之后,Activity被設計成只有在onStop()之后才會被殺死,所以onSaveInstanceState()會在onStop()之前,而不是在onPause之前調用。
pre-Honeycomb
post-Honeycomb
Activities can be killed before onPause()? NO NO
Activities can be killed before onStop()? YES NO
onSaveInstanceState(Bundle) is guaranteed to be called before... onPause() onStop()
由于這個原因,若是在舊機型的onPause()之后調用FragmentTransaction#commit(),這個狀態(tài)就可能會丟失。這是由于Android開發(fā)者為了避免過多的異常而做出的讓步,允許在onPause()和onStop()之間偶爾丟失commit()狀態(tài)。
pre-Honeycomb
post-Honeycomb
commit() before onPause() OK OK
commit() between onPause() and onStop() STATE LOSS OK
commit() after onStop() EXCEPTION
EXCEPTION
應當注意的是,不僅僅是Activity的生命周期會影響,若使用的是嵌套在Fragment中的子Fragment,由上述代碼可知,也會有類似情況。
第二部分:mNoTransactionsBecause
再看一下FragmentManagerImpl#checkStateLoss()方法,
private void checkStateLoss() {
if (mStateSaved) {
throw new IllegalStateException(
"Can not perform this action after onSaveInstanceState");
}
if (mNoTransactionsBecause != null) {
throw new IllegalStateException(
"Can not perform this action inside of " + mNoTransactionsBecause);
}
}
還有一個和StateLoss相關的字段是mNoTransactionsBecause,若這個字段不為空,則拋出異常。先查看這個字段在哪里使用,或者說,在什么情況下這個字段會不為空。
可以看到調用的地方主要是兩個類,F(xiàn)ragmentManager 和 LoaderManager,在FragmentManager中的使用都是用于拋出異常的,并不在意這個,而是在意何時不為空的,在LoaderManager中主要兩個地方為這個字段賦值:
1)destory
void destroy() {
if (DEBUG) Log.v(TAG, " Destroying: " + this);
mDestroyed = true;
boolean needReset = mDeliveredData;
mDeliveredData = false;
if (mCallbacks != null && mLoader != null && mHaveData && needReset) {
if (DEBUG) Log.v(TAG, " Reseting: " + this);
String lastBecause = null;
if (mActivity != null) {
lastBecause = mActivity.mFragments.mNoTransactionsBecause;
mActivity.mFragments.mNoTransactionsBecause = "onLoaderReset";
}
try {
mCallbacks.onLoaderReset(mLoader);
} finally {
if (mActivity != null) {
mActivity.mFragments.mNoTransactionsBecause = lastBecause;
}
}
}
mCallbacks = null;
mData = null;
mHaveData = false;
if (mLoader != null) {
if (mListenerRegistered) {
mListenerRegistered = false;
mLoader.unregisterListener(this);
}
mLoader.reset();
}
if (mPendingLoader != null) {
mPendingLoader.destroy();
}
}
2)callOnLoadFinished
void callOnLoadFinished(Loader<Object> loader, Object data) {
if (mCallbacks != null) {
String lastBecause = null;
if (mActivity != null) {
lastBecause = mActivity.mFragments.mNoTransactionsBecause;
mActivity.mFragments.mNoTransactionsBecause = "onLoadFinished";
}
try {
if (DEBUG) Log.v(TAG, " onLoadFinished in " + loader + ": "
+ loader.dataToString(data));
mCallbacks.onLoadFinished(loader, data);
} finally {
if (mActivity != null) {
mActivity.mFragments.mNoTransactionsBecause = lastBecause;
}
}
mDeliveredData = true;
}
在這兩個地方的考量,也是由于loader的異步可能導致fragment在onSaveInstanceState()之后調用導致狀態(tài)丟失。
四、解決方案
那么如何解決這個問題?
1)在LoaderManager.LoaderCallbacks#onLoadFinished 或者 LoaderManager.LoaderCallbacks#onLoaderReset中使用
這里直接使用commit()方法會直接拋出異常,需要加Handler避免在這兩個回調函數(shù)中直接使用commit()方法
2)在使用FragmentTransaction#commit()方法時注意當前的生命周期。
一般而言,會在onCreate()或者響應用戶的操作事件時才會使用commit()方法,這不會有問題,但假若在別的生命周期中使用就要小心了。例如onActivityResult(), onStart(), 和 onResume(),就需要注意。例如,在onResume()中使用,但onResume()并不會保證在Activity狀態(tài)恢復之后調用,此時需要使用FragmentActivity#onResumeFragments()或者Activity#onPostResume()中調用,這兩個會保證在狀態(tài)恢復之后調用。
3)避免異步回調中使用commit()方法
異步回調,如AsyncTask#onPostExecute() 和 LoaderManager.LoaderCallbacks#onLoadFinished(),之后無法保證當前的生命狀態(tài),而且異步通常會執(zhí)行一些比較耗時的操作,更容易使得這樣的丟失發(fā)生,如用戶發(fā)出一個請求之后點了HOME鍵。
4)使用 commitAllowingStateLoss()方法
這個方法會跳過mStateSaved的檢查,也不會在意會發(fā)生怎樣的影響,就算無法執(zhí)行或Activity狀態(tài)恢復之后發(fā)生了UI變動也不會有警報。
5)針對DialogFragment的解決方案
由于DialogFragment和其它Fragment相比比較特殊,創(chuàng)建、回收更頻繁也更不容易控制。
方法一:
對于DialogFragment而言,只有show()方法而沒有showAllowingStateLoss()方法。。。而且很多時候都需要在網(wǎng)絡請求返回之后根據(jù)返回的字段來顯示,所以最好在base中添加對Activity和Fragment的生命周期追蹤方法。但自己添加的方法和原生的畢竟在執(zhí)行時間上還是有一點時間差的,不能夠100%避免crash。
方法二:
或許有想法說可以重寫DialogFragment#show()方法,讓它支持commitAllowingStateLoss(),好吧,來看下源碼。。。這是show方法:
/**
* Display the dialog, adding the fragment to the given FragmentManager. This
* is a convenience for explicitly creating a transaction, adding the
* fragment to it with the given tag, and committing it. This does
* <em>not</em> add the transaction to the back stack. When the fragment
* is dismissed, a new transaction will be executed to remove it from
* the activity.
* @param manager The FragmentManager this fragment will be added to.
* @param tag The tag for this fragment, as per
* {@link FragmentTransaction#add(Fragment, String) FragmentTransaction.add}.
*/
public void show(FragmentManager manager, String tag) {
mDismissed = false;
mShownByMe = true;
FragmentTransaction ft = manager.beginTransaction();
ft.add(this, tag);
ft.commit();
}
/**
* Display the dialog, adding the fragment using an existing transaction
* and then committing the transaction.
* @param transaction An existing transaction in which to add the fragment.
* @param tag The tag for this fragment, as per
* {@link FragmentTransaction#add(Fragment, String) FragmentTransaction.add}.
* @return Returns the identifier of the committed transaction, as per
* {@link FragmentTransaction#commit() FragmentTransaction.commit()}.
*/
public int show(FragmentTransaction transaction, String tag) {
mDismissed = false;
mShownByMe = true;
transaction.add(this, tag);
mViewDestroyed = false;
mBackStackId = transaction.commit();
return mBackStackId;
}
可以看到,和普通的Fragment顯示方法區(qū)別并不大,其實DialogFragment只是一個Fragment里面套了個Dialog而已。但,有個很神奇的字段,mShownByMe,這個字段是做什么的?定義處和這里并沒有注釋,通過查找這個字段的使用,發(fā)現(xiàn)了這兩個方法:
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
if (!mShownByMe) {
// If not explicitly shown through our API, take this as an
// indication that the dialog is no longer dismissed.
mDismissed = false;
}
}
@Override
public void onDetach() {
super.onDetach();
if (!mShownByMe && !mDismissed) {
// The fragment was not shown by a direct call here, it is not
// dismissed, and now it is being detached... well, okay, thou
// art now dismissed. Have fun.
mDismissed = true;
}
}
通過這里可以發(fā)現(xiàn),這個字段是判斷DialogFragment是不是通過原生API的show()方法來顯示的。。否則就不在onAttach()和onDetach()里設置mDismissed字段的,再看onDetach里的注釋,為什么有種深深惡意。。(我辛辛苦苦寫好了API,你為什么不用?你為什么不用?你為什么不用?(╯‵□′)╯︵┻━┻)
重寫show()也是可以的,但需要緊接著重寫onAttach()、onDetach()等方法。
若有需要重寫show(FragmentTransaction transaction, String tag)方法,除了mShownByMe字段還需要注意mViewDestroyed字段的值設置。
其實對于重寫show()方法的弊端主要在于無法保證show()的執(zhí)行,從而導致isShowing()等方法的判斷不準確,引起一些其他的問題。
方法三:
棄用DialogFragment。這種方法可以避免如此繁復的生命周期,代碼和使用簡潔不少,但也少了 DialogFragment 的優(yōu)勢,例如不能在切換屏幕時保留 Dialog 等。
方法一的優(yōu)勢在于并不需要對DialogFragment的內(nèi)部實現(xiàn)做詳盡的了解,也避免了開發(fā)者的惡意,但在onPause()之后,onSaveInstanceState()之前的dialog都不會展示出來。
方法二的優(yōu)勢在于API使用者并不需要在調用API的同時還為它的上下文環(huán)境擔驚受怕,但重寫難度較大,而且會有狀態(tài)丟失的情況。
方法三也不是不可,只是有些問題只適用 DialogFragment 解決,手動實現(xiàn)會有很多變數(shù)。
自行斟酌使用。