一、背景问题:
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
at android.support.v4.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:1341)
at android.support.v4.app.FragmentManagerImpl.enqueueAction(FragmentManager.java:1352)
at android.support.v4.app.BackStackRecord.commitInternal(BackStackRecord.java:595)
at android.support.v4.app.BackStackRecord.commit(BackStackRecord.java:574)
二、问题再现
写了一段可以产生如此错误的程序:
public class MainFragment extends Fragment {
static final int MSG_SHOW = 100;
public static MainFragment newInstance() {
return new MainFragment();
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.content_main, container, false);
view.findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
MyHandler handler = new MyHandler(MainFragment.this, MainFragment.this.getFragmentManager());
handler.sendEmptyMessageDelayed(MSG_SHOW, 2000);
}
});
return view;
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
Log.e("TAG", "onSaveInstanceState main");
}
static class MyHandler extends Handler {
private WeakReference<MainFragment> reference;
private FragmentManager fm;
public MyHandler(MainFragment reference, FragmentManager fm) {
this.reference = new WeakReference<MainFragment>(reference);
this.fm = fm;
}
@Override
public void handleMessage(Message msg) {
MainFragment fragment = reference.get();
switch (msg.what) {
case MSG_SHOW:
SecondFragment fr = SecondFragment.newInstance();
Log.e("TAG", "new instance");
fr.show(fragment.getFragmentManager(), "dialog"); IllegalStateException.
}
super.handleMessage(msg);
}
}
}
当点击按钮后,点击home键就会产生如上的exception。
三、原因
如错误中显示的那样,fragment
的 commit
操作发生在了 onSaveInstanceState
之后。
在应用变为不可见时,安卓系统有可能会为了释放资源而杀死后台应用。为了使应用再返回时能够不影响用户的体验,应用需要在去到后台之前保存自身的一些状态,如EditText中的输入等。这个就是 onSaveInstanceState
函数完成的事情。所以在 onSaveInstanceState
之后,试图commit的话,这个新fragment的状态就无法得到保存,那么就会造成state的丢失,可能会影响用户的体验,所以安卓系统不允许这样的事情发生,就抛出丢失state的异常了!
四、一些知识
在Honeycomb之前和之后的版本中,新加了一个change,即什么时候应用在不可见时可以被kill。在之前的版本中,onPause
之后可以被kill,也就意味着onSaveInstanceState
需在此之前调用。在之后的版本中onStop
之后才能被kill,也就意味着onSaveInstanceState
在此之前调用即可。
在support library中(能够兼容不同版本),由于Honeycomb之前的版本在 onPause
之后就会报错有些太restriction了,于是安卓团队就容许在 onPause
与onStop
之间有state loss的发生。也就是当commit在 onPause
与onStop
之间,虽然在Honeycomb之前版本会有state loss发生,但是不报错了!
看到这里有点奇怪,为什么讨论commit的时间而不是commit和onSaveInstanceState
的时间了?因为onSaveInstanceState
会在onPause
之前调用,原话是immediately before
也就认为commit如果在onPause之前的话就是在onSaveInstanceState
之前了。Honeycomb之后版本是在immediately before onStop
之前调用onSaveInstanceState
。
Version | pre-Honeycomb | post-Honeycomb |
---|---|---|
commit() before onPause() | OK | OK |
commit() between onPause() and onStop() | STATE LOSS | OK |
commit() after onStop() | EXCEPTION | EXCEPTION |
五、解决方案
- 不要在AsyncTask和Callback中去commit
- 用PauseHandler,在
onPause
时,将message保存起来,在onResume
时,再处理message。
http://stackoverflow.com/questions/8040280/how-to-handle-handler-messages-when-activity-fragment-is-paused - 最后一招,用commitAllowingStateLoss,不过如果程序退出,可能会有nullpointerexception。最好不用。
六、一些还没想透的问题
- 会用callback的情况大概可以想象,比如dialog在中显示progress。但一般什么时候会在onResume的时候commit?
- How to handle AsyncTask onPostExecute when paused to avoid IllegalStateException。
- 如下
Be careful when committing transactions inside Activity lifecycle methods. A large majority of applications will only ever commit transactions the very first time onCreate() is called and/or in response to user input, and will never face any problems as a result. However, as your transactions begin to venture out into the other Activity lifecycle methods, such as onActivityResult(), onStart(), and onResume(), things can get a little tricky. For example, you should not commit transactions inside the FragmentActivity#onResume() method, as there are some cases in which the method can be called before the activity’s state has been restored (see the documentation for more information). If your application requires committing a transaction in an Activity lifecycle method other than onCreate(), do it in either FragmentActivity#onResumeFragments() or Activity#onPostResume(). These two methods are guaranteed to be called after the Activity has been restored to its original state, and therefore avoid the possibility of state loss all together. (As an example of how this can be done, check out my answer to this StackOverflow question for some ideas on how to commit FragmentTransactions in response to calls made to the Activity#onActivityResult() method).
七、参考
- http://stackoverflow.com/questions/8040280/how-to-handle-handler-messages-when-activity-fragment-is-paused
- http://www.androiddesignpatterns.com/2013/08/fragment-transaction-commit-state-loss.html
- http://stackoverflow.com/questions/7992496/how-to-handle-asynctask-onpostexecute-when-paused-to-avoid-illegalstateexception