Android墓碑机制
本文连接地址:https://www.jianshu.com/p/f5be35aaed32
一、墓碑定义
墓碑机制是手机操作系统中的一个程序运行规则。说简单点,就是手机上一个任务被迫中断时(如有电话打入),系统记录下当前应用程序的状态后,(像把事件记录在墓碑上一样),然后中止程序。当需要恢复时,根据“墓碑”上的内容,将程序恢复到中断之前的状态。这样的一种机制就是“墓碑机制”
二、墓碑的保存与恢复
而这种方式在Android
的表示形式为:在内存不够的情况下应用进入了后台,系统会有可能杀死这个Activity
,用户切换回该应用时就会恢复当前Activity
的内容。因此出现这种状况我们该怎么处理?
针对这种状况,系统自带的View或Fragment都已经帮我们实现了状态的自动保存与恢复,但是对于自己开发的自定义View,就需要去保存状态和恢复状态,这里系统提供了两个API方便我们去实现保存和恢复,分别是onSaveInstanceState
和onRestoreInstanceState
这两个方法。
三、如何触发墓碑机制
简单说就是onSaveInstanceState
和onRestoreInstanceState
函数的调用时间
当用户按下HOME键时
长按HOME键,选择运行其他的程序时
按下电源按键(关闭屏幕显示)时
从activity A中启动一个新的activity时
屏幕方向切换时,例如从竖屏切换到横屏时
语言的切换
先说第五、六点,在屏幕切换之前,系统会销毁activity A,在屏幕切换之后系统又会自动地创建activity A,所以onSaveInstanceState()
一定会被执行,且也一定会执行onRestoreInstanceState()
。
针对第五、六点打印的数据(activity A所发生的生命周期):
MainActivity: onPause
MainActivity: onSaveInstanceState
MainActivity: onStop
MainActivity: onDestroy
MainActivity: onCreate
MainActivity: onStart
MainActivity: onRestoreInstanceState
MainActivity: onResume
回到前4点,每次触发都会调用onSaveInstanceState
,但是再次唤醒却不一定调用onRestoreInstanceState
,这是为什么呢?onSaveInstanceState
与onRestoreInstanceState
难道不是配对使用的?
首先在Android中,onSaveInstanceState
是为了预防Activity
被后台杀死的情况做的预处理,如果Activity
没有被后台杀死,那么自然也就不需要进行现场的恢复,也就不会调用onRestoreInstanceState
,而大多数情况下,Activity
不会那么快被杀死。
那么我们要如何测试这4种情况?
四、如何调试
前4种要在唤醒时候调用onRestoreInstanceState
,那前提是只有Activity
或者App被异常杀死,走恢复流程时候才会被调用。
应用是如何知道我是被异常杀死的,由于底层涉猎不深,只能大概的描述下:应用被异常杀死后在重新打开,系统底层会判断该应用是否异常退出,接着把当时现场的数据传递给它,应用拿到数据后传给Activity
,调用起onRestoreInstanceState
,这是Framework里ActivityThread
中启动Activity
的源码:
private Activity performLaunchActivity(){
...
mInstrumentation.callActivityOnCreate(activity, r.state);
r.activity = activity;
r.stopped = true;
if (!r.activity.mFinished) {
activity.performStart();
r.stopped = false;
}
if (!r.activity.mFinished) {
if (r.state != null) {
mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state);
}
}
if (!r.activity.mFinished) {
activity.mCalled = false;
mInstrumentation.callActivityOnPostCreate(activity, r.state);
}
}
可以看出,只有r.state != null
的时候,才通过mInstrumentation.callActivityOnRestoreInstanceState
回调OnRestoreInstanceState
,而r.state
就是ActivityManagerService
通过Binder
传给ActivityThread
数据,主要用来做场景恢复。
那我们要怎么测试这种情况呢?
- 开发者模式下勾选不保留活动选择
该方式是为了方便测试,在开发者模式下勾选不保留活动选择,这样应用的Activity
进入后台就不会保留,从而执行onSaveInstanceState
,再次恢复到前台执行onRestoreInstanceState
。
- 内存不足下触发OOM
先修改模拟起的内存大小,然后在打开新的Activity
里面加载大数据,不断打开新界面,这时候内存会不断增多,直到超出系统可分配的内存,导致OOM并提示错误,确认后系统会杀掉应用释放内存,这时候会重新恢复界面。
打印日志如下:
MainActivity: onCreate
MainActivity: onStart
MainActivity: onRestoreInstanceState
MainActivity: onResume
- 直接杀掉应用
按Home把当前应用放到后台,然后从Android Studio进入Devive Monitor,选择当前应用,接着选stop按钮。
如图:
这时恢复应用时就会触发onRestoreInstanceState
。
五、关于onSaveInstanceState的探讨
目前统计线上的bug,偶尔会看到这样的一个bug:
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
at android.support.v4.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:1842)
at android.support.v4.app.FragmentManagerImpl.popBackStackImmediate(FragmentManager.java:775)
at android.support.v4.app.FragmentActivity.onBackPressed(FragmentActivity.java:178)
at android.app.Activity.onKeyUp(Activity.java:2282)
at android.view.KeyEvent.dispatch(KeyEvent.java:3232)
尝试了网上各种方案,但总偶尔会出现,要解决这个bug,我们不妨先提出这几个问题:
错误是在哪里出现的
错误来源
为什么会出现这个错误
如何解决
1、错误是在哪里出现的
首先定位问题,观察源码可以发现,它是在FragmentManager
的checkStateLoss
方法里面抛出错误。
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);
}
}
2、错误来源
我们根据该方法追溯上去。
@Override
public boolean popBackStackImmediate() {
checkStateLoss();
executePendingTransactions();
return popBackStackState(mActivity.mHandler, null, -1, 0);
}
很明显的看出,popBackStackImmediate
这个出栈的方法调用之前会去检查状态是否改变,然后再去执行Fragment
操作。
继续追踪,看看到底是谁调用了。
FragmentActivity
的onBackPressed
:
public void onBackPressed() {
if (!mFragments.popBackStackImmediate()) {
supportFinishAfterTransition();
}
}
来源找到了,接着分析为什么出现错误。
3、为什么会出现这个错误
观察上述代码,产生该错误的原因是mStateSaved
变量为true
,而这个变量是从哪里设置的呢?
我们从Activity
调用onSaveInstanceState
方法开始,该方法先保存view的状态
protected void onSaveInstanceState(Bundle outState) {
outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState());
// view树的状态保存完之后,处理fragment相关的
Parcelable p = mFragments.saveAllState();
if (p != null) {
outState.putParcelable(FRAGMENTS_TAG, p);
}
getApplication().dispatchActivitySaveInstanceState(this, outState);
}
接着调用mFragments.saveAllState();
该方法里面对mStateSaved
进行的设置true操作。
Parcelable saveAllState() {
// Make sure all pending operations have now been executed to get
// our state update-to-date.
execPendingActions();
mStateSaved = true;
if (mActive == null || mActive.size() <= 0) {
return null;
}
...
}
而这个方法里面一系列操作都是保存fragment
的状态。
除了在onSaveInstanceState
中设置以外,在onStop
中也把mStateSaved
置为true
。
public void dispatchStop() {
// See saveAllState() for the explanation of this. We do this for
// all platform versions, to keep our behavior more consistent between
// them.
mStateSaved = true;
moveToState(Fragment.STOPPED, false);
}
那么什么时候才把mStateSaved
设置为false
呢。
回到Activity
的onCreate
方法里,这里可以发现它调用了Fragment
的dispatchCreate
方法,dispatchCreate
把mStateSaved
设置为false
。
protected void onCreate(@Nullable Bundle savedInstanceState) {
...
mFragments.dispatchCreate();
getApplication().dispatchActivityCreated(this, savedInstanceState);
if (mVoiceInteractor != null) {
mVoiceInteractor.attachActivity(this);
}
mCalled = true;
}
同理既然onCreate
有设置,那么resume
也有做设置
final void performResume() {
performRestart();
...
mFragments.dispatchResume();
mFragments.execPendingActions();
onPostResume();
...
}
以下几个方法是FragmentManager
源码抽取的,被上述方法调用。
public void dispatchCreate() {
mStateSaved = false;
moveToState(Fragment.CREATED, false);
}
public void dispatchStart() {
mStateSaved = false;
moveToState(Fragment.STARTED, false);
}
public void dispatchResume() {
mStateSaved = false;
moveToState(Fragment.RESUMED, false);
}
至此,我们可以知道如果onBackPressed
发生在onSavedInstanceState
之后,那么就会出现上面的crash。
4、如何解决
重载
onBackPressed
在里面做finish操作,这样可以避免使用到Fragment
api的出栈操作,因为在super.onBackPressed
方法里面调用了FragmentManager#popBackStackImmediate()
。在基类里面管理属于自己的
mStateSaved
,用它来控制是否要做onBackPressed
操作。
public class FragmentStateLossActivity extends Activity {
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) {
// 不调用super对我们意义不大,还是会崩溃,而且会丢失现场
super.onSaveInstanceState(outState);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
mStateSaved = true;
}
}
@Override
protected void onResume() {
super.onResume();
mStateSaved = false;
}
@Override
protected void onPause() {
super.onPause();
}
@Override
protected void onStop() {
super.onStop();
mStateSaved = true;
}
@Override
protected void onStart() {
super.onStart();
mStateSaved = false;
}
@Override
protected void onDestroy() {
super.onDestroy();
}
@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();
}
}
}
最后从上述问题我们可以知道:
1.为什么要在一些生命周期之前完成Fragment
的commit
操作
在
onCreate
里面完成在
onPostResume
里面完成(onPostResume
是在onResume
后调用的,确保Activity
加载完毕,mStateSaved
状态已经改变)在
onPause
之前完成(onPause
能确保在onSaveInstanceState
之前执行)
2.小心控制异步任务,尽可能避免在一些生命周期函数中使用异步方法来调用commit
,如AsyncTask
等。
3.使用commitAllowingStateLoss
,它的意思是在状态丢失是不会抛出异常,但在一些必须确保状态被保存的场合下,尽量不使用commitAllowingStateLoss
方法。它只能预防在create Fragment
时候出现的问题,但是不能解决destroy Fragment
时候出现的问题。
六、总结
1.了解了安卓的状态保存与恢复大致流程
2.如何触发安卓的状态恢复
3.解决因为安卓的状态保存导致出现的异常
参考资料
http://www.jianshu.com/p/6e3e0176f74d
http://blog.csdn.net/a553181867/article/details/54600695
http://www.androiddesignpatterns.com/2013/08/fragment-transaction-commit-state-loss.html
http://toughcoder.net/blog/2016/11/28/fear-android-fragment-state-loss-no-more/
https://stackoverflow.com/questions/7469082/getting-exception-illegalstateexception-can-not-perform-this-action-after-onsa
测试项目
https://github.com/whosea/TestSaveInstance