关于 Activity & View 生命周期的解惑
App 内存不足时,系统会回收 Activity 吗?
Android 在运行过程中发现内存不足,会杀掉一些后台进程,来获取内存,这个过程称为内存回收。如果后台进程都杀光了,内存还是不够,此时可能有 2 种表现:1. 跳出OOM崩溃;2. 杀死前台进程。并不会发生回收某个或某些activity的行为。
-
Android app out of memory issues - tried everything and still at a loss Android 之母 hackbod 的回答。
-
activity-lifecycle#asem 中有更明确的官方说明。简单来说,当系统资源不足时会杀死整个进程,而要模拟这个过程,可以从 Setting -> Application Manager 中去杀死这个进程。网络上还有人说通过 adb shell ulimit -Sv 2000 命令可以将内存限制到 2MB,我试了没有效果。
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 之前被系统回收了(用户从程序列表中将其杀死不算,从系统 setting 中 force stop 算不算呢?),现在被恢复了
场景:进程被系统回收后,在最近使用程序列表,或者在主页程序列表点击 icon 再次启动应用,系统会重启进程并恢复 activity
比如我们的栈顶 activity 中有一个 EditText
,里面填了 "yy",进程被回收之后,我们在最近程序列表里点击应用,进程会重启,然后恢复 activity。此 EditText
会恢复 "yy",这是如何做到的?不需要我们写一行代码,activity 本身就能够恢复 EditText
的值,简单的说就是在回收进程之前会通过 onSaveInstanceState 来保存数据(Activity 调用了 View.onSaveInstanceState),然后进程启动,activity 重新启动的过程中调用 onRestoreInstanceState 来恢复数据(Activity 调用了 View.onRestoreInstanceState)。
我们来看看这个场景的生命周期:
- 启动 activity:onCreate(null: Bundle) → onStart → onResume
- 点击 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 之后。 - 进程被杀死(通过ddms, 系统内存不足,adb kill pid (需要 root)、setting 中应用管理器等等),很暴力,不会有任何生命周期函数被调用(但是从最近程序列表中删除进程,Activity 的 onDestroy 会被调用,因为这是用户主动行为。用户主动回收 activity 的行为(按 back,调用 finish() 方法)会导致 Activity.onDestroy() 被调用)
- 从最近程序列表中打开刚才的进程,进程会再次启动,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 会被一起恢复吗?
activity records
A: 不会,只会恢复栈顶的activity,但是栈是恢复了的,在按 back 键后,会创建倒数第二个 activity 实例。举个例子,我们现在有 MainActivity、SecondActivity、ThirdActivity 三个 activity,栈顶是 ThirdActivity。然后进程被回收,之后用户从最近列表点击,导致进程重启,activity 恢复,第一步是恢复 ThirdActivity(栈顶的)
但是从 activity record 的记录里是可以看到栈记录的:adb shell dumpsys activity activities:
上面例子是,我们栈顶是 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())
}
上面代码是在一个 ParentView
的 onAttachedToWindow()
的回调中,使用 childView.isAttachedToWindow()
来判断 childView
是否被添加,这里就会返回 false
。因为 view tree 的遍历是一个广度遍历,当 ParentView
的 onAttachedToWindow()
被调用时,childView.dispatchAttachedToWindow()
还未被调用,所以 childView.isAttachedToWindow() == false
。
从上面源码里我们也可以看到,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()
中被置空的。因此在父 View 的 onDetachedFromWindow()
中去判断子 View 的 isAttachedToWindow()
就会为 false
。