1. 程式人生 > >FragmentTransaction的commit和commitAllowingStateLoss的區別

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()這種方式,因為後者會有不可預料的副作用。

總結:

  1. 如果你需要同步提交Fragment並且無需新增到回退棧中,則使用commitNow()。Support庫中在 FragmentPagerAdapter中使用這個函式,來確保更新Adapter的時候頁面被正確的新增和刪除。一般來說,只要不新增到回退棧中,都可以使用這個函式來提交。

  2. 如果執行的提交不需要是同步的,或者需要將提交都新增到回退棧中,那麼就使用commit()

  3. 如果你需要把多次提交的操作在同一個時間點一起執行,則使用 executePendingTransactions()

  4. 如果你需要在Activity執行完onSaveInstanceState()之後還要進行提交,而且不關心恢復時是否會丟失此次提交,那麼可以使用commitAllowingStateLoss()commitNowAllowingStateLoss()

 

1、什麼是FragmentTransaction?

使用Fragment時,可以通過使用者互動來執行一些動作,比如增加、移除、替換等。

所有這些改變構成一個集合,這個集合被叫做一個transaction。

可以呼叫FragmentTransaction中的方法來處理這個transaction,並且可以將transaction存進由activity管理的back stack中,這樣使用者就可以進行fragment變化的回退操作。

可以這樣得到FragmentTransaction類的例項:

 

 
  1. FragmentManager mFragmentManager = getSupportFragmentManager();

  2. 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的方法

 
  1. /**

  2. * Return the FragmentManager for interacting with fragments associated

  3. * with this activity.

  4. */

  5. public FragmentManager getSupportFragmentManager() {

  6. return mFragments;

  7. }



查詢到mFragments。
final FragmentManagerImpl mFragments = new FragmentManagerImpl();
我們發現FragmentManagerImpl是繼承於FragmentManager的一個實體類

 
  1. /**

  2. * Container for fragments associated with an activity.

  3. */

  4. final class FragmentManagerImpl extends FragmentManager {

  5.  
  6. ........

  7.  
  8.  
  9. @Override

  10. public FragmentTransaction beginTransaction() {

  11. return new BackStackRecord(this);

  12. }

  13.  
  14.  
  15. ........

  16.  
  17.  
  18. }



為了簡便我們刪除了一些不要的程式碼只留下關鍵的方法。
通過這段程式碼,我們可以檢視到beginTransaction方法實際返回的是一個繼承於FragmentTransaction的BackStackRecord類
我們來檢視BackStackRecord的程式碼,檢視他的用法

 
  1. /**

  2. * @hide Entry of an operation on the fragment back stack.

  3. */

  4. final class BackStackRecord extends FragmentTransaction implements

  5. FragmentManager.BackStackEntry, Runnable {

  6.  
  7.  
  8. ..........

  9. public int commit() {

  10. return commitInternal(false);

  11. }

  12.  
  13.  
  14. public int commitAllowingStateLoss() {

  15. return commitInternal(true);

  16. }

  17.  
  18.  
  19. int commitInternal(boolean allowStateLoss) {

  20. if (mCommitted) throw new IllegalStateException("commit already called");

  21. if (FragmentManagerImpl.DEBUG) Log.v(TAG, "Commit: " + this);

  22. mCommitted = true;

  23. if (mAddToBackStack) {

  24. mIndex = mManager.allocBackStackIndex(this);

  25. } else {

  26. mIndex = -1;

  27. }

  28. mManager.enqueueAction(this, allowStateLoss);

  29. return mIndex;

  30. }

  31. ..........

  32.  
  33.  
  34. }



繞了大半天,終於找到commit方法和commitAllowingStateLoss方法,他們都同時呼叫了commitInternal方法,只是傳的引數略有不同,一個是true,一個是false。我們發現在執行這個方法之前會首先對mCommitted進行判斷,根據程式碼語義我們可以知道mCommitted就是是否已經commit的意思
最後,commitInternal呼叫了mManager.enqueueAction的方法。讓我們回到FragmentManager,看這個方法是如何操作的。我們找到這個方法。

 
  1. /**

  2. * @hide Entry of an operation on the fragment back stack.

  3. */

  4. final class BackStackRecord extends FragmentTransaction implements

  5. FragmentManager.BackStackEntry, Runnable {

  6.  
  7.  
  8. ..........

  9. public int commit() {

  10. return commitInternal(false);

  11. }

  12.  
  13.  
  14. public int commitAllowingStateLoss() {

  15. return commitInternal(true);

  16. }

  17.  
  18.  
  19. int commitInternal(boolean allowStateLoss) {

  20. if (mCommitted) throw new IllegalStateException("commit already called");

  21. if (FragmentManagerImpl.DEBUG) Log.v(TAG, "Commit: " + this);

  22. mCommitted = true;

  23. if (mAddToBackStack) {

  24. mIndex = mManager.allocBackStackIndex(this);

  25. } else {

  26. mIndex = -1;

  27. }

  28. mManager.enqueueAction(this, allowStateLoss);

  29. return mIndex;

  30. }

  31. ..........

  32.  
  33.  
  34. }



經分析後,我們可以發現,此方法在對 commit和commitAllowingStateLoss的傳參進行判斷後,將任務扔進activity的執行緒佇列中。那這個兩個方法區別就在傳參判斷後的處理方法checkStateLoss,那接下來,讓我們檢視一下checkStateLoss方法,看對引數進行判斷後,做了什麼樣的處理。

 
  1. private void checkStateLoss() {

  2. if (mStateSaved) {

  3. throw new IllegalStateException(

  4. "Can not perform this action after onSaveInstanceState");

  5. }

  6. if (mNoTransactionsBecause != null) {

  7. throw new IllegalStateException(

  8. "Can not perform this action inside of " + mNoTransactionsBecause);

  9. }

  10. }



ok,到這裡,真相總算大明,當使用commit方法時,系統將進行狀態判斷,如果狀態(mStateSaved)已經儲存,將發生"Can not perform this action after onSaveInstanceState"錯誤。
如果mNoTransactionsBecause已經存在,將發生"Can not perform this action inside of " + mNoTransactionsBecause錯誤。