关于 Activity & View 生命周期的解惑

2019-05-23  本文已影响0人  你可记得叫安可

App 内存不足时,系统会回收 Activity 吗?

Android 在运行过程中发现内存不足,会杀掉一些后台进程,来获取内存,这个过程称为内存回收。如果后台进程都杀光了,内存还是不够,此时可能有 2 种表现:1. 跳出OOM崩溃;2. 杀死前台进程。并不会发生回收某个或某些activity的行为。

onTrimMemory

Android 对内存情况也提供了精细的回调信息。ComponentCallbacks2

import android.content.ComponentCallbacks2
// Other import statements ...

class MainActivity : AppCompatActivity(), ComponentCallbacks2 {

    // Other activity code ...

    /**
     * Release memory when the UI becomes hidden or when system resources become low.
     * @param level the memory-related event that was raised.
     */
    override fun onTrimMemory(level: Int) {

        // Determine which lifecycle or system event was raised.
        when (level) {

            ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN -> {
                /*
                   Release any UI objects that currently hold memory.

                   The user interface has moved to the background.
                */
            }

            ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE,
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW,
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> {
                /*
                   Release any memory that your app doesn't need to run.

                   The device is running low on memory while the app is running.
                   The event raised indicates the severity of the memory-related event.
                   If the event is TRIM_MEMORY_RUNNING_CRITICAL, then the system will
                   begin killing background processes.
                */
            }

            ComponentCallbacks2.TRIM_MEMORY_BACKGROUND,
            ComponentCallbacks2.TRIM_MEMORY_MODERATE,
            ComponentCallbacks2.TRIM_MEMORY_COMPLETE -> {
                /*
                   Release as much memory as the process can.

                   The app is on the LRU list and the system is running low on memory.
                   The event raised indicates where the app sits within the LRU list.
                   If the event is TRIM_MEMORY_COMPLETE, the process will be one of
                   the first to be terminated.
                */
            }

            else -> {
                /*
                  Release any non-critical data structures.

                  The app received an unrecognized memory level value
                  from the system. Treat this as a generic low-memory message.
                */
            }
        }
    }
}

onSaveInstanceState、onRestoreInstance 与 activity 恢复

我们都知道在 onSaveInstanceState 里保存数据,而在 onRestoreInstanceState 里恢复数据。但具体是怎样的呢?
onSaveInstanceState 是在 activity 未来可能被系统回收时被调用:


场景:进程被系统回收后,在最近使用程序列表,或者在主页程序列表点击 icon 再次启动应用,系统会重启进程并恢复 activity

比如我们的栈顶 activity 中有一个 EditText,里面填了 "yy",进程被回收之后,我们在最近程序列表里点击应用,进程会重启,然后恢复 activity。此 EditText 会恢复 "yy",这是如何做到的?不需要我们写一行代码,activity 本身就能够恢复 EditText 的值,简单的说就是在回收进程之前会通过 onSaveInstanceState 来保存数据(Activity 调用了 View.onSaveInstanceState),然后进程启动,activity 重新启动的过程中调用 onRestoreInstanceState 来恢复数据(Activity 调用了 View.onRestoreInstanceState)。

我们来看看这个场景的生命周期:

  1. 启动 activity:onCreate(null: Bundle) → onStart → onResume
  2. 点击 home,将进程变为后台进程,注意 onSaveInstanceState 被调用
    2.1 onPause → onStop → onSaveInstanceState
    2.2 有人认为杀进程时才调用 onSaveInstanceState,事实上,任何使得 activity 变为 stopped state,大部分会调用 onSaveInstanceState。例外情况:按 back 键或者调用 finish() 导致的 stopped state 不会调用 onSaveInstanceState,因为 activity 就要销毁了,所以不需要恢复。其他的情况导致 activity 变为 stoppedState 都会调用 onSaveInstanceState,比如切换到其他 activity,按 home,锁屏,旋转等等
    2.3 onSaveInstanceState 和 onPause 前后关系不定:在api 11之前,onSaveInstanceState 回调是在 onPause 之前;api11之后调整到了 onPause 之后,onStop 之前。 目前我测试的 Android P 上是在 onStop 之后。
  3. 进程被杀死(通过ddms, 系统内存不足,adb kill pid (需要 root)、setting 中应用管理器等等),很暴力,不会有任何生命周期函数被调用(但是从最近程序列表中删除进程,Activity 的 onDestroy 会被调用,因为这是用户主动行为。用户主动回收 activity 的行为(按 back,调用 finish() 方法)会导致 Activity.onDestroy() 被调用)
  4. 从最近程序列表中打开刚才的进程,进程会再次启动,activity 会恢复:onCreate(savedInstanceState: Bundle) → onStart → onRestoreInstance → onResume。这里的 onRestoreInstance 只有在进程之前被系统杀死过后,被恢复时才会有。
    4.1. 此时的启动和第一次的启动主要有 2 点不一样
    - onCreate 的参数不再是 null,而是有 savedInstanceState 数据了,在 onCreate 的时候就会去读数据
    - onStart 之后,onResume 之前会调用一个 onRestoreInstanceState,在这个 onRestoreInstanceState 里,真正地把控件的相关数据给恢复。这里可以注意,onPause 和 onResume 是一对,onStart 和 onStop 是一对,因此 onSaveInstanceState 和 onRestoreInstanceState 也是一对,在前面连个之间,显得很恰当。

Q: 当两个activity切换的时候,是第二个 activity 调用 onResume 后,才调用第一个 activity 的onStop,那现在第一个 activity 多了一个 onSaveInstanceState,那它应该在哪里呢?
A: onPause → onCreate → onState → onResume → onStop → onSaveInstanceState

onSaveInstanceState 在后一个 activity 的 onResume 之后,这样设计是为了,onSaveInstanceState 不会影响切换的流畅性。

Activity 恢复原则

Q: 如果 app 被系统杀死后再恢复 app 进程时,app 有多个 activity 呢?多个 activity 会被一起恢复吗?
A: 不会,只会恢复栈顶的activity,但是栈是恢复了的,在按 back 键后,会创建倒数第二个 activity 实例。举个例子,我们现在有 MainActivity、SecondActivity、ThirdActivity 三个 activity,栈顶是 ThirdActivity。然后进程被回收,之后用户从最近列表点击,导致进程重启,activity 恢复,第一步是恢复 ThirdActivity(栈顶的)
但是从 activity record 的记录里是可以看到栈记录的:adb shell dumpsys activity activities:

activity records

上面例子是,我们栈顶是 ThirdActivity,后台杀死进程后再进,可以看到系统是恢复了 ThirdActivity,通过 log 可以验证没有调用 MainActivity 的onCreate 方法,但是直接调用了 ThirdActivity 的 onCreate 方法。但是可以看当前的 activity record 中,栈是存在的。

此时点击 back,会导致 SecondActivity 被恢复,再点击 back 会导致 MainActivity 被恢复:


activity lifecycle

其他形式的进程死亡再恢复

上面说的是系统内存不足引起的进程回收,导致进程死亡,但是实际上我们常遇到的还有崩溃(比如空指针),我们还可以 ddms 杀进程。

Q: 当进程在前台时,进程死亡,然后恢复,并不会恢复栈顶activity,而是恢复栈顶前面那个activity,why?

A: 其实很好理解:

  • 如果是崩溃导致进程死亡,那崩溃发生在栈顶的那个 activity,此 activity 根本没调用 onSaveInstanceState,那怎么恢复?没法恢复,只能恢复上一个 activity。
  • 同样,ddms 杀进程也是一样的,只能恢复上一个。
    举个例子,当前有 activity,A,B,C,D,此时界面上显示的是 D,如果这 2 种方式杀了进程,那么进程重启之后,恢复的是 activity C。
    还有一点需要注意,如果此时 D 还没显示出来,界面上显示的是 C,那用这 2 种方式杀了进程后,重启后,恢复的是 activity B,很好理解吧。

Q: 那有个问题,在 D 的 onCreate 过程中出了崩溃,此时再恢复,是恢复哪个 activity?
A: 恩,D 还在 onCreate,所以此时界面是 C,恢复的应该是前一个界面,所以恢复的是 B。

  • 结论,前台进程死亡后恢复,恢复的是当前显示的 activity 的上一个 activity。记住 activity 要想被恢复,必须是经历过 onSaveInstanceState 的 activity。

Activity 的生命周期

这里有个坑:两个 activity 之间切换,第一个 activity 的 onStop
是使用的 MessageQueue.IdleHandler,所以它会在 MainLooper 将所有消息执行完后再执行,这就是第一个 activity 的 onStop 会在第二个 activity.onResume 后再执行的原因。这样会导致的一个问题就是,如下图


例如在 LifecycleActivity.onStart 打开相机,LifecycleActivity.onStop 关闭相机。现在处在 LifecycleActivity,相机打开状态,如图中返回 MainActivity,300ms 后又进入 LifecycleActivity。第一个 LifecycleActivity.onStop 却在最后被调用。这就导致重新进入 LifecycleActivity 相机为关闭状态。因此,onStop 的生命周期调用时机并不能得到保证,与顺序严格相关的操作应放在 onPause 中。
https://linroid.com/2017/05/24/Pit-of-Activity-destory/

View 生命周期

  • onFinishInflate 时,getLayoutParams 返回为 null。应该在 onLayout 之后再 getLayoutParams 才会获取得到。
  • setX, setY 也是只能等 onLayout 之后才能使用。

1. onAttachedToWindow() 的调用时机,isAttachedToWindow() 的时机

onAttachedToWindow() 的调用是由 dispatchAttachedToWindow() 来调用的。isAttachedToWindow() 是判断 mAttachInfo != null,而 mAttachInfo 就是在 dispatchAttachedToWindow() 中第一步被赋值的。因此 onAttachedToWindow() 被调用时,isAttachedToWindow() 一定为 true
但是开发实践中出现了一个对 isAttachedToWindow() 的错误理解和使用。

@Override
public void onAttachedToWindow() {
    Log.i(TAG, "childView is attached to window: " + childView.isAttachedToWindow())
}

上面代码是在一个 ParentViewonAttachedToWindow() 的回调中,使用 childView.isAttachedToWindow() 来判断 childView 是否被添加,这里就会返回 false。因为 view tree 的遍历是一个广度遍历,当 ParentViewonAttachedToWindow() 被调用时,childView.dispatchAttachedToWindow() 还未被调用,所以 childView.isAttachedToWindow() == false

ViewGroup.dispatchAttachedToWindow()

从上面源码里我们也可以看到,ParentView.dispatchAttachedToWindow() 会先调用 super.dispatchAttachedToWindow(),其中就会调用它自己的 onAttachedToWindow(),此时 childView.dispatchAttachedToWindow() 还没被调用。
那么如何应对这种要在 Parent.onAttachedToWindow() 中判断 childView.isAttachedToWindow() 的情况呢?可以用 getParent() != null 来判断。

2. onDetachedFromWindow 的调用时机,isAttachedToWindow() 的调用时机

ViewGroup.dispatchDetachedFromWindow
由上可见,它跟 dispatchAttachedToWindow 中调用子 View 和自己的顺序不一样。我们知道 isAttachedToWindow 是判断的 mAttachInfo == null,而 mAttachInfo 是在 dispatchDetachedFromWindow() 中被置空的。因此在父 ViewonDetachedFromWindow() 中去判断子 ViewisAttachedToWindow() 就会为 false
上一篇下一篇

猜你喜欢

热点阅读