• 从源码角度理解Can not perform this action after onSaveInstanceState异常


    在开发中经常遇到Fatal Exception: java.lang.IllegalStateException:Can not perform this action after onSaveInstanceState异常,那这个异常出现原因是什么呢,怎么解决呢?

    问题描述
    出现Fatal Exception: java.lang.IllegalStateException:Can not perform this action after onSaveInstanceState异常有两种情况:

    1. FragmentTransaction的commit()时出现:
      具体堆栈信息如下:

    2. Activity/FragmentActivity的onBackPressed时出现:
      具体堆栈信息如下:

    问题原因和解决方法
    出现Fatal Exception: java.lang.IllegalStateException:Can not perform this action after onSaveInstanceState异常细分为两种情况,但产生原因是一样的,都是因为在存储状态之后调用了commit()/onBackPressed()方法,在commit()/onBackPrssed()中会调用checkStateLoss()方法,具体如下:

    private void checkStateLoss() {
        if (this.mStateSaved) {
          throw new IllegalStateException("Can not perform this action after onSaveInstanceState");
        } else if (this.mNoTransactionsBecause != null) {
          throw new IllegalStateException("Can not perform this action inside of " + this.mNoTransactionsBecause);
        }
      }
    
    

    其中如果this.mStateSaved为true,就会抛出这个异常。而对于mStateSaved用了保存Fragment状态,是在Activity#onSaveInstanceState时通过调用FragmentManager#saveAllState方法,来进行Fragment的状态保存,同时设置mStateSaved为true,用来标识状态已被保存过。下面具体分析两种情况:
    1.FragmentTransaction的commit()时出现:
    FragmentTransaction的commit()会调用BackStackRecord.java的commit()方法,可以看到commit()和commitAllowingStateLoss()的具体实现,如下:
    BackStackRecord.java

    @Override
        public int commit() {
            return commitInternal(false);
        }
    
        @Override
        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);
                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;
        }
    

    可以看出commit()和commitAllowingStateLoss()都是调用commitInternal()方法,只是传参不一样,而在commitInternal()方法中,根据传参会调用FragmentManager的enqueueAction方法,具体如下:

    public void enqueueAction(OpGenerator action, boolean allowStateLoss) {
            if (!allowStateLoss) {
                checkStateLoss();
            }
            synchronized (this) {
                if (mDestroyed || mHost == null) {
                    throw new IllegalStateException("Activity has been destroyed");
                }
                if (mPendingActions == null) {
                    mPendingActions = new ArrayList<>();
                }
                mPendingActions.add(action);
                scheduleCommit();
            }
        }
    

    如果allowStateLoss为true的话会调用checkStateLoss()方法进行检测。所以从源码可知commit()和commitAllowingStateLoss()的区别在于,commit()方法会检测fragment的状态,而commitAllowingStateLoss()不会对状态进行检测,状态会丢失。
    由此得到结论和解决方法:
    (1)在activity的生命周期方法中提交事务要小心,越早越好,比如onCreate或是在接收用户的输入时来提交。尽量避免在onActivityResult()方法中提交。
    (2)避免在异步的回调方法中执行commit。因为他们感知不到当前activity生命周期的状态。
    (3)如果ui状态的改变对用户来说是可以接受的话使用commitAllowingStateLoss()代替commit()。
    2.Activity/FragmentActivity的onBackPressed时出现:
    按返回键时会调用super.onBackPressed()方法,如下:
    FragmentActivity.java中

    @Override
        public void onBackPressed() {
            if (!mFragments.getSupportFragmentManager().popBackStackImmediate()) {
                super.onBackPressed();
            }
        }
    

    会调用FragmentManagerImpl的popBackStackImmediate()方法,如下:

    @Override
        public boolean popBackStackImmediate() {
            checkStateLoss();
            return popBackStackImmediate(null, -1, 0);
        }
    

    会调用checkStateLoss()方法,同上,会检测状态。
    那这个问题怎么解决呢?
    (1)添加try…catch,感觉这种方法并没有解决根本问题,不推荐。
    (2)重写onSaveInstanceState方法,不调用super,但onSaveInstanceState方法是用来存储状态使用的,不调用super,防止系统保存fragment的状态,可能会引发一引起其他的问题;再有就是,对于support包下,在其onStop时也会把mStateSaved置为true,仍然能够遇到state loss exception。不推荐。
    (3)设置标志位,状态保存过后,不处理KEY事件。具体如下:

    public class FragmentStateLossActivity extends Activity {
        private static final String TAG = "Fragment state loss";
        private boolean mStateSaved;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_fragment_state_loss);
            mStateSaved = false;
        }
    
        @Override
        protected void onSaveInstanceState(Bundle outState) {
            // Not call super won't help us, still get crash
            super.onSaveInstanceState(outState);
            imStateSaved = true;
        }
    
        @Override
        protected void onResume() {
            super.onResume();
            mStateSaved = false;
        }
    
        @Override
        protected void onStop() {
            super.onStop();
            mStateSaved = true;
        }
    
        @Override
        protected void onStart() {
            super.onStart();
            mStateSaved = false;
        }
    
        @Override
        public boolean onKeyDown(int keyCode, KeyEvent event) {
            if (!mStateSaved) {
                return super.onKeyDown(keyCode, event);
            } else {
                // State already saved, so ignore the event
                return true;
            }
        }
    
        @Override
        public void onBackPressed() {
            if (!mStateSaved) {
                super.onBackPressed();
            }
        }
    }
    

    但这种方法虽然能从根本上解决crash,但相对比较麻烦。
    目前还没有发现更好的方法。

    参考
    https://juejin.im/entry/58636864128fe10069efc4b5
    https://huxian99.github.io/2016/08/28/cj3qymo360000owxk9zp17alo/

  • 相关阅读:
    shell 冒泡算法 解决数组排序问题
    react(二)
    react(一)
    mybatis-plus:3
    lambda
    配置中心(nacos)-控制台使用
    nacos作为配置中心的简单项目配置
    nacos作为注册中心的简单项目配置
    Vue初步学习
    nacos单例模式简单搭建
  • 原文地址:https://www.cnblogs.com/sishuiliuyun/p/14294353.html
Copyright © 2020-2023  润新知