第 10 周回到 Android 四大组件里最熟悉、也最容易被写浅的一个东西:Activity。刚学 Android 的时候,很多人会把 Activity 理解成"一个页面类",然后把所有初始化都塞进 onCreate():网络请求、埋点、播放器、传感器、弹窗、列表、缓存、诊断面板,能放的都放进去。
这事前期看不出问题,页面能跑,按钮能点。但真正进入业务以后,坑会慢慢冒出来:切后台以后资源还在跑,旋转屏幕后状态丢了,onPause() 里做了重活导致返回卡顿,onCreate() 越来越长,最后谁也不敢改。
所以这一周的重点不是背生命周期顺序,而是把 Activity 当成一个页面状态机:什么时候创建,什么时候可见,什么时候可交互,什么时候不可见,什么时候销毁。只有先把这些边界立住,后面讲跳转、启动模式、状态恢复、ViewModel、Service,才不会混成一团。
一、相关资料
| 来源 | 内容 |
|---|---|
| Android Developers:The activity lifecycle | onCreate()、onStart()、onResume()、onPause()、onStop()、onDestroy() 语义来源 |
| Android Developers:Lifecycle | 用来解释 LifecycleOwner、Lifecycle、DefaultLifecycleObserver 的职责 |
| Android Developers:Save UI states | 只引用状态保存边界:Saved State 保存少量 UI 状态,深水区留到第13周 |
AndroidX API:DefaultLifecycleObserver |
用于本周 Demo 的生命周期观察者实现 |
| 当前项目 Demo | 作为正文代码和实践映射落点 |
二、先把 Demo 主结构摆出来
第 10 周的 Demo 是一个生命周期实验页。它做四件事:打印生命周期回调、用 DefaultLifecycleObserver 管理资源、用 onSaveInstanceState() 保存少量日志、用 lazy 延迟初始化非首屏模块。
kotlin
class Week10ActivityLifecycleActivity : AppCompatActivity() {
private lateinit var binding: ActivityWeek10ActivityLifecycleBinding
private val logLines = mutableListOf<String>()
private val lifecycleResourceObserver by lazy {
Week10LifecycleResourceObserver(
addLog = ::addLog,
updateState = ::updateObserverState
)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityWeek10ActivityLifecycleBinding.inflate(layoutInflater)
setContentView(binding.root)
setupHeader()
setupActions()
lifecycle.addObserver(lifecycleResourceObserver)
addLifecycleLog("onCreate", "创建 Activity、绑定 ViewBinding、注册生命周期观察者。")
}
}
这段代码先把一个误区拆掉:onCreate() 可以做基础初始化,但不应该变成"万物起点"。这里放的是 ViewBinding、页面文案、按钮事件、生命周期观察者注册,这些是页面正常工作必需的骨架。
lifecycle.addObserver(lifecycleResourceObserver) 是本周第一个关键点。Activity 本身是 LifecycleOwner(生命周期持有者),它暴露了一个 Lifecycle(生命周期状态与事件对象)。观察者被注册进去以后,就能在 ON_START、ON_RESUME、ON_PAUSE、ON_STOP 这些事件发生时处理资源。
如果少了观察者注册,资源管理逻辑就只能继续堆在 Activity 的各个回调里。Demo 小的时候无所谓,真实项目里页面一复杂,onStart() 和 onStop() 很快就会变成垃圾桶。
实践
内容 App 的详情页、短视频播放页、地图页、扫码页,通常都会有很多"跟生命周期绑定"的东西:播放器、定位、传感器、相机、埋点、计时器。成熟团队常见做法不是让每个资源都散落在 Activity 回调里,而是把它们拆成生命周期感知组件,再统一挂到 lifecycle 上。
相关技术
- API / 类:
AppCompatActivity、Activity、LifecycleOwner、Lifecycle、DefaultLifecycleObserver、ViewBinding - 系统机制:Activity 创建、生命周期事件派发、配置变化重建
- Jetpack / 三方库:AndroidX Lifecycle、AppCompat
- 性能工具:Logcat、Android Studio Profiler、StrictMode(后续性能周会继续用)
- 优化关键词:生命周期解耦、资源配对释放、首屏初始化瘦身
- 常见坑:把所有资源都放进
onCreate(),回调能跑但资源边界混乱
三、Activity 生命周期:别背顺序,要看状态变化
官方文档给出的主链路可以先记成这一条:
scss
onCreate()
→ onStart()
→ onResume()
→ onPause()
→ onStop()
→ onDestroy()
但只背这条线没什么用。真实页面里更重要的是理解每个回调背后的状态:onCreate() 是创建,onStart() 是可见,onResume() 是可交互,onPause() 是失去焦点,onStop() 是不可见,onDestroy() 是销毁。
本周 Demo 把这些回调都打出来:
kotlin
override fun onStart() {
super.onStart()
addLifecycleLog("onStart", "页面已经可见,适合启动可见期间需要的轻量资源。")
}
override fun onResume() {
super.onResume()
addLifecycleLog("onResume", "页面进入前台并可交互,适合启动前台交互资源。")
}
override fun onPause() {
addLifecycleLog("onPause", "页面失去焦点;这里不要放数据库事务或大文件保存。")
super.onPause()
}
override fun onStop() {
addLifecycleLog("onStop", "页面完全不可见;适合停止可见资源、保存轻量草稿。")
super.onStop()
}
这几行看起来普通,但它们决定了页面资源的边界。
onStart() 表示页面已经可见。用户可能还不能操作它,但至少能看见它。所以只要"页面被看见就需要"的东西,可以放在 onStart() 到 onStop() 之间。例如可见埋点、轻量 UI 刷新、某些多窗口下仍需要展示的预览。
onResume() 表示页面已经到了前台并且可交互。只有用户正在操作当前页面时才需要的资源,才适合放在 onResume() 到 onPause() 之间。例如相机预览、传感器监听、播放器播放、输入焦点相关逻辑。
最容易掉进去的坑是 onPause()。很多人以为页面要走了,就赶紧在 onPause() 里保存数据、写数据库、发网络请求。官方文档明确提醒:onPause() 执行时间很短,不适合做耗时操作。你可以暂停前台资源,但不要把重型保存塞进去。
实践
短视频页是最容易理解的例子:用户能看到视频卡片,不代表这个页面一定拥有焦点;页面拥有焦点,也不代表可以把资源拖到不可见才释放。成熟团队会把"可见"和"可交互"拆开,否则分屏、多窗口、半透明页面、权限弹窗都会让资源状态变得很奇怪。
相关技术
- API / 类:
onCreate()、onStart()、onResume()、onPause()、onStop()、onRestart()、onDestroy() - 系统机制:可见状态、前台状态、失焦、不可见、销毁、配置变化
- Jetpack / 三方库:AndroidX Lifecycle
- 性能工具:Logcat 时间线、Profiler CPU / Memory 观察
- 优化关键词:资源生命周期配对、耗时操作后移、页面状态机
- 常见坑:在
onPause()做数据库事务、在onResume()获取资源却拖到onStop()才释放
四、onStart/onStop 和 onResume/onPause:这是第10周最重要的分界线
如果这一周只记一个判断,我会选这句:
可见资源放在
onStart()/onStop(),前台交互资源放在onResume()/onPause()。
本周 Demo 用 DefaultLifecycleObserver 把这件事拆出来,而不是继续塞在 Activity 里:
kotlin
class Week10LifecycleResourceObserver(
private val addLog: (String) -> Unit,
private val updateState: (String) -> Unit
) : DefaultLifecycleObserver {
private var visibleResourceStarted = false
private var foregroundResourceStarted = false
override fun onStart(owner: LifecycleOwner) {
visibleResourceStarted = true
addLog("Observer ON_START:启动可见期间资源,例如页面可见埋点、轻量 UI 刷新。")
renderState()
}
override fun onResume(owner: LifecycleOwner) {
foregroundResourceStarted = true
addLog("Observer ON_RESUME:启动前台交互资源,例如相机预览、传感器、播放器。")
renderState()
}
override fun onPause(owner: LifecycleOwner) {
foregroundResourceStarted = false
addLog("Observer ON_PAUSE:释放前台交互资源,避免失焦后继续占用。")
renderState()
}
override fun onStop(owner: LifecycleOwner) {
visibleResourceStarted = false
addLog("Observer ON_STOP:页面不可见,停止可见期间资源。")
renderState()
}
}
DefaultLifecycleObserver 是 AndroidX Lifecycle 里的观察者接口。你实现它以后,可以按生命周期事件写方法:onStart()、onResume()、onPause()、onStop()。这比老式的注解型 @OnLifecycleEvent 更直观,也更适合 Kotlin 项目。
这段代码做了两组配对:
onStart()开启可见资源,onStop()停止可见资源。onResume()开启前台资源,onPause()停止前台资源。
如果少了这种配对,资源就很容易"借了不还"。比如在 onResume() 开了传感器,但忘了在 onPause() 停,用户切到别的页面后传感器还在跑;再比如在 onStart() 做了可见埋点计时,却拖到 onDestroy() 才结束,用户按 Home 后统计时长就会偏大。
实践
地图、扫码、直播、音视频、运动健康类 App 都会遇到这个边界。成熟团队常见做法是给每类资源做自己的生命周期控制器,比如 PlayerLifecycleController、LocationLifecycleObserver、CameraPreviewController。当前 Demo 没有引入真实相机或播放器,是为了先把生命周期配对讲清楚;完整播放器和相机资源管理后续会进入对应专题。
相关技术
- API / 类:
DefaultLifecycleObserver、LifecycleOwner、Lifecycle.Event.ON_START、ON_RESUME、ON_PAUSE、ON_STOP - 系统机制:生命周期事件派发、前台焦点、多窗口可见性
- Jetpack / 三方库:AndroidX Lifecycle
- 性能工具:Profiler 电量 / CPU 观察、Logcat 回调顺序观察
- 优化关键词:资源配对、观察者解耦、可见资源、前台资源
- 常见坑:把相机、传感器、播放器这类独占资源放错生命周期区间
五、onSaveInstanceState():能保存状态,但别把它当仓库
第 10 周只轻轻碰一下状态保存,因为第 13 周会专门讲"页面状态保存 + 数据恢复优化"。但这里必须先立一个边界:onSaveInstanceState() 适合保存少量 UI 状态,不适合保存大对象、图片、大列表、复杂业务数据。
Demo 里只保存最近几条日志:
scss
override fun onSaveInstanceState(outState: Bundle) {
outState.putStringArrayList(KEY_LOGS, ArrayList(logLines.takeLast(12)))
addLifecycleLog("onSaveInstanceState", "保存少量 UI 状态:这里只保存最近日志,不塞大对象。")
super.onSaveInstanceState(outState)
}
savedInstanceState?.getStringArrayList(KEY_LOGS)?.let { restoredLogs ->
logLines.clear()
logLines.addAll(restoredLogs)
renderLogs()
}
Bundle 可以理解成系统帮你临时保存的一小包键值数据。它适合放 String、数字、布尔值、少量列表、当前选中的 id、搜索词、滚动位置这种轻量信息。
如果你把大图、完整列表、复杂对象都塞进去,问题不只是"写法丑",还可能带来序列化成本、主线程卡顿,甚至因为数据过大导致异常。真实项目里,复杂数据应该在数据库、DataStore、文件、Repository 缓存里,Bundle 只保存恢复它们所需要的 key。
这里还有一个新手容易忽略的点:状态保存不等于长期持久化。它服务的是配置变化、系统回收后恢复这类场景,不是用户数据存储方案。用户写的草稿、订单、聊天记录,不能只靠 onSaveInstanceState()。
实践
搜索页可以保存搜索词,详情页可以保存商品 id,筛选页可以保存当前筛选条件,表单页可以保存少量输入草稿。但成熟团队不会把完整商品列表、Bitmap、接口返回大对象塞进 Saved State。否则页面恢复看似方便,后面性能和稳定性会一起还债。
相关技术
- API / 类:
onSaveInstanceState()、Bundle、savedInstanceState、SavedStateHandle、ViewModel - 系统机制:配置变化、进程死亡、临时 UI 状态恢复
- Jetpack / 三方库:AndroidX Lifecycle、ViewModel、SavedState(第13周深入)
- 性能工具:StrictMode、Profiler、崩溃日志分析
- 优化关键词:轻量状态、最小恢复参数、状态分层
- 常见坑:把 Saved State 当数据库,把大对象塞进
Bundle
六、lazy:不是炫技,是把非首屏初始化往后挪
第 10 周规划里还有一个优化点:非必要初始化延迟加载,使用 Kotlin 的 lazy 委托。
真实业务里,onCreate() 最容易变胖。很多模块不是首屏必须,却被顺手初始化了:调试面板、统计诊断、复杂配置解析、非首屏 tab 的控制器、暂时不会出现的弹窗管理器。它们单个看不重,加起来就会拖慢页面创建。
Demo 里把诊断模块写成懒加载:
scss
private val delayedReportDelegate = lazy {
addLog("lazy 初始化:非首屏诊断模块第一次被真正使用,现在才创建。")
Week10DelayedReportCenter()
}
private val delayedReportCenter: Week10DelayedReportCenter
get() = delayedReportDelegate.value
binding.btnInitLazy.setOnClickListener {
binding.tvLazyState.text = delayedReportCenter.renderReport()
updateLazyState()
}
lazy 的意思是:这个对象不会在 Activity 创建时立刻生成,第一次访问 delayedReportDelegate.value 时才会创建。Demo 里只有用户点击"触发 lazy"按钮时,Week10DelayedReportCenter 才会真正初始化。
如果这类对象放进 onCreate(),用户可能根本没用到它,却已经为它付出了首屏成本。成熟项目里这种浪费非常常见:单个模块只慢 5ms,十几个模块加起来就能明显影响页面启动。
当然,lazy 不是万能药。它适合"可能用到,但不是立即用"的对象;不适合生命周期很敏感、需要明确释放、或者初始化失败必须提前暴露的对象。尤其不要为了省事把持有 Activity 的重对象随便做成长期懒加载,否则可能反过来制造泄漏。
实践
首页、详情页、搜索页经常会做首屏瘦身:首屏只初始化用户立刻能看到和操作的部分,非首屏 tab、诊断工具、低频能力延后。成熟团队通常会配合启动耗时统计、方法耗时打点、Trace 或 Profiler,判断哪些初始化真的该延后,而不是凭感觉乱挪。
相关技术
- API / 类:Kotlin
lazy、Lazy.isInitialized()、属性委托、SystemClock.elapsedRealtime() - 系统机制:首屏初始化、对象创建时机、主线程执行成本
- Jetpack / 三方库:无强依赖;后续可结合 App Startup、Hilt lazy 注入
- 性能工具:Android Studio Profiler、Trace、启动耗时打点
- 优化关键词:延迟初始化、首屏瘦身、按需创建
- 常见坑:把需要及时释放的 Activity 资源做成长期懒加载,或把必须提前失败的初始化延后到用户操作时才崩
七、onDestroy():最后一道门,但不要什么都等它
onDestroy() 很容易被误解成"页面结束时统一清理"。它确实是销毁回调,但不能把所有释放都拖到这里。
Demo 里只做最终日志和观察者移除:
kotlin
override fun onDestroy() {
addLifecycleLog("onDestroy", "Activity 即将销毁,isFinishing=$isFinishing。")
lifecycle.removeObserver(lifecycleResourceObserver)
super.onDestroy()
}
isFinishing 可以帮助你区分一种常见情况:用户真的结束页面,还是系统因为配置变化临时销毁页面。比如旋转屏幕时,旧 Activity 会 onDestroy(),但这不等于用户退出了业务流程。
这里最关键的判断是:不要把前台资源拖到 onDestroy() 才释放。因为用户按 Home 后,页面可能只是 onStop(),并没有马上 onDestroy()。如果播放器、相机、传感器一直等 onDestroy(),它们就会在后台继续占资源。
实践
成熟团队做页面治理时,通常会要求资源"在哪里开始,在哪里成对结束"。onDestroy() 更像兜底清理,不是主要资源管理点。尤其是音视频、相机、定位这类高成本资源,必须在更早的生命周期回调里释放。
相关技术
- API / 类:
onDestroy()、isFinishing、lifecycle.removeObserver() - 系统机制:正常 finish、配置变化销毁、系统回收
- Jetpack / 三方库:AndroidX Lifecycle、ViewModel(后续周深入)
- 性能工具:LeakCanary(后续专题再展开)、Memory Profiler
- 优化关键词:最终清理、泄漏规避、配置变化判断
- 常见坑:把
onDestroy()当成所有资源释放的唯一出口
八、本周 Demo 功能清单
| 能力 | Demo 落点 | 你可以怎么验证 |
|---|---|---|
| 全生命周期回调 | Week10ActivityLifecycleActivity 重写 onCreate() 到 onDestroy() |
打开页面、按 Home、返回页面、点击 finish、旋转屏幕观察日志 |
| 页面创建 / 销毁 | ViewBinding 初始化、isFinishing 日志 |
点击 finish 和旋转屏幕,对比 isFinishing 变化 |
| 状态监听 | DefaultLifecycleObserver |
观察可见资源和前台资源状态变化 |
| 轻量状态保存 | onSaveInstanceState() 保存最近日志 |
旋转屏幕后查看日志是否恢复 |
| 生命周期泄漏规避 | lifecycle.addObserver() / removeObserver() |
资源逻辑从 Activity 回调中拆出,不长期散落在页面类里 |
| 延迟加载 | lazy 创建 Week10DelayedReportCenter |
不点击按钮时不初始化,点击后才创建 |
九、这一周最容易踩的坑
- 把
onCreate()当成万能初始化入口:页面能跑,但首屏会越来越重。 - 把
onPause()当成保存数据的地方:它执行时间短,不适合数据库事务、网络请求、大文件保存。 - 混淆可见和可交互:多窗口、半透明页面、权限弹窗都会让这个问题暴露出来。
- 资源只启动不释放:尤其是相机、传感器、播放器、定位、计时器。
- 把
Bundle当仓库:Saved State 只适合轻量 UI 状态。 - 什么都等
onDestroy():用户切后台不一定立刻销毁页面。 - 滥用
lazy:延迟初始化不是不管理生命周期,持有 Activity 的对象仍然要小心释放。
十、技术清零表
| 技术 | 它是什么 | Demo 落点 | 真实项目价值 | 常见坑 |
|---|---|---|---|---|
Activity |
Android 四大组件之一,通常承载一个用户可交互页面 | Week10ActivityLifecycleActivity |
页面创建、展示、交互、销毁的核心入口 | 只把它当普通页面类,忽略生命周期状态 |
onCreate() |
Activity 创建回调 | 初始化 ViewBinding、按钮、观察者 | 搭建页面骨架 | 塞入所有初始化,拖慢首屏 |
onStart() / onStop() |
页面可见 / 不可见边界 | 可见资源状态开关 | 管理可见期间资源 | 和前台交互资源混用 |
onResume() / onPause() |
页面可交互 / 失焦边界 | 前台资源状态开关 | 管理相机、传感器、播放器等前台资源 | 在 onPause() 做重型保存 |
onDestroy() |
Activity 销毁回调 | 记录 isFinishing,移除观察者 |
最终清理和销毁判断 | 把所有释放都拖到这里 |
LifecycleOwner |
提供生命周期的对象 | Activity 自身提供 lifecycle |
让组件感知宿主生命周期 | 不理解 owner 范围,导致观察者挂错对象 |
Lifecycle |
表示生命周期状态和事件 | lifecycle.addObserver() |
统一派发生命周期事件 | 手动分散调用,破坏解耦 |
DefaultLifecycleObserver |
AndroidX 生命周期观察者接口 | Week10LifecycleResourceObserver |
把资源启停从 Activity 中拆出去 | 只注册不移除,或回调里持有重对象不释放 |
onSaveInstanceState() |
保存少量临时 UI 状态的回调 | 保存最近 12 条日志 | 应对旋转等临时重建 | 保存大对象、列表、Bitmap |
Bundle |
键值型状态容器 | putStringArrayList() / getStringArrayList() |
保存恢复 UI 所需的小数据 | 把它当数据库或缓存层 |
Kotlin lazy |
第一次访问时才初始化对象的属性委托 | delayedReportDelegate |
首屏瘦身,非必要模块按需创建 | 延迟了该提前暴露的错误,或持有 Activity 造成泄漏 |
isFinishing |
判断 Activity 是否正在 finish | onDestroy() 日志 |
区分用户退出和配置变化销毁 | 以为 onDestroy() 都代表用户退出 |