第10周:Activity 基础功能与生命周期优化

第 10 周回到 Android 四大组件里最熟悉、也最容易被写浅的一个东西:Activity。刚学 Android 的时候,很多人会把 Activity 理解成"一个页面类",然后把所有初始化都塞进 onCreate():网络请求、埋点、播放器、传感器、弹窗、列表、缓存、诊断面板,能放的都放进去。

这事前期看不出问题,页面能跑,按钮能点。但真正进入业务以后,坑会慢慢冒出来:切后台以后资源还在跑,旋转屏幕后状态丢了,onPause() 里做了重活导致返回卡顿,onCreate() 越来越长,最后谁也不敢改。

所以这一周的重点不是背生命周期顺序,而是把 Activity 当成一个页面状态机:什么时候创建,什么时候可见,什么时候可交互,什么时候不可见,什么时候销毁。只有先把这些边界立住,后面讲跳转、启动模式、状态恢复、ViewModelService,才不会混成一团。

一、相关资料

来源 内容
Android Developers:The activity lifecycle onCreate()onStart()onResume()onPause()onStop()onDestroy() 语义来源
Android Developers:Lifecycle 用来解释 LifecycleOwnerLifecycleDefaultLifecycleObserver 的职责
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_STARTON_RESUMEON_PAUSEON_STOP 这些事件发生时处理资源。

如果少了观察者注册,资源管理逻辑就只能继续堆在 Activity 的各个回调里。Demo 小的时候无所谓,真实项目里页面一复杂,onStart()onStop() 很快就会变成垃圾桶。

实践

内容 App 的详情页、短视频播放页、地图页、扫码页,通常都会有很多"跟生命周期绑定"的东西:播放器、定位、传感器、相机、埋点、计时器。成熟团队常见做法不是让每个资源都散落在 Activity 回调里,而是把它们拆成生命周期感知组件,再统一挂到 lifecycle 上。

相关技术
  • API / 类:AppCompatActivityActivityLifecycleOwnerLifecycleDefaultLifecycleObserver、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/onStoponResume/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 都会遇到这个边界。成熟团队常见做法是给每类资源做自己的生命周期控制器,比如 PlayerLifecycleControllerLocationLifecycleObserverCameraPreviewController。当前 Demo 没有引入真实相机或播放器,是为了先把生命周期配对讲清楚;完整播放器和相机资源管理后续会进入对应专题。

相关技术
  • API / 类:DefaultLifecycleObserverLifecycleOwnerLifecycle.Event.ON_STARTON_RESUMEON_PAUSEON_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()BundlesavedInstanceStateSavedStateHandleViewModel
  • 系统机制:配置变化、进程死亡、临时 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 lazyLazy.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()isFinishinglifecycle.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 不点击按钮时不初始化,点击后才创建

九、这一周最容易踩的坑

  1. onCreate() 当成万能初始化入口:页面能跑,但首屏会越来越重。
  2. onPause() 当成保存数据的地方:它执行时间短,不适合数据库事务、网络请求、大文件保存。
  3. 混淆可见和可交互:多窗口、半透明页面、权限弹窗都会让这个问题暴露出来。
  4. 资源只启动不释放:尤其是相机、传感器、播放器、定位、计时器。
  5. Bundle 当仓库:Saved State 只适合轻量 UI 状态。
  6. 什么都等 onDestroy() :用户切后台不一定立刻销毁页面。
  7. 滥用 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() 都代表用户退出
相关推荐
alexhilton14 小时前
Android车载OS中的Remote Compose
android·kotlin·android jetpack
落魄Android在线炒饭1 天前
Android 自定义HAL开发篇之 HIDL篇——从入门到实战(上)
android
plainGeekDev1 天前
广播接收器 → Flow + Lifecycle
android·java·kotlin
plainGeekDev1 天前
EventBus → SharedFlow
android·java·kotlin
37手游移动客户端团队2 天前
招聘-高级安卓开发工程师
android·客户端
用户41659673693552 天前
WebView 请求异常排查操作手册
android·前端
Kapaseker2 天前
学不动了,入门 Compose Styles API
android·kotlin
墨狂之逸才3 天前
Android TV WebView 遥控器按键处理:从全透传到白名单
android
plainGeekDev3 天前
MVC 写法 → MVVM
android·java·kotlin