由FragmentTabHost导致的Can not perform this action after onSaveInstanceState异常

这是开发中,用户反馈很多的一个exception。主要是Fragment的commit和commitAllowingStateLoss的问题,出现这种问题的原因很多,本次主要因为FragmentTabHost导致的该bug的发生

<Image_1>

错误日志中完全没有,我们应用的我们熟悉的那些类的堆栈信息

<Image_2>

    是不是,有种无从下手的感觉,因为没有我们自己写的类的信息,那我们需要从异常发生的地方,进行逆向分析了。

但是我们找不到FragmentManagerImpl这个类,所以我们先去FragmentManager看看,因为从命名方式看来,肯定这两个类有很大的关系

<Image_3>

我们顺利的找到了该类,原来是FragmentManager的一个实现类,找到发生错误的那个方法

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);
        }
    }

所以,我们顺利的找到了报错的地方,看来就是mStateSaved这个参数导致的异常,所以我们需要看看该参数到底是用来做什么的。通过源码,我们可以发现,是activity保存状态时,会让这个值为true。那么什么时候调用checkStateLoss这个方法呢?

 public void enqueueAction(Runnable 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<Runnable>();
            }
            mPendingActions.add(action);
            if (mPendingActions.size() == 1) {
                mHost.getHandler().removeCallbacks(mExecCommit);
                mHost.getHandler().post(mExecCommit);
            }
        }
    }

源码中的如上代码,我们可以看到,就是FragmentManager进行fragment切换的操作,这时候我们该怎么办呢?这个方法到底在哪里会被调用呢?他传递过来的这个allowStateLoss参数,是在哪里定义的呢?为什么会有true和false的区别呢?因为我们在使用Fragment的时候,其实印象中并没有用到这个参数。

所以我们需要从另外一个方向进行思考了?既然逆推理不行,那么我们就从正面突破,也就是什么情况下会进行fragment的替换了,无非就是在我们对fragment进行commit的时候,我们对fragment的使用,通常如下代码

  FragmentManager fragmentManager = getSupportFragmentManager();
  transaction = fragmentManager.beginTransaction();
  transaction.commit();

上述代码,通过FragmentManager开启了fragment的事务,然后进行commit操作,我们找到了commit的具体实现,BackStackRecord类,

<Image_4>

通过查看该类的源码,如下

    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);
            LogWriter logw = new LogWriter(TAG);
            PrintWriter pw = new PrintWriter(logw);
            dump("  ", null, pw, null);
        }
        mCommitted = true;
        if (mAddToBackStack) {
            mIndex = mManager.allocBackStackIndex(this);
        } else {
            mIndex = -1;
        }
        mManager.enqueueAction(this, allowStateLoss);//FragmentManagerImp的enqueueAction的调用之处
        return mIndex;
    }
我们发现我们顺利的找到了,FragmentTransaction的切换fragment的具体过程了,而这个allowStateLoss参数,很明显就是导致我们应用抛出异常的终极原因,那么为什么会有commit和commitAllowingStateLoss的区别呢?

    /**
     * Like {@link #commit} but allows the commit to be executed after an
     * activity's state is saved.  This is dangerous because the commit can
     * be lost if the activity needs to later be restored from its state, so
     * this should only be used for cases where it is okay for the UI state
     * to change unexpectedly on the user.
     */
    public abstract int commitAllowingStateLoss();
我们通过查看FragmentTransaction这个抽象类发现了上述一段注释

  <Image_5>

上段英文就明确的提出了,为什么在有了commit方法之后,google还为我们提供了commitAllowingStateLoss方法,而该方法和commit方法的唯一的区别就是,在activity进行了状态saved的情况下,是否还执行commit操作。而且注释也很明确的提出了,这是个很危险的操作,如果activity在之后会根据他保存的状态恢复的话。所以我们应该在保证我们ui不会出问题的情况下才使用该方法。那么虽然我们找到了发生问题的根本原因,那么下一步就是去找我们的源代码中何处既进行了activity的state的保存,又进行了commit提交的地方。


神转折

根据上面,我进行了更改,但是发现居然还继续报这个错误,那么只有一个原因,我们没有找对地方,所以我们需要再次看看最上面的堆栈信息,来找找到底是哪里导致的这个问题?

这次我们发现了另一个类FragmentTabHost,那么这是个什么类呢?通过查看源码,发现他是TabHost的子类,原来是项目中用到的TabHost+Fragment来实现子页面的切换效果,而并不是用ViewPager+Fragment进行页面切换的。那么我们去查看一下FragmentTabHost的相关源码,可以发现,他内部进行fragment的切换,其实就是通过commit的方式。

比如下面的添加tab的方法,

public void addTab(TabHost.TabSpec tabSpec, Class<?> clss, Bundle args) {
        tabSpec.setContent(new DummyTabFactory(mContext));
        String tag = tabSpec.getTag();

        TabInfo info = new TabInfo(tag, clss, args);

        if (mAttached) {
            // If we are already attached to the window, then check to make
            // sure this tab's fragment is inactive if it exists.  This shouldn't
            // normally happen.
            info.fragment = mFragmentManager.findFragmentByTag(tag);
            if (info.fragment != null && !info.fragment.isDetached()) {
                FragmentTransaction ft = mFragmentManager.beginTransaction();
                ft.detach(info.fragment);
                ft.commit();
            }
        }

        mTabs.add(info);
        addTab(tabSpec);
    }


或者下面的,Tab切换的代码,都明确的说明了这一点。

 @Override
    public void onTabChanged(String tabId) {
        if (mAttached) {
            FragmentTransaction ft = doTabChanged(tabId, null);
            if (ft != null) {
                ft.commit();
            }
        }
        if (mOnTabChangeListener != null) {
            mOnTabChangeListener.onTabChanged(tabId);
        }
    }

哈哈哈,你说我是不是很机智,那么我不就轻松找到了问题原因了吗?都找到原因了?那解决不是分分钟的事情吗?

所以,我们只需要找到我们用来承载TabHost的那个activity,然后看他进行state保存的操作是否是必须的,然后进行相应的修改,不就万事大吉了吗?

but。。。我还是太年轻了。。。因为我发现,那个activity以及他的父类都没有重写activity的如下方法,也没有其他的进行保存state的地方

 @Override
    protected void onSaveInstanceState(Bundle outState) {
	super.onSaveInstanceState(outState);
    }

are you kidding me???

这是怎么回事呢?既然没进行状态保存的话,那么为什么会出现这样的bug呢?

突然灵光一闪,好像activity是会自动进行一些参数的保存的,让我们进去看看activity的源码,我们项目是用的AppCompatActivity,然后点进去我们发现了

  @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        getDelegate().onSaveInstanceState(outState);
    }
哈哈哈 ,小样,你还跑,我找到你犯罪的证据了吧”,如此的话,我们可以通过重写onSaveInstanceState方法,但是让他空实现的方式,来避免state的保存

@Override
    protected void onSaveInstanceState(Bundle outState) {
    }
但是这样做,也是有风险的,因为activity默认是会帮助我们保存一些state信息的,这样做我们是强制性的,让他不进行state的保存,那么可能会出现一些奇奇怪怪的问题

!!!!

所以,如果你也需要这样做,那么请确保这样不会影响activity的其他使用,建议先去看看他的默认实现里面到底保存了一些什么数值,是否会对我们造成影响。这里就不继续展开了,请各位有兴趣的话,自己进行查看。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
### 回答1: 这个错误是因为在 onSaveInstanceState() 方法之后尝试执行某些操作,这是不允许的。 onSaveInstanceState() 方法是在 Activity 即将被销毁之前调用的,它的主要作用是保存 Activity 的状态信息,以便在 Activity 重新创建时恢复状态。因此,在 onSaveInstanceState() 方法之后执行某些操作可能会导致状态信息丢失或不一致。如果需要在 onSaveInstanceState() 方法之后执行某些操作,可以考虑将这些操作放在 onResume() 方法中执行。 ### 回答2: 在Android开发中,当我们的应用程序遇到设备旋转或配置更改等情况时,系统可能会销毁并重建活动,以适应新的设备状态。在这种情况下,在 onSaveInstanceState() 方法中,我们通常将一些重要的应用程序状态保存到 Bundle 对象中,以便在活动重新创建时进行恢复。但是,在 onSaveInstanceState() 方法调用后,一些操作可能会受到限制,因为活动实际上已被销毁。其中包括使用 FragmentTransaction 进行 Fragment 操作,或者提交 AsynTask 等异步操作。如果在此时尝试执行此类操作,则会导致崩溃或其他不可预测的结果。 如果您需要在 onSaveInstanceState() 方法之后执行某些操作,您可以使用简单的技巧来避免出现错误。一种方法是使用 Handler 来执行延迟操作。使用此方法,您可以先在 onSaveInstanceState() 方法中将 Runnable 对象传递给 Handler,并设置合适的延迟时间。当 Runnable 对象在指定时间后在主线程上执行时,Activity 已经重建,所以不存在任何问题。 另一个解决方案是使用 onSaveInstanceState() 方法的一种变体,即 onSaveInstanceState(Bundle, PersistableBundle) 方法。该方法与 onSaveInstanceState(Bundle) 方法不同的是,它还接收一个 PersistableBundle 参数,该参数可以在拥有足够空间的设备上持久化保存。这样,即使活动被销毁并重建,也可以恢复 PersistableBundle 中保存的状态。 总之,在 onSaveInstanceState() 方法被调用后,您应避免执行可能影响 Activity 生命周期的任何操作。如果确实需要执行此类操作,请使用上述技巧来确保正确性和稳定性。 ### 回答3: onSaveInstanceStateAndroid中一个很重要的生命周期方法之一,它通常在Activity或Fragment即将被销毁之前被调用,用于保存Activity或Fragment的状态,以便在Activity或Fragment被重建时恢复它们的状态。在这个方法被调用后,Activity或Fragment的状态已经被保存,如果继续进行操作,可能会导致状态丢失或不一致。 因此,如果在onSaveInstanceState方法被调用后再尝试执行某些操作,就会出现 can not perform this action after onSaveInstanceState 的错误信息。 例如,在Activity的onSaveInstanceState方法被调用后,如果尝试在onPause方法中执行一个Fragment的事务,就会出现这个错误。这是因为在onSaveInstanceState方法被调用后,Fragment的状态已经被保存,并且任何与Fragment相关的操作都不应该被执行,以避免状态丢失或不一致。 为了避免出现can not perform this action after onSaveInstanceState的错误,可以在onSaveInstanceState方法被调用后,避免执行任何与状态相关的操作。如果确实需要在onSaveInstanceState之后执行某些操作,可以考虑使用Handler或post方法进行延迟执行,以确保状态已经被保存并恢复,而不会出现错误。另外,可以尝试使用Fragment的setRetainInstance方法保留Fragment的实例,以避免重建时出现状态丢失的问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值