这是开发中,用户反馈很多的一个exception。主要是Fragment的commit和commitAllowingStateLoss的问题,出现这种问题的原因很多,本次主要因为FragmentTabHost导致的该bug的发生
错误日志中完全没有,我们应用的我们熟悉的那些类的堆栈信息
是不是,有种无从下手的感觉,因为没有我们自己写的类的信息,那我们需要从异常发生的地方,进行逆向分析了。
但是我们找不到FragmentManagerImpl这个类,所以我们先去FragmentManager看看,因为从命名方式看来,肯定这两个类有很大的关系
我们顺利的找到了该类,原来是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();
}
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类,
通过查看该类的源码,如下
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这个抽象类发现了上述一段注释
上段英文就明确的提出了,为什么在有了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);
}
这是怎么回事呢?既然没进行状态保存的话,那么为什么会出现这样的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的其他使用,建议先去看看他的默认实现里面到底保存了一些什么数值,是否会对我们造成影响。这里就不继续展开了,请各位有兴趣的话,自己进行查看。