🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
#### 1.1.2 异常情况下的生命周期分析 上一节我们分析了典型情况下Activity的生命周期,本节我们接着分析Activity在异常情况下的生命周期。我们知道,Activity除了受用户操作所导致的正常的生命周期方法调度,还有一些异常情况,比如当资源相关的系统配置发生改变以及系统内存不足时,Activity就可能被杀死。下面我们具体分析这两种情况。 **1.情况1:资源相关的系统配置发生改变导致Activity被杀死并重新创建** 理解这个问题,我们首先要对系统的资源加载机制有一定了解,这里不详细分析**系统的资源加载机制**,只是简单说明一下。**拿最简单的图片来说,当我们把一张图片放在drawable目录后,就可以通过Resources去获取这张图片。同时为了兼容不同的设备,我们可能还需要在其他一些目录放置不同的图片,比如drawable-mdpi、drawable-hdpi、drawable-land等。这样,当应用程序启动时,系统就会根据当前设备的情况去加载合适的Resources资源,比如说横屏手机和竖屏手机会拿到两张不同的图片(设定了landscape或者portrait状态下的图片)。比如说当前Activity处于竖屏状态,如果突然旋转屏幕,由于系统配置发生了改变,在默认情况下,Activity就会被销毁并且重新创建**,当然我们也可以阻止系统重新创建我们的Activity。 **在默认情况下,如果我们的Activity不做特殊处理,那么当系统配置发生改变后,Activity就会被销毁并重新创建**,其生命周期如图1-3所示。 :-: ![](https://img.kancloud.cn/45/40/4540116ed4f76b0bfa3ba6bc9cf646ff_1438x757.png) 图1-3 异常情况下Activity的重建过程 当系统配置发生改变后,Activity会被销毁,其onPause、onStop、onDestroy均会被调用,同时**由于Activity是在异常情况下终止的,系统会调用onSaveInstanceState来保存当前Activity的状态。这个方法的调用时机是在onStop之前,它和onPause没有既定的时序关系,它既可能在onPause之前调用,也可能在onPause之后调用。需要强调的一点是,这个方法只会出现在Activity被异常终止的情况下,正常情况下系统不会回调这个方法。当Activity被重新创建后,系统会调用onRestoreInstanceState,并且把Activity销毁时onSaveInstanceState方法所保存的Bundle对象作为参数同时传递给onRestoreInstanceState和onCreate方法。因此,我们可以通过onRestoreInstanceState和onCreate方法来判断Activity是否被重建了,如果被重建了,那么我们就可以取出之前保存的数据并恢复,从时序上来说,onRestoreInstanceState的调用时机在onStart之后**。 同时,我们要知道,**在onSaveInstanceState和onRestoreInstanceState方法中,系统自动为我们做了一定的恢复工作。当Activity在异常情况下需要重新创建时,系统会默认为我们保存当前Activity的视图结构,并且在Activity重启后为我们恢复这些数据**,比如文本框中用户输入的数据、ListView滚动的位置等,这些View相关的状态系统都能够默认为我们恢复。具体针对某一个特定的View系统能为我们恢复哪些数据,我们可以查看View的源码。**和Activity一样,每个View都有onSaveInstanceState和onRestoreInstanceState这两个方法,看一下它们的具体实现,就能知道系统能够自动为每个View恢复哪些数据**。 关于**保存和恢复View层次结构,系统的工作流程**是这样的:**首先Activity被意外终止时,Activity会调用onSaveInstanceState去保存数据,然后Activity会委托Window去保存数据,接着Window再委托它上面的顶级容器去保存数据。顶层容器是一个ViewGroup,一般来说它很可能是DecorView。最后顶层容器再去一一通知它的子元素来保存数据,这样整个数据保存过程就完成了**。可以发现,这是一种**典型的委托思想,上层委托下层、父容器委托子元素去处理一件事情**,这种思想在**Android中有很多应用**,比如**View的绘制过程、事件分发等都是采用类似的思想**。至于数据恢复过程也是类似的,这里就不再重复介绍了。接下来举个例子,拿TextView来说,我们分析一下它到底保存了哪些数据。 源码:TextView# onSaveInstanceState ``` @Override public Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); // Save state if we are forced to boolean save = mFreezesText; int start = 0; int end = 0; if (mText ! = null) { start = getSelectionStart(); end = getSelectionEnd(); if (start >= 0 || end >= 0) { // Or save state if there is a selection save = true; } } if (save) { SavedState ss = new SavedState(superState); // XXX Should also save the current scroll position! ss.selStart = start; ss.selEnd = end; if (mText instanceof Spanned) { Spannable sp = new SpannableStringBuilder(mText); if (mEditor ! = null) { removeMisspelledSpans(sp); sp.removeSpan(mEditor.mSuggestionRangeSpan); } ss.text = sp; } else { ss.text = mText.toString(); } if (isFocused() && start >= 0 && end >= 0) { ss.frozenWithFocus = true; } ss.error = getError(); return ss; } return superState; } ``` 从上述源码可以很容易看出,TextView保存了自己的文本选中状态和文本内容,并且通过查看其onRestoreInstanceState方法的源码,可以发现它的确恢复了这些数据,具体源码就不再贴出了,读者可以去看看源码。下面我们看一个实际的例子A,**来对比一下Activity正常终止和异常终止的不同,同时验证系统的数据恢复能力。为了方便,我们选择旋转屏幕来异常终止Activity**,如图1-4所示。 **示例A** :-: ![](https://img.kancloud.cn/ab/b3/abb3db2821c808c7edcf3fad524c7e22_1436x508.png) 图1-4 Activity旋转屏幕后数据的保存和恢复 通过**图1-4可以看出,在我们选择屏幕以后,Activity被销毁后重新创建,我们输入的文本“这是测试文本”被正确地还原,这说明系统的确能够自动地做一些View层次结构方面的数据存储和恢复**。下面再用一个例子B,来验证我们自己做数据存储和恢复的情况,代码如下: **示例B** @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if (savedInstanceState ! = null) { String test = savedInstanceState.getString("extra_test"); Log.d(TAG, "[onCreate]restore extra_test:" + test); } } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); Log.d(TAG, "onSaveInstanceState"); outState.putString("extra_test", "test"); } @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); String test = savedInstanceState.getString("extra_test"); Log.d(TAG, "[onRestoreInstanceState]restore extra_test:" + test); } 上面的代码很简单,首先我们**在onSaveInstanceState中存储一个字符串,然后当Activity被销毁并重新创建后,我们再去获取之前存储的字符串。接收的位置可以选择onRestoreInstanceState或者onCreate**,二者的区别是:**onRestoreInstanceState一旦被调用,其参数Bundle savedInstanceState一定是有值的,我们不用额外地判断是否为空;但是onCreate不行,onCreate如果是正常启动的话,其参数Bundle savedInstanceState为null,所以必须要额外判断。这两个方法我们选择任意一个都可以进行数据恢复,但是官方文档的建议是采用onRestoreInstanceState去恢复数据**。下面我们看一下运行的日志,如图1-5所示。 :-: ![](https://img.kancloud.cn/5f/87/5f87fd555a2c8d9d6b8a92a8a5e77396_1439x336.png) 图1-5 系统日志 如图1-5所示,**Activity被销毁了以后调用了onSaveInstanceState来保存数据,重新创建以后在onCreate和onRestoreInstanceState中都能够正确地恢复我们之前存储的字符串**。这个例子很好地证明了上面我们的分析结论。**针对onSaveInstanceState方法**还有一点需要说明,那就是**系统只会在Activity即将被销毁并且有机会重新显示的情况下才会去调用它**。考虑这么一种情况,**当Activity正常销毁的时候,系统不会调用onSaveInstanceState,因为被销毁的Activity不可能再次被显示**。这句话不好理解,但是我们可以对比一下**旋转屏幕所造成的Activity异常销毁,这个过程和正常停止Activity是不一样的,因为旋转屏幕后,Activity被销毁的同时会立刻创建新的Activity实例,这个时候Activity有机会再次立刻展示,所以系统要进行数据存储。这里可以简单地这么理解,系统只在Activity异常终止的时候才会调用onSaveInstanceState和onRestoreInstanceState来存储和恢复数据,其他情况不会触发这个过程**。 >[info]注意:但是按Home键后者启动新的Activity仍然会单独触发onSaveInstanceState的调用,当然,这种情况和上面所说的异常终止Activity也不一样。 **2.情况2:资源内存不足导致低优先级的Activity被杀死** 这种情况我们不好模拟,但是其数据存储和恢复过程和情况1完全一致。这里我们描述一下Activity的优先级情况Activity按照优先级从高到低,可以分为如下三种: * (1)**前台Activity**——正在和用户交互的Activity,**优先级最高**。 * (2)**可见但非前台Activity**——比如Activity中弹出了一个对话框,导致Activity可见但是位于后台无法和用户直接交互。 * (3)**后台Activity**——已经被暂停的Activity,比如执行了onStop,**优先级最低**。 **当系统内存不足时,系统就会按照上述优先级去杀死目标Activity所在的进程,并在后续通过onSaveInstanceState和onRestoreInstanceState来存储和恢复数数据。如果一个进程中没有四大组件在执行,那么这个进程将很快被系统杀死,因此,一些后台工作不适合脱离四大组件而独自运行在后台中,这样进程很容易被杀死。比较好的方法是将后台工作放入Service中从而保证进程有一定的优先级,这样就不会轻易地被系统杀死**。 上面分析了系统的数据存储和恢复机制,我们知道,**当系统配置发生改变后,Activity会被重新创建,那么有没有办法不重新创建呢?答案是有的**,接下来我们就来分析这个问题。**系统配置中有很多内容,如果当某项内容发生改变后,我们不想系统重新创建Activity,可以给Activity指定configChanges属性。比如不想让Activity在屏幕旋转的时候重新创建,就可以给configChanges属性添加orientation这个值**,如下所示。 ``` android:configChanges="orientation" ``` **如果我们想指定多个值,可以用“|”连接起来**,比如android:configChanges="orientation|keyboardHidden"。系统配置中所含的项目是非常多的,下面介绍每个项目的含义,如表1-1所示。 :-: 表1-1 configChanges的项目和含义 ![](https://img.kancloud.cn/93/c0/93c05be329eb581e8bd1778b996ad507_1012x742.png) 从表1-1可以知道,**如果我们没有在Activity的configChanges属性中指定该选项的话,当配置发生改变后就会导致Activity重新创建。上面表格中的项目很多,但是我们常用的只有locale、orientation和keyboardHidden这三个选项,其他很少使用**。 >[info]需要注意的是screenSize和smallestScreenSize,它们两个比较特殊,它们的行为和编译选项有关,但和运行环境无关。 下面我们再看一个demo,看看当我们指定了configChanges属性后,Activity是否真的不会重新创建了。我们所要修改的代码很简单,只需要在AndroidMenifest.xml中加入Activity的声明即可,代码如下: <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="19" /> <activity android:name="com.ryg.chapter_1.MainActivity" android:configChanges="orientation|screenSize" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); Log.d(TAG, "onConfigurationChanged, newOrientation:" + newConfig. orientation); } 需要说明的是,由于编译时笔者指定的minSdkVersion和targetSdkVersion有一个大于13,所以为了防止旋转屏幕时Activity重启,除了orientation,我们还要加上screenSize,原因在上面的表格里已经说明了。其他代码还是不变,运行后看看log,如图1-6所示。 :-: ![](https://img.kancloud.cn/d6/da/d6dac9e19234330de6b9b246121fa3f1_1435x211.png) 图1-6 系统日志 由上面的日志可见,**Activity的确没有重新创建,并且也没有调用onSaveInstanceState和onRestoreInstanceState来存储和恢复数据,取而代之的是系统调用了Activity的onConfigurationChanged方法,这个时候我们就可以做一些自己的特殊处理了**。