FragmentTransaction的commit和commitAllowingStateLoss的區別
1. 每個事務(FragmentTranscation)只能被commit一次
承接Fragment進階 - 基本用法中“Fragment動態載入”的事例,如果介面裡有多個Fragment需要提交,而且我不想一次性全部提交,而是分幾次提交...(事例程式碼如下)
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); getSupportFragmentManager().beginTransaction().add(R.id.fl_main, new ContentFragment(), null).commit(); getSupportFragmentManager().beginTransaction().add(R.id.fl_main, new ContentFragment(), null).commit(); getSupportFragmentManager().beginTransaction().add(R.id.fl_main, new ContentFragment(), null).commit(); } }
執行程式,沒有問題程式正常,然後看下此時的檢視結構:
add.png
我們add了3次,我們的Activty裡有了3個Fragment的檢視(這也是小編踩坑之後才成功的)。
在最初進行編寫程式碼的時候,小編遇到一個坑,最初的程式碼是這樣寫的:
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); transaction.add(R.id.fl_main, new ContentFragment(), null).commit(); transaction.add(R.id.fl_main, new ContentFragment(), null).commit(); transaction.add(R.id.fl_main, new ContentFragment(), null).commit(); } }
我們把事務物件提取了出來(相信這是有潔癖的程式設計師的愛好),進行了多次提交,執行一下結果程式崩潰了(崩潰日誌如下)...
Caused by: java.lang.IllegalStateException: commit already called
日誌告訴我們commit已經被呼叫了,看來每個事務物件只能commit一次...
但是之前的寫法是沒問題的,由此可以推測:每次呼叫getSupportFragmentManager().beginTransaction()
獲取的都是一個新的例項。
檢視原始碼,證實了我們的推測...
fragment_transation.png
原來,我們開啟的每一個事務都是一個回退棧記錄(BackStackRecord是FragmentTransaction的一個具體實現類)
接下來,我們研究下BackStackRecord,看看commit異常是怎麼丟擲的。
commit_error.png
BackStackRecord的commit操作會呼叫一個commitInternal方法。
mCommitted記錄了提交狀態,我們的第1次提交mCommitted被置為true,第2次提交就拋異常了(整個原始碼中對mCommitted進行賦值的地方僅此1處(628行))。
結論:每個事務物件只能被commit一次。
2. Activity執行完onSaveInstanceState()方法後不能再執行commit()方法
小編想手動控制Fragment的新增顯示,在Activity被銷燬的時候將Fragment移除掉(程式碼如下)
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
getSupportFragmentManager().beginTransaction().add(R.id.fl_main, new ContentFragment(), "ContentFragment").commit();
}
@Override
protected void onDestroy() {
getSupportFragmentManager().beginTransaction().remove(getSupportFragmentManager().findFragmentByTag("ContentFragment")).commit();
super.onDestroy();
}
}
執行下程式,發現退出時程式崩潰了...我們得到如下崩潰日誌:
Caused by: java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
異常日誌告訴我們:不能在onSaveInstanceState()
方法被執行之後呼叫commit()
方法。
那麼我們就提到onSaveInstanceState()
之前呼叫,程式碼如下:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
getSupportFragmentManager().beginTransaction().add(R.id.fl_main, new ContentFragment(), "ContentFragment").commit();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
getSupportFragmentManager().beginTransaction().remove(getSupportFragmentManager().findFragmentByTag("ContentFragment")).commit();
super.onSaveInstanceState(outState);
}
}
執行下程式,發現退出程式不蹦了...但又有一個新問題,手機滅屏時也會呼叫onSaveInstanceState方法,在開啟介面發現介面變成空白了...這種方法(手動移除Fragment)顯然不行,這點需要注意。
3. commitAllowingStateLoss()方法
FragmentTranscation 給我們提供了另外一個方法 commitAllowingStateLoss()
,從名字我們也能看出這個方法的作用:允許狀態丟失的提交。
看下官方文件的解釋,我們就能明白原因了:
FragmentTransaction API commitAllowingStateLoss() (需要翻牆)
commit.png
原來,在Activity的狀態被儲存之後的提交操作是沒有被Activity所記錄的,恢復時也就沒辦法恢復這些提交操作,所以官方文件稱這個方法是一個危險的操作。如果這些提交操作不是很重要,丟不丟失無所謂的話你就可以使用commitAllowingStateLoss()
這個方法了。
我們調整下程式碼,用commitAllowingStateLoss()
代替commit()
方法(如下):
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
getSupportFragmentManager().beginTransaction().add(R.id.fl_main, new ContentFragment(), "ContentFragment").commit();
}
@Override
protected void onDestroy() {
getSupportFragmentManager().beginTransaction().remove(getSupportFragmentManager().findFragmentByTag("ContentFragment")).commitAllowingStateLoss();
super.onDestroy();
}
}
執行下程式碼,現在看起來沒問題了...但我們知道如果出現了Activity異常銷燬重啟的情況,我們的移除操作在恢復時就丟失了,我們的Fragment將會出現重複疊加的問題。
之前小編在Fragment進階 - FragmentTransaction詳解最開始的時候闡述過這個問題,給出過最佳的解決辦法,就不再重複說明了。
最後,我們從原始碼上看下commit()
和commitAllowingStateLoss()
有什麼區別。
下面為BackStackRecord(FragmentTransaction的具體實現類)的部分原始碼。
final class BackStackRecord extends FragmentTransaction implements
FragmentManager.BackStackEntry, Runnable {
......
public int commit() {
return commitInternal(false);
}
public int commitAllowingStateLoss() {
return commitInternal(true);
}
int commitInternal(boolean allowStateLoss) {
......
mManager.enqueueAction(this, allowStateLoss);
......
}
......
}
我們看到commit()
和commitAllowingStateLoss()
都呼叫了commitInternal(boolean allowStateLoss)
這個方法只不過傳入引數不同而已(commit()
傳入的false,commitAllowingStateLoss()
傳入的true),接下來會呼叫FragmentManagerImpl(FragmentManager的具體實現類)的enqueueAction方法。
final class FragmentManagerImpl extends FragmentManager implements LayoutInflaterFactory {
......
public void enqueueAction(Runnable action, boolean allowStateLoss) {
if (!allowStateLoss) {
checkStateLoss();
}
......
}
.....
}
我們看到,如果commitAllowingStateLoss()
傳的是true所以忽略掉了檢查,commit()
傳的是false,所以進行了檢查。
最後我們看下檢查的方法checkStateLoss()
:
final class FragmentManagerImpl extends FragmentManager implements LayoutInflaterFactory {
......
private void checkStateLoss() {
if (mStateSaved) {
throw new IllegalStateException(
"Can not perform this action after onSaveInstanceState");
}
......
}
......
}
看來我們之前的異常就是在這丟擲的,也算是找到根源了。
4. commit()
方法被呼叫時並不會立即執行
舉個簡單的例子:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
getSupportFragmentManager().beginTransaction().add(R.id.fl_main, new ContentFragment(), null).commit();
Log.e("TAG", "MainActivity onCreated");
}
@Override
public void onAttachFragment(Fragment fragment) {
super.onAttachFragment(fragment);
Log.e("TAG", "MainActivity onAttachFragmented");
}
}
onAttachFragment(Fragment fragment)
方法,會在新增進來的Fragment執行完onAttach方法後被回撥。
我們得到如下的執行日誌:
10-17 17:26:51.076 9035-9035/com.sina.example.fragmentdemo E/TAG: MainActivity onCreated
10-17 17:26:51.078 9035-9035/com.sina.example.fragmentdemo E/TAG: Fragment onAttach()
10-17 17:26:51.078 9035-9035/com.sina.example.fragmentdemo E/TAG: MainActivity onAttachFragmented
我們看到commit()
被呼叫後Fragment的生命週期沒有立即開始,而是放在了onCreate(Bundle savedInstanceState)
執行完成之後的某個時間。
(具體開始執行的時間點:在原始碼中該任務是被放在一個mHost.getHandler().post(mExecCommit)
方法的引數裡,也就是Handler初始化完畢,開始傳送訊息的時候,我們提交的任務將會被真正執行)
如果不想等待Handler初始化完畢,想要立即執行commit的操作可以使用FragmentManager裡提供的executePendingTransactions()
方法,這個方法將會將你所有提交的操作一併執行,而且是立即執行。
我們更改程式碼,新增上這個方法(如下所示)。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
getSupportFragmentManager().beginTransaction().add(R.id.fl_main, new ContentFragment(), null).commit();
getSupportFragmentManager().executePendingTransactions();
Log.e("TAG", "MainActivity onCreated");
}
@Override
public void onAttachFragment(Fragment fragment) {
super.onAttachFragment(fragment);
Log.e("TAG", "MainActivity onAttachFragmented");
}
}
執行程式,列印下日誌:
10-17 17:51:37.626 22400-22400/com.sina.example.fragmentdemo E/TAG: Fragment onAttach()
10-17 17:51:37.626 22400-22400/com.sina.example.fragmentdemo E/TAG: MainActivity onAttachFragmented
10-17 17:51:37.627 22400-22400/com.sina.example.fragmentdemo E/TAG: MainActivity onCreated
我們看到在"MainActivity onCreated"日誌還未被列印之前,我們提交的Fragment的生命週期已經開始了。
5. commitNow()
和commitNowAllowingStateLoss()
在最新的API_24文件裡FragmentTranslation裡添加了兩個方法:
-
commitNow()
-
commitNowAllowingStateLoss()
呼叫這兩個方法類似於先執行commit()/commitAllowingStateLoss()
然後執行executePendingTransactions()
方法。但也有區別。
區別一:不支援新增到回退棧的操作(原始碼如下)
final class BackStackRecord extends FragmentTransaction implements
FragmentManager.BackStackEntry, Runnable {
......
@Override
public void commitNow() {
disallowAddToBackStack();
mManager.execSingleAction(this, false);
}
@Override
public void commitNowAllowingStateLoss() {
disallowAddToBackStack();
mManager.execSingleAction(this, true);
}
......
}
如果還呼叫addToBackStack(String name)
方法會報一個IllegalStateException異常,告訴你不能向回退棧中新增FragmentTransaction。
區別二:原始碼沒有再使用Handler,而是直接執行(原始碼如下)
final class FragmentManagerImpl extends FragmentManager implements LayoutInflaterFactory {
......
public void execSingleAction(Runnable action, boolean allowStateLoss) {
......
action.run();
......
}
......
}
BackStackRecord實現了Runnable介面重寫了run方法,action.run()
會直接執行BackStackRecord的run方法。
補充:
官方更推薦使用commitNow()
和commitNowAllowingStateLoss()
來代替先執行commit()/commitAllowingStateLoss()
然後執行executePendingTransactions()
這種方式,因為後者會有不可預料的副作用。
總結:
-
如果你需要同步提交Fragment並且無需新增到回退棧中,則使用
commitNow(
)。Support庫中在 FragmentPagerAdapter中使用這個函式,來確保更新Adapter的時候頁面被正確的新增和刪除。一般來說,只要不新增到回退棧中,都可以使用這個函式來提交。 -
如果執行的提交不需要是同步的,或者需要將提交都新增到回退棧中,那麼就使用
commit()
。 -
如果你需要把多次提交的操作在同一個時間點一起執行,則使用
executePendingTransactions()
-
如果你需要在Activity執行完onSaveInstanceState()之後還要進行提交,而且不關心恢復時是否會丟失此次提交,那麼可以使用
commitAllowingStateLoss()
或commitNowAllowingStateLoss()
。
1、什麼是FragmentTransaction?
使用Fragment時,可以通過使用者互動來執行一些動作,比如增加、移除、替換等。
所有這些改變構成一個集合,這個集合被叫做一個transaction。
可以呼叫FragmentTransaction中的方法來處理這個transaction,並且可以將transaction存進由activity管理的back stack中,這樣使用者就可以進行fragment變化的回退操作。
可以這樣得到FragmentTransaction類的例項:
-
FragmentManager mFragmentManager = getSupportFragmentManager();
-
FragmentTransaction mFragmentTransaction = mFragmentManager.beginTransaction();
2、commit和executePendingTransactions的區別
用add(), remove(), replace()方法,把所有需要的變化加進去,然後呼叫commit()方法,將這些變化應用。
在commit()方法之前,你可以呼叫addToBackStack(),把這個transaction加入back stack中去,這個back stack是由activity管理的,當用戶按返回鍵時,就會回到上一個fragment的狀態。
你只能在activity儲存它的狀態(當用戶要離開activity時)之前呼叫commit(),如果在儲存狀態之後呼叫commit(),將會丟擲一個異常。
這是因為當activity再次被恢復時commit之後的狀態將丟失。如果丟失也沒關係,那麼使用commitAllowingStateLoss()方法。
3、問什麼在儲存狀態之後呼叫commit會報異常?
我們檢視Android原始碼發現FragmentManager和FragmentTransaction是一個虛類
那他們在activity中的例項化程式碼是如何處理的呢?
首先是getSupportFragmentManager的方法
-
/**
-
* Return the FragmentManager for interacting with fragments associated
-
* with this activity.
-
*/
-
public FragmentManager getSupportFragmentManager() {
-
return mFragments;
-
}
查詢到mFragments。
final FragmentManagerImpl mFragments = new FragmentManagerImpl();
我們發現FragmentManagerImpl是繼承於FragmentManager的一個實體類
-
/**
-
* Container for fragments associated with an activity.
-
*/
-
final class FragmentManagerImpl extends FragmentManager {
-
........
-
@Override
-
public FragmentTransaction beginTransaction() {
-
return new BackStackRecord(this);
-
}
-
........
-
}
為了簡便我們刪除了一些不要的程式碼只留下關鍵的方法。
通過這段程式碼,我們可以檢視到beginTransaction方法實際返回的是一個繼承於FragmentTransaction的BackStackRecord類
我們來檢視BackStackRecord的程式碼,檢視他的用法
-
/**
-
* @hide Entry of an operation on the fragment back stack.
-
*/
-
final class BackStackRecord extends FragmentTransaction implements
-
FragmentManager.BackStackEntry, Runnable {
-
..........
-
public int commit() {
-
return commitInternal(false);
-
}
-
public int commitAllowingStateLoss() {
-
return commitInternal(true);
-
}
-
int commitInternal(boolean allowStateLoss) {
-
if (mCommitted) throw new IllegalStateException("commit already called");
-
if (FragmentManagerImpl.DEBUG) Log.v(TAG, "Commit: " + this);
-
mCommitted = true;
-
if (mAddToBackStack) {
-
mIndex = mManager.allocBackStackIndex(this);
-
} else {
-
mIndex = -1;
-
}
-
mManager.enqueueAction(this, allowStateLoss);
-
return mIndex;
-
}
-
..........
-
}
繞了大半天,終於找到commit方法和commitAllowingStateLoss方法,他們都同時呼叫了commitInternal方法,只是傳的引數略有不同,一個是true,一個是false。我們發現在執行這個方法之前會首先對mCommitted進行判斷,根據程式碼語義我們可以知道mCommitted就是是否已經commit的意思
最後,commitInternal呼叫了mManager.enqueueAction的方法。讓我們回到FragmentManager,看這個方法是如何操作的。我們找到這個方法。
-
/**
-
* @hide Entry of an operation on the fragment back stack.
-
*/
-
final class BackStackRecord extends FragmentTransaction implements
-
FragmentManager.BackStackEntry, Runnable {
-
..........
-
public int commit() {
-
return commitInternal(false);
-
}
-
public int commitAllowingStateLoss() {
-
return commitInternal(true);
-
}
-
int commitInternal(boolean allowStateLoss) {
-
if (mCommitted) throw new IllegalStateException("commit already called");
-
if (FragmentManagerImpl.DEBUG) Log.v(TAG, "Commit: " + this);
-
mCommitted = true;
-
if (mAddToBackStack) {
-
mIndex = mManager.allocBackStackIndex(this);
-
} else {
-
mIndex = -1;
-
}
-
mManager.enqueueAction(this, allowStateLoss);
-
return mIndex;
-
}
-
..........
-
}
經分析後,我們可以發現,此方法在對 commit和commitAllowingStateLoss的傳參進行判斷後,將任務扔進activity的執行緒佇列中。那這個兩個方法區別就在傳參判斷後的處理方法checkStateLoss,那接下來,讓我們檢視一下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);
-
}
-
}
ok,到這裡,真相總算大明,當使用commit方法時,系統將進行狀態判斷,如果狀態(mStateSaved)已經儲存,將發生"Can not perform this action after onSaveInstanceState"錯誤。
如果mNoTransactionsBecause已經存在,將發生"Can not perform this action inside of " + mNoTransactionsBecause錯誤。