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

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 未来可能被系统回收时被调用:

  • 当用户按下 HOME 键 → 因为 app 到后台了,可能被系统回收
  • 从最近应用列表中选择其他程序 → 因为 app 到后台了,可能被系统回收
  • 按下电源键(关闭显示器) → 因为 app 到后台了,可能被系统回收
  • 从当前 activity 启动一个新的 activity 时 → 因为 app 未来可能到后台,先保存压栈的 activity 的信息
  • 屏幕方向切换时 → 因为 activity 会被销毁重启
    onRestoreInstanceState 则在以下两个条件都满足时才会被调用
  • 之前调用过 onSaveInstanceState
  • app 之前被系统回收了(用户从程序列表中将其杀死不算,从系统 settingforce stop 算不算呢?),现在被恢复了

场景:进程被系统回收后,在最近使用程序列表,或者在主页程序列表点击 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 的生命周期

  • onStart 是 Activity 即将可见(此时用户看到的 activity 是它的 window 的背景,这个看主题),onResume 是 Activity 可操作(此时 view 已经被 inflate),即获取了焦点。
  • 由 Activity A 启动 Activity B,生命周期:A.onPause()【此时 A 还可见】 → B.onCreate() → B.onStart() → B.onResume【B 获取到屏幕焦点】→ A.onStop()。
  • onPause 是保证能够被调用,onStop 和 onDestroy 不保证被调用。因此持久化数据等操作放在 onPause 中,但是 onPause 中又不能做太耗时的操作,因为只有当 onPause 调用完成后,下一个 activity 的 onCreate 才会被调用,因此在 onPause 中做太耗时的操作会影响下一个 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 → onVisibilityChanged → onAttatchedToWindow → onWindowVisibilityChanged → onMeasure → onLayout → onDraw
  • onFinishInflate 时,getLayoutParams 返回为 null。应该在 onLayout 之后再 getLayoutParams 才会获取得到。
  • setX, setY 也是只能等 onLayout 之后才能使用。
  • 销毁:onWindowFocusChanged → onWindowVisibilityChanged → onDetachedFromWindow

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,142评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,298评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,068评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,081评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,099评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,071评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,990评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,832评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,274评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,488评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,649评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,378评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,979评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,625评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,643评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,545评论 2 352

推荐阅读更多精彩内容