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()获取的都是一个新的实例。

查看源码,证实了我们的推测...

扫描二维码关注公众号,回复: 4201973 查看本文章

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错误。

猜你喜欢

转载自blog.csdn.net/kdsde/article/details/82493198
今日推荐