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

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qqchenjian318/article/details/62435234

这是开发中,用户反馈很多的一个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的其他使用,建议先去看看他的默认实现里面到底保存了一些什么数值,是否会对我们造成影响。这里就不继续展开了,请各位有兴趣的话,自己进行查看。

猜你喜欢

转载自blog.csdn.net/qqchenjian318/article/details/62435234