唠唠 Activity 的生命周期

2021-08-31 15:33:29 浏览数 (1)

Android 复习笔记目录

  1. 唠唠任务栈,返回栈和生命周期
  2. 唠唠 Activity 的生命周期

上一篇文章唠了唠 任务栈,返回栈和启动模式,今天来聊一聊同样和 Activity 息息相关的 生命周期

关于 Activity 的生命周期,我相信大家倒着都可以说出来了。这里放一张 android-lifecycle 里的经典大图,其中也包含了 Fragment 的生命周期图。

由于这是一张 2014 年的图,注意 onRestoreInstanceState()onSaveInstanceState() 的调用时机会因为 SDK 版本的不同稍有变化,文章后面也会提到。

目录

  • 每个生命周期做了什么?
  • onStart/onStop ?还是 onResume/onPause ?
  • 如何进行 UI 状态的存储与恢复?
  • Activity 和应用进程的关系
  • 在什么时机触发 LeakCanary 的检测?
  • 被 SharedPreference 拖累的 Activity

每个生命周期做了什么?

onCreate()

这是 Activity 的第一个生命周期方法,其中必须要做的操作就是 setContentView()

setContentView() 里面大概做了这么几件事:

  1. 创建 DecorView,并设置 PhoneWindow
  2. 解析 xml 布局文件,生成 View 对象并塞到 DecorView 中

此时 DecorView 并没有被绘制,Window 对象也没有被显示到屏幕,Activity 也是不可见的。

除此之外,开发者通常也会在 onCreate() 方法中做一些数据的初始化操作。

onCreate() 在一次完整的生命周期中只会回调一次,它也不是一个长驻状态,完成工作只会就会进入 onStart()

onStart()

onStart() 也不是一个长驻状态,官方文档对于它的描述是这样的:

The onStart() call makes the activity visible to the user, as the app prepares for the activity to enter the foreground and become interactive.

onStart() 方法中,Activity 对用户可见,应用准备进入前台和用户交互。我对这句 Activity 对用户可见 其实抱有很大的疑问。

不考虑特殊情况,正常启动一个 Activity,onCreate -> onStart ,此时所谓的 “可见”,见到的是什么?

这个问题在下面的 onResume 一节中会详细说明,读者可以先仔细揣摩一下。

onStart() 方法中可以做些什么呢?通常会和 onStop() 搭配做一些资源申请和释放的工作,例如相机的申请和释放。

onResume

熟悉 UI 绘制流程的读者肯定知道,onResume() 是真正进行 UI 绘制以及显示的地方。其中的核心逻辑就是 WindowManager.addView() 方法,实际调用的是 WindowManagerGlobal.addView() 方法。它大致干了这么几件事:

  1. 创建 ViewRootImpl 对象
  2. 调用 ViewRootImpl.setView() 方法

ViewRootImpl 的构造函数中做了这么几件事:

  1. 初始化跟 WMS 通信的 WIndowSession 对象,这是一个 Binder 对象
  2. 初始化 Choreographer 对象

ViewRootImpl.setView() 方法做了这么几件事:

  1. 调用 requestLayout() 方法,发起绘制
  2. Binder 调用 WMS.addToDisplay() 方法,将 window 添加到屏幕

requestLayout() 方法中就会进行我们所熟知的 测量、布局和绘制 流程,但并不是直接进行的,它依赖 vsync 信号。requestLayout() 只是通过前面已经初始化了的 Choreographer 对象进行注册监听,当下一个 vsync 信号来临时,会回调 performTraversals() 方法,这其中就会真正的进行测量、布局和绘制。

整个 UI 绘制流程的知识点很多,仅靠以上简单一段文字肯定是无法完全概括的,感兴趣的读者可以自己去翻翻源码。但是我们可以肯定是,onResume 是真正的用户界面可见的时机。

再回到之前的问题,onStart 中可见的是什么?我也无法回答这个问题,或者可能大家都曲解了官方文档的意思,是否应该理解为 “Activity 即将可见”。大家可以在留言区说说你的看法。

同样,onResume() 通常也可以和 onPause() 搭配做一些资源申请和释放的工作。那么,既然 onStart/onStoponResume/onPause 都可以,该如何选择呢?同样放到后面进行解答。

onPause

onPause() 是一个很短暂的过程,之后如果用户返回了之前的 Activity,则会回调 onResume 。如果没有,则会回调 onStop

onPause 一个精准的描述的话,应该是 非前台,不可交互,但不一定不可见 。对于系统来说,无论是手机还是 PC ,同一个时间一定只有一个处于前台,获取焦点,且可与用户交互的活动窗口,所以 非前台,不可交互 很好理解。那 不一定不可见 如何理解呢?其实也很简单,类似 PC 的多窗口,Android 系统也是有多窗口模式的。

最后,注意 onPause 中不建议进行重量级的耗时操作,因为在 Activity 跳转过程中,前一个 Activity 的 onPause() 是发生在后一个 Activity 的任何生命周期之前的。

onStop

完全的不可见状态。但此时的 Activity 仍在内存中,只是没有关联到任何 Window 。如果后续有机会再次返回,则会回调 onRestart -> onStart

onDestroy

onStop 之后没有被用户捞回去,最后就得被销毁。主动的调用 finish 或者系统配置改变也会可能导致销毁。

注意在 onDestroy 中释放所有不需要的资源,否则可能导致内存泄露。

到目前为止,简单介绍了各个生命周期的回调时机和应该处理的事情,同时也带了一些疑问。下面来说说一些知识点。

onStart/onStop ?还是 onResume/onPause ?

在使用 EventBus 的时候,需要在相应的声明周期中进行注册和解注册的操作,如下所示:

代码语言:javascript复制
@Override
public void onStart() {
    super.onStart();
    EventBus.getDefault().register(this);
}

@Override
public void onStop() {
    super.onStop();
    EventBus.getDefault().unregister(this);
}

当然,在 onResume/onPause 中一般也是没有问题的。

代码语言:javascript复制
@Override
public void onResume() {
    super.onStart();
    EventBus.getDefault().register(this);
}

@Override
public void onPause() {
    super.onStop();
    EventBus.getDefault().unregister(this);
}

但它们之间并不是完全没有区别的。通常情况下,onPause 之后很快就会 onStop,但是考虑到 Android 7.0 之后新增的 多窗口模式 的话,Activity 可能会停留在 onPause 一段时间。这种情况下,如果在 onStop 中进行资源释放操作的话,可能并不能及时释放。如果你的 Activity 持有的是相机等系统资源,会导致其他应用无法使用该资源,对用户来说无疑是很不友好的。所以,在进行类似操作的时候要考虑一下应用场景。onResume/onPause 关注的是 Activity 是否可以交互,onStart/onStop 关注的是 Activity 是否可见。

最后得说下,上面的代码中把生命周期处理和视图控制器耦合在一起,并不是那么优雅,可以通过 LifeCycle 组件进行解耦。拿我之前文章中的代码说明一下:

代码语言:javascript复制
class LocationUtil( ) : LifeCycleObserver {
  @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
  fun startLocation( ){
    ......
  }

  @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
  fun stopLocation( ){
    ......
  }
}

传送门:

硬核讲解 Jetpack 之 LifeCycle 使用篇 硬核讲解 Jetpack 之 LifeCycle 源码篇

如何进行 UI 状态的存储与恢复?

除了正常状态下的数据持久化存储,异常情况下的数据保存和恢复也是必要的。这里的异常情况一般指系统配置变化,典型的横竖屏切换,系统语言切换等。

异常情况下终止的 Activity,系统会调用 onSaveInstanceState() 方法来保存当前 Activity 的状态。那么哪些状态默认会被保存呢?我们可以看一下 TextViewonSaveInstanceState() 方法。

代码语言:javascript复制
@Override
public Parcelable onSaveInstanceState() {
    Parcelable superState = super.onSaveInstanceState();

    ......

    if (freezesText || hasSelection) {
        SavedState ss = new SavedState(superState);

        if (freezesText) {
            if (mText instanceof Spanned) {
                final Spannable sp = new SpannableStringBuilder(mText);

                if (mEditor != null) {
                    removeMisspelledSpans(sp);
                    sp.removeSpan(mEditor.mSuggestionRangeSpan);
                }
                // 保存文字
                ss.text = sp;
            } else {
                ss.text = mText.toString();
            }
        }

        ...
        return ss;
    }

    return superState;
}

可以看到文字是保存了的,这里删减了很多代码,其实还保存了其他一些状态。只要是实现了 onSaveInstanceState()方法的 View,都会被保存下来。当 Activity 重新创建时,会通过 onCreate() 或者 onRestoreInstanceState() 方法恢复状态。

代码语言:javascript复制
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
}

override fun onRestoreInstanceState(savedInstanceState: Bundle) {
    super.onRestoreInstanceState(savedInstanceState)
}

在 Kotlin 中重写这两个方法也可以看到,onCreate() 方法的参数是可空的,因为会有正常启动的情况。所以一般建议直接在 onRestoreInstanceState 方法中进行状态恢复即可。

对于非 UI 状态的其他数据,就得自己手动进行保存和恢复了。这里直接拿官方文档的示例代码:

代码语言:javascript复制
override fun onSaveInstanceState(outState: Bundle?) {
    // Save the user's current game state
    outState?.run {
        putInt(STATE_SCORE, currentScore)
        putInt(STATE_LEVEL, currentLevel)
    }

    // Always call the superclass so it can save the view hierarchy state
    super.onSaveInstanceState(outState)
}qubie

override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
    // Always call the superclass so it can restore the view hierarchy
    super.onRestoreInstanceState(savedInstanceState)

    // Restore state members from saved instance
    savedInstanceState?.run {
        currentScore = getInt(STATE_SCORE)
        currentLevel = getInt(STATE_LEVEL)
    }
}

要注意的是,通过 onSaveInstanceState 的方式存储数据,由于是发生在主线程,且存在序列化/反序列化的开销,并不建议存储大量数据。其实更好的做法是使用 ViewModel ,它可以在系统配置变化发生的 Activity 重建过程中来保存数据。

最后来说一下 onSaveInstanceState 的调用时机问题。在不同的 SDK 版本中,这个时机是不唯一的。

  • SDK 11 之前,在 onPause() 之前调用
  • SDK 28 之前,会在 onStop() 之前调用
  • SDK 28 之后,会在 onStop 之后调用

当然,这对我们来说并没有什么实质的区别。

Activity 和应用进程的关系

当系统内存不足时,会存在单个 Activity 直接被系统回收的情况吗?

答案是否定的。

首先应用进程的生存时间并不是由自己直接控制的,而是由系统决定的。每一个 App 都至少对应着一个 Linux 进程。当系统内存不足无法满足正在与用户交互的进程的需求时,可能会回收一些资源。这些资源是一个一个进程,而不是进程里的一个一个组件,不会存在单个 Activity 被系统回收的情况。

那么,既然要回收进程,那么肯定会给进程分个三六五等。按照官方文档的描述,大致有这么几类,重要性依次降低。

Foreground Process :

  • 有 Activity 处于前台,正在和用户交互
  • BroadcastReceiver 的 onReceive 方法正在执行
  • Service 正在运行代码,包括 onCreate,onStart,onDestroy

Visible Process :

  • 有 Activity 可见,但不在前台
  • Service 正在运行前台服务
  • 持有一些用户可以感知的特定服务,如动态壁纸,输入法服务

Service Process :

有正在运行的 Service ,对用户不可见,但正在进行一些用户关心的工作,例如后台下载等。当系统内存不足的时候,可能会被干掉。

官方文章说运行超过 30 min 可能会被降级,但这个在不同的国产 ROM 肯定是有魔改的,具体应该以实际测试为准。

Cached Process :

当系统内存不足时可以随时自由终止的无用线程。但是为了更高效的切换应用,系统一般不会把它们全部 kill 掉。

在什么时机触发 LeakCanary 的检测?

之前群里的小伙伴 codelang 分享了一个有趣的知识点:

为什么 LeakCanary 要在 Activity onDestroy 之后调用 RefWatcher.watch() 开始监测内存泄露?

他给出的答案是:

从 ActivityThread 的角度看,Activity 就是一个对象,按照 GC Root,Activity 是被 ActivityThread 给引用着的。为什么要在 onDestroy 之后开始检测,因为这个时候 Activity 和 ActivityThread 的引用断开了,在 ActivityThread.performDestroyActivity() 中从 mActivities 中移除了当前 Activity 对象。

的确,在 onDestroy() 之前,Activity 根本没有断开和 GC Roots 的引用,检测个啥呢。

被 SharedPreference 拖累的 Activity

之前写过一篇 细数 SharedPreference 的槽点 来吐槽 SP 。不合理的使用 SP 可能会导致卡顿,甚至 ANR 。

apply() 方法和 commit() 方法不同,它是通过异步任务来进行存储操作的,通过 QueuedWork 类实现。

代码语言:javascript复制
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);

而在 onStop 中,需要等待这个异步任务的完成。看一下 ActivityThread.javahandleStopActivity() 方法:

代码语言:javascript复制
@Override
public void handleStopActivity(IBinder token, boolean show, int configChanges,
        PendingTransactionActions pendingActions, boolean finalStateRequest, String reason) {

  ......

    // 可能因等待写入造成卡顿甚至 ANR
    if (!r.isPreHoneycomb()) {
        QueuedWork.waitToFinish();
    }

  ......
}

无论修改了什么数据,这里的存储都是全量写入,如果数据量过大,一定是会存在性能问题的。此外还要注意 SP 的舒适化过程也是全量读取放到内存中,所以在数据量大的情况下,注意提前初始化。

最后

关于 Activity 的生命周期就说这么多了,后面如果碰到相关的有意思的问题,再回来补充。


都看到这了,不妨关注我一下呗!

0 人点赞