跳转至

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的系统自动重建。

<activity android:name=".MainActivity"
        android:configChanges="orientation|screenSize" />

当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

<EditText
        android:id="@+id/edit_text"
        ...
        android:saveEnabled="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,需要引入它的库。

// ViewModel and LiveData
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'

简单演示下加速器的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使用,它将更加强大。

评论