DialogFragment這個(gè)控件作為一個(gè)Android開發(fā)者來說,應(yīng)該都是再熟悉不過的了。不過在showDialogFragment發(fā)的時(shí)候經(jīng)常會(huì)碰到下面這個(gè)crash:
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
at androidx.fragment.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:2080)
at androidx.fragment.app.FragmentManagerImpl.enqueueAction(FragmentManager.java:2106)
at androidx.fragment.app.BackStackRecord.commitInternal(BackStackRecord.java:683)
at androidx.fragment.app.BackStackRecord.commit(BackStackRecord.java:637)
at androidx.fragment.app.DialogFragment.show(DialogFragment.java:144)
at com.netease.demo.MainActivity$1.run(MainActivity.java:23)
at android.os.Handler.handleCallback(Handler.java:873)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:193)
at android.app.ActivityThread.main(ActivityThread.java:6669)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
對(duì)于上面這個(gè)問題,首先肯定是得去查看源碼為什么會(huì)出現(xiàn)這個(gè)錯(cuò)誤
一般來說使用DialogFragment基本上就是下面這種寫法:
DialogFragment dialogFragment = new DialogFragment();
dialogFragment.show(getSupportFragmentManager(),"");
- 獲取宿主即當(dāng)前Activity所持有的FragmentManager,是一個(gè)FragmentManagerImpl對(duì)象
- 調(diào)用dialogFragment的show方法
- 通過FragmentManagerImpl調(diào)用beginTransaction方法new了一個(gè)BackStackRecord對(duì)象
- 通過BackStackRecord將Fragment添加到布局中的控件中
- 通過事務(wù)提交這次操作。
crash就出現(xiàn)在了第五步中,我們來看下第四步的邏輯:
@Override
public int commit() {
return commitInternal(false);
}
int commitInternal(boolean allowStateLoss) {
if (mCommitted) throw new IllegalStateException("commit already called");
if (FragmentManagerImpl.DEBUG) {
Log.v(TAG, "Commit: " + this);
LogWriter logw = new LogWriter(TAG);
PrintWriter pw = new PrintWriter(logw);
dump(" ", null, pw, null);
pw.close();
}
mCommitted = true;
if (mAddToBackStack) {
mIndex = mManager.allocBackStackIndex(this);
} else {
mIndex = -1;
}
mManager.enqueueAction(this, allowStateLoss);
return mIndex;
}
邏輯非常簡(jiǎn)單,先判斷當(dāng)前Fragment添加的事務(wù)是否已經(jīng)被提交過一次(防止復(fù)提交出現(xiàn)頁(yè)面錯(cuò)亂的check)。然后直接調(diào)用FragmentManager的enqueueAction方法:
public void enqueueAction(OpGenerator action, boolean allowStateLoss) {
if (!allowStateLoss) {
checkStateLoss();
}
synchronized (this) {
if (mDestroyed || mHost == null) {
if (allowStateLoss) {
// This FragmentManager isn't attached, so drop the entire transaction.
return;
}
throw new IllegalStateException("Activity has been destroyed");
}
if (mPendingActions == null) {
mPendingActions = new ArrayList<>();
}
mPendingActions.add(action);
scheduleCommit();
}
}
private void checkStateLoss() {
if (isStateSaved()) {
throw new IllegalStateException(
"Can not perform this action after onSaveInstanceState");
}
if (mNoTransactionsBecause != null) {
throw new IllegalStateException(
"Can not perform this action inside of " + mNoTransactionsBecause);
}
}
關(guān)鍵就處在了 checkStateLoss();這里,isStateSaved是true的時(shí)候就會(huì)拋出這個(gè)異常,那么什么時(shí)候拋出呢?
@Override
public boolean isStateSaved() {
// See saveAllState() for the explanation of this. We do this for
// all platform versions, to keep our behavior more consistent between
// them.
return mStateSaved || mStopped;
}
是在當(dāng)前宿主頁(yè)面onStop的時(shí)候會(huì)是true(回到home,或者是被其他窗口遮擋),這個(gè)時(shí)候如果再去執(zhí)行commit的話那就直接crash了。
這個(gè)時(shí)候就需要想辦法解決這個(gè)問題了,所以以下列出四種方法:
在每個(gè)DialogFragment的show方法調(diào)用之前,都判斷當(dāng)前宿主Activity是否已經(jīng)被其他窗口覆蓋或者進(jìn)入home了??梢詫懸粋€(gè)boolean變量,在onSaveInstance執(zhí)行的時(shí)候就置為true,然后根據(jù)這個(gè)變量判斷。
不過這個(gè)方案浸入性太強(qiáng),需要修改的地方太多了。這不是個(gè)好方案既然commit直接crash,那么怎么做才能阻止crash呢。我們?cè)倏聪耬nqueueAction方法:
if (!allowStateLoss) {
checkStateLoss();
}
只有在allowStateLoss為false的時(shí)候才需要去做checkStateLoss操作,所以我們只要設(shè)置alloStateLoss為true即可。不過我們沒法修改DialogFragment的show方法,所以我們可以在BaseDialogFragment里面重寫show方法
public void show(FragmentManager manager, String tag) {
mDismissed = false;
mShownByMe = true;
FragmentTransaction ft = manager.beginTransaction();
ft.add(this, tag);
//將ft.commit改成ft.commitAllowingStateLoss()。
ft.commit();
}
將ft.commit改成ft.commitAllowingStateLoss()。這樣就解決了浸入性強(qiáng)的問題,但是這里又有一個(gè)問題了,因?yàn)閙Dismissed和mShownByMe是默認(rèn)訪問權(quán)限的變量,我們自己的APP無法直接獲取這個(gè)參數(shù),所以也就沒法直接修改這個(gè)參數(shù),因此這兩個(gè)參數(shù)未被修改可能會(huì)引發(fā)其他的問題。所以這個(gè)方案也不行。
與方案二類似,既然我們無法獲取這兩個(gè)參數(shù),那我們干脆直接將DialogFragment的源碼拷貝出來,自己寫一個(gè)DialogFragment,這樣就可以修改show方法了。這種方案雖然沒有什么弊端,但是個(gè)人還是不太贊成這個(gè)方案的。
接下來就是我比較推薦的方案了,我們重新捋下邏輯:
為什么DialogFragment會(huì)crash? 是因?yàn)槲覀冊(cè)赾heckStateLoss();發(fā)現(xiàn)當(dāng)前宿主Activity已經(jīng)不在屏幕上顯示了,所以才會(huì)拋出這個(gè)異常,那么我們能否在show的時(shí)候判斷呢?自然是可以的,我們的第一種方案就是,不過第一種方案是在show外面判斷,接下來我要介紹的方案是重寫show方法:
/**
* 檢查當(dāng)前頁(yè)面是否處于活躍狀態(tài)
* @param manager 當(dāng)前Activity對(duì)應(yīng)的fragment管理者
* @return
*/
protected boolean checkActivityIsActive(FragmentManager manager) {
if(manager.isStateSaved()){
return false;
}
return true;
}
@Override
public void show(FragmentManager manager, String tag) {
if(!checkActivityIsActive(manager)){
return;
}
super.show(manager, tag);
}
既然checkStateLoss是在FragmentManager里面判斷的,所以我們也可以直接通過FragmentManager的isStateSaved進(jìn)行判斷。不過如果我們使用的是show(FragmentTransaction transaction, String tag)方法,那么就稍微麻煩點(diǎn),下面直接給出
@Override
public int show(FragmentTransaction transaction, String tag) {
try {
Field field = transaction.getClass().getDeclaredField("mManager");
field.setAccessible(true);
if (!checkActivityIsActive((FragmentManager) field.get(transaction))) return STATUS_COMMIT_FAILED;
} catch (Exception e) {
e.printStackTrace();
}
return super.show(transaction, tag);
}
通過事務(wù)反射獲取對(duì)應(yīng)的FragmentManager,然后在調(diào)用之前的判斷方法來判斷是否需要show出來。這樣浸入性不高,另外對(duì)源碼的邏輯改動(dòng)也不多。