Activity生命周期
Activity 类是 Android 应用的关键组件,Android 系统会通过调用与其生命周期特定阶段对应的特定回调方法,在 Activity 实例中启动代码。
正常生命周期¶
正常情况下,新建一个**Activity A**会顺序经历如下几个生命周期:
-
onCreate A正在被创建,这个方法中,我们可以做一些Activity的初始化操作。例如布局文件的加载与事件的绑定(setContentView,findViewById,setOnClickListener等)。
-
onStart A 正在被启动,A 由不可见变为可见时调用,此时 A 还无法与用户交互。此时可以做一些数据的初始化操作(开启线程去拉本地数据库数据,或从后台拉数据)。
-
onResume A 已经可见,并出现在前台,该Activity位于返回栈栈顶,可以响应用户的操作,即可以与用户交互了。
如果此时用户拉起另一个**Activity B**, Activity A 会顺序经历如下几个生命周期:
-
onPause 表示 A 正在停止,准备从前台返回至后台,此时可以做一些停止动画,数据存储等工作。值得注意的是,在onPause生命周期进行的工作不能太耗时,不然会影响 B 的显示。(Activity A的onPause执行完后,Activity B的onResume才会执行)。
-
onStop 在 A 完全不可见时调用,紧随着onPause执行,表 A 即将停止,此时 A 已经不在前台,可以做一些稍微重量级的回收工作,但同样不能太耗时,(如果此时新打开的Activity B是对话框式的Activity,背景存在一定区域是透明的,则Activity A的onStop不会调用)。
-
onDestroy 表示 A 即将被销毁,在这里可以进行资源的回收、释放工作。一般是经过用户按下back键或者系统资源紧张时,将Activity A释放掉以获得更多的内存时调用。
Activity B经历了onResume生命周期后已经显示在前台,如果此时按下back返回键,从 B 页面返回,而 A 还停留在onStop,没有经过onDestroy生命周期的话,A 会经历如下几个生命周期后重新显示:
- onRestart: A 由onStop停止状态,转为运行状态时调用,表 A 正在被重新启动。
- onStart
- onResume
可以看到,排除Activity退到后台的情况,Activity从创建到销毁,总共会经过6个生命周期,分别是onCreate,onStart,onResume,onPause,onStop,onDestroy。从这几个生命周期发生时的特性来看,会发现onCreate与onDestroy、onStart与onStop、onResume与onPause是一种相反的状态。
如果该Activity有事件或服务需要注册(register),一般会在onCreate中进行,而对应的解注册操作(unregister),最好在与onCreate对应的onDestroy中完成,释放资源,这是一种良好的编程习惯。
将上述几个生命周期总结成一张图:
通过上面的文字描述,看这个图应该已经很清楚了。未提到的是,上图中onPause()还有个箭头指向了onResume(),这是一种极端情况;即考虑当Activity A 跳转到Activity B 的情况,此时 A 还在执行onPause() , B 还未显示出来。快速地从B回到A,此时会直接执行 A 的onResume()而不会走onRestart()。不过一般很难复现这种操作,大家留个心眼就行。
在将上文的细节提炼一下:
- onStart、onResume、onPause、onStop看起来回调调用的时机差不多,它们俩区别在哪呢?
onStart和onStop是从Activity是否可见的角度来回调的,而onResume和onPause则是从Activity是否位于前台、是否可以与用户交互的角度来回调的,除了这方面的差别,在时机使用过程中,它们没有其他明显区别。
- 从Activity A 跳转到Activity B,是先执行 A 的onPause(),还是先执行 B 的onResume()呢?
这部分设计Activity跳转的源码,源码逻辑太深、太复杂就不先在基础篇讨论了,大家目前 先记住结论就好:A 的onPause()会先执行,然后才执行 B 的onResume(),这个细节也是面试中可能会问到的点。
- 在onPause中不能进行耗时的操作,否则会影响新Activity的显示,稍微重一点的操作可以放在onStop中,但依然不能太耗时。
如何 Activity A 启动一个透明的 Activity B,会经历哪些生命周期呢?
这是我面试遇到的一个问题,因为 B 页面透明, 所以跳转到 B 页面后,A 页面依然可见,因此就不会调用 Activity A 的 onStop 方法。
-
一般情况,A/B 均不是透明页面:
- A 跳转 B 页面会经历的生命周期:A.onPause() -> B.onCreate() -> B.onStart() -> B.onResume() -> A.onStop。
- 从 B 页面返回 A 页面经历的生命周期:B.onPause() -> A.onRestart() -> A.onStart() -> A.onResume() -> B.onStop()。
-
B是透明页面的情况:
- 如果 B 是透明的,A 跳转到 B:A.onPause() -> B.onCreate() -> B.onStart() -> B.onResume()。
- 从 B 返回 A:B.onPause() -> A.onResume() -> B.onStop()。
异常生命周期¶
异常情况就是除开用户自己主动退出Activity的情况。
考虑一种异常情况,Activity C 打开了Actvity D后,C进入了停止状态(调用了onStop()),此时系统内存不足,需要回收 C(调用C的onDestroy()) ,当用户从 D 返回到 C,C 会被重新创建(调用onCreate())。如果原来 C 里边有临时状态存储着,比如TextView中的文字。那么从 D 返回 C 时,C因为重新创建,如果TextView未指定ID,那它原来的文字就会消失,这一定程度影响了用户的体验。
因此为了优化用户体验,Activity提供了一个onSaveInstanceState()回调方法,这个方法可以保证异常情况下,在Activity被回收之前一定会被调用。
onSaveInstanceState()方法会携带一个bundle参数,我们可以通过bundle对象,存储一些简单的状态信息。
Activity重新创建后,系统会调用onRestoreInstanceState(),并把Activity销毁时onSaveInstanceState()方法所保存的Bundle对象作为参数同时传递给onRestoreInstanceState()和onCreate()。
你可以选择这两个方法中任意一个来恢复数据,二者的区别是:onRestoreInstanceState一旦被调用,其bundle对象一定是有值的,而onCreate在正常启动Activity的情况下bundle对象是无值的。
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
}
调用时机
onSaveInstanceState()
在onStop()
之前调用,onRestoreInstanceState()
会在onStart()
之后调用。
异常情况下,Activity数据的存储和恢复的生命过程都是一样的,常见的异常情况主要有以下两种:
系统配置发生改变引起生命周期的异常变化¶
举例:开启手机的自动旋转后,当Activity从竖屏状态转变为横屏时,系统会自动销毁原先的Activity并重建。如果不做特殊处理,那么每当系统配置改变时,Activity都会销毁重建。 - 当手机从竖屏转变为横屏时,可以看到原Activity执行: onPause -> onSaveInstanceState -> onStop -> onDestroy ,走完原Activity的生命周期。
-
当原Activity销毁后又会迅速的开启新的Activity执行 :onCreate -> onStart -> onRestoreInstanceState -> onResume, 最终新的Activity显示在用户界面上。
-
onSaveInstanceState 和 onRestoreInstanceState 两个方法仅在生命周期异常情况下执行。 onSaveInstanceState主要是对异常销毁的Activity进行数据保存,onRestoreInstanceState主要是对存储的数据进行恢复,数据存取都是通过Bundle,因此我们可以在Bundle中附加个人数据进行读写。 经过测试onSaveInstanceState在onStop前调用,onRestoreInstanceState在onStart方法后调用。这两个方法执行的过程中,系统会自动对视图进行信息数据的存取,例如:ListView的滚动位置等等。
系统配置信息¶
不同手机设备的分辨率不同,要将图片适配不同大小的手机屏幕,我们通常会在drawable-xhdpi,drawable-xxhdpi,drawable-xxxhdpi等目录中存放对应大小的图片Resource文件。 当App启动时,系统就会根据当前设备的屏幕情况去加载合适的Resource资源。同一台设备的横屏和竖屏时的屏幕大小也是不一样的,如果当前Activity处于竖屏状态,突然旋转至横屏,那么此时系统的屏幕配置发生了改变。
如何避免这种因为系统配置更改而导致Activity重建的异常情况?
如果app在应用配置变更期间无需更新资源,我们可以在AndroidManifest.xml文件中相应的Activity声明,自行处理相关配置的变更,从而阻止系统重建Activity。 只需指定相关的configChanges属性。比如下面的例子,就阻止了当屏幕发生旋转时Activity的系统自动重建。
当configChanges中指定的配置发生变化时,系统会调用Activity的onConfigurationChanged()方法,如果有需要处理配置变更的话,可以在这个方法手动处理。一般我们在屏幕旋转时,希望Activity能保持原样,不重建就好了,所以空实现该方法即可。
当然,需要自行处理时,比如检查当前设备的方向,你可以这么写:
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
//TODO
} else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
//TODO
}
}
configChanges属性可以指定很多属性,如果你还想指定更多配置,不同配置间用"|"分隔,比如上面那样。
部分常用的configChanges配置项目如下:
项目 | 描述 |
---|---|
keyboard | 键盘类型发生变更 — 例如,用户插入外置键盘。 |
keyboardHidden | 键盘无障碍功能发生变更 — 例如,用户显示硬键盘。 |
locale | 语言区域发生变更 — 用户已为文本选择新的显示语言。 |
orientation | 屏幕方向发生变更 — 用户旋转设备。 |
screenLayout | 屏幕布局发生变更 — 不同的显示现可能处于活跃状态。 |
screenSize | 当前可用屏幕尺寸发生变更。 该值表示当前可用尺寸相对于当前纵横比的变更,当用户在横向与纵向之间切换时,它便会发生变更。 |
资源内存不足导致低优先级的Activity被杀死¶
这种情况就是我们分析异常情况下的生命周期时举的例子。
Activity C 跳转至Activity D,C 处于后台,当系统内存资源不足时,C的优先级较低,会被系统销毁以获得更多的内存,然后再从 D 回到 C ,C 会被重建,走一遍异常时的数据存储和恢复的生命过程。
Activity按照优先级从高到低,可以分为如下三中情况:
-
前台Activity,正在与用户交互的Activity,其优先级最高。
-
可见但非前台Activity,比如Activity中弹出了一个对话框,导致Activity可见但出于后台,无法与用户直接交互。
-
后台Activity,已经被暂停的Activity,比如执行了onStop,用户看不见,优先级最低。
当系统内存不足时,系统会按照上述优先级的顺序去杀死Activity所在的进程。并在后续通过onSaveInstanceState()和onRestoreInstanceState()去存储和恢复数据。
拓展¶
View也有onSaveInstanceState、onRestoreInstanceState()方法¶
在这两个方法中,系统自动为我们做了一定的恢复工作。当Activity在异常情况下需要重建时,系统为默认为我们保存当前Activity的视图结构,并且在重启后为我们恢复这些数据,比如文本框中用户输入的数据、ListView滚动的位置等等。
// TextView 的onSaveInstanceState()方法
@Override
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
// Save state if we are forced to
final boolean freezesText = getFreezesText();
boolean hasSelection = false;
int start = -1;
int end = -1;
if (mText != null) {
start = getSelectionStart();
end = getSelectionEnd();
if (start >= 0 || end >= 0) {
// Or save state if there is a selection
hasSelection = true;
}
}
if (freezesText || hasSelection) {
SavedState ss = new SavedState(superState);
...
ss.error = getError();
if (mEditor != null) {
// 存储editor状态
ss.editorState = mEditor.saveInstanceState();
}
return ss;
}
return superState;
}
// TextView 的onRestoreInstanceState()方法
@Override
public void onRestoreInstanceState(Parcelable state) {
if (!(state instanceof SavedState)) {
super.onRestoreInstanceState(state);
return;
}
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
// XXX restore buffer type too, as well as lots of other stuff
if (ss.text != null) {
// 调用setText,恢复数据
setText(ss.text);
}
...
}
上面是EditText竖屏和横屏切换时,text数据存储和恢复演示的效果。
View的onSaveInstanceState()调用过程是这样的:首先Activity被重建时,会调用onSaveInstanceState()去保存数据,然后Activity会委托Window去保存数据,接着Window再委托与它关联的顶层容器DecorView去保存数据,最后DecorView再遍历它一个个子View去保存数据。
View的onRestoreInstanceState()也是类似的思想。
值得注意的是,如果你希望View在异常销毁时能顺利调用onSaveInstanceState()方法,你必须得为该View指定一个id,否则View不会走到onSaveInstanceState()流程。
isSaveEnable也会影响onSaveInstanceState()的调用,这个值是个标志位,表示View是否会保存其状态。默认为true,即,默认会保存状态。
如果你不想View保存状态,可以将其设为false
竖屏与横屏切换时,状态存储的更优方案¶
接下来我将为大家介绍Jetpack库的组件ViewModel,它在设备竖屏与横屏切换时的状态存储和恢复,比传统的用onSaveInstanceState()、onRestoreInstanceState()方法更优。
JetPack库是一个由多个库组成的套件,可帮助开发者遵循最佳做法、减少样板代码,降低代码间的耦合度,并让代码在不同Android版本的设备中保持一致的体验。fragment、dataBinding、ViewModel,MVVM等都是JetPack的一部分。
大家现在基本都androidx库,在这之前,你可能还用过android-support-v7,v28库等等,为什么会有support库,就是因为随着安卓版本的迭代,一些旧的控件在新的安卓版本上难以兼容,所以为了保持体验的一致性,安卓每发一个新的版本,就会有一个support库跟着发出来。
最后发布的是android.support.v28系列库,这样的命名格式,随着安卓版本的升级,support库越来越多,谷歌也意识到这不是一个办法。因此发布了一个androidx库来统一,以后发布新的版本,只需要对androidx库进行升级迭代就可以了,就不需要再发布新的库。JetPack库是androidx库的一部分。
所以回到正题,ViewModel为啥更优?主要有以下几个优点:
- 可以存储更复杂的临时状态。
前文提到,Activity的onSaveInstanceState()主要是通过将状态存储到Bundle对象中,Bundle对象中的数据是一个个键值对,一个key对应着一个value,存储基本数据类型,如Int、Float、Boolean等很方便,但如果我们要在bundle中存储一个对象,就会很麻烦。
首先这个对象需要继承Parcelable接口,并实现几个方法,使对象能够序列化,这需要开发者编写一定的代码。
其次要从Bundle取出对象,需要经过反序列化的过程,如果你需要反序列化的对象有很多个,那么用户可能需要等待一段时间才能看到之前的状态,不仅消耗了性能,对应用的用户体验也会有一定的影响。
而ViewModel,它是一个对象,其状态是存储在内存的,存取复杂状态不需要经过序列化和反序列化的过程,性能会比从Bundle高很多。
- 将数据,也就是状态,交给ViewModel管理,可以分担Activity的工作,易于解耦,方便后期的维护。
像Activity和Fragment之类的组件,主要用于显示界面数据,对用户操作做出响应,进而给予用户UI上的反馈。比如用户按下了一个按钮,按下时按钮颜色变深。
如果要求Activity也负责从数据库或网络加载数据,这不仅需要在Activity维护加载数据的代码,也需要维护数据加载回来后的一个个异步的数据加载回调,甚至你还得考虑当Activity退出时,异步回调对象的释放工作,这会使Activity类越发膨胀,臃肿。
给Activity分配过多的责任可能会导致单个类尝试自己处理应用的所有工作,会增加测试和维护的难度。将数据加载和回调的工作交给ViewModel,将数据加载的逻辑工作与Activity的UI显示工作分离,不仅能让逻辑更清晰,同时也可以避免一个类代码量太多,太臃肿,难以维护。
ViewModel的生命周期
ViewModel对象存在的时间范围是获取ViewModel时传递给 ViewModelProvider的Lifecycle 。ViewModel 将一直留在内存中,直到限定其存在时间范围的 Lifecycle 永久消失。
比如你在Activity中通过ViewModelProvider获取了一个ViewModel,那么这个lifecycle指的就是Activity的生命周期。
我们一般会在Activity执行onCreate()方法时拿到ViewModel对象,系统可能会在activity的整个生命周期内多次调用onCreate(),比如在旋转设备屏幕时。
**ViewModel存在的时间范围是在首次请求ViewModel对象直到Activity完成并销毁。**在一个生命周期内,多次请求ViewModel对象,获取到的都是同一个ViewModel对象。
ViewModel的简单使用¶
首先要用ViewModel,需要引入它的库。
简单演示下加速器的demo,布局文件如下:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".viwemodel.ViewModelActivity">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="24sp"
android:textColor="#000"
android:gravity="center"
android:layout_centerInParent="true"/>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn_add"
android:layout_width="match_parent"
android:layout_height="46dp"
android:text="ADD NUMBER"
android:layout_below="@+id/tv_number"
android:layout_marginTop="20dp"/>
</RelativeLayout>
布局文件比较简单,就不解释了,接下来看看逻辑代码:
class ViewModelActivity : AppCompatActivity() {
private lateinit var vNumber: AppCompatTextView
private lateinit var mViewModel: UserModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_view_model)
val viewModelProvider = ViewModelProvider(this)
mViewModel = viewModelProvider.get(UserModel::class.java)
vNumber = findViewById(R.id.tv_number)
findViewById<AppCompatButton>(R.id.btn_add).setOnClickListener {
mViewModel.count++
setCounter()
}
setCounter()
}
private fun setCounter() {
vNumber.text = mViewModel.count.toString()
}
}
可以看到,activity没有指定configchanges属性,在竖屏和横屏切换时,TextView的状态也是没有丢失的。
ViewModel的介绍就到这里啦,ViewModel还有很多用途,例如在同一个Activity间的两个Fragment之间共享数据。如果配合JetPack库的另一个LiveData使用,它将更加强大。