第121篇 Activity 生命周期全解:七个回调的成对关系

简介: 本文深入解析Activity生命周期的两大维度:视图生命周期(6个回调)与进程生命周期(状态保存/恢复),直击面试高频考点——“为何如此设计”及“进程被杀后如何恢复”。厘清`onSaveInstanceState`触发条件、Bundle大小限制、ViewModel与状态保存的边界,并给出工程最佳实践与避坑指南。

Kotlin 语言线到此收尾,从这一篇开始进入 Android 框架层。第一个切入口就是 Activity 生命周期——它看起来是最基础的八道题,但面试里真正能区分人的是"为什么这样设计"与"进程被回收后怎么恢复"这两层。

先把结论放在前面:Activity 生命周期有两组。视图生命周期(onCreate/onStart/onResume/onPause/onStop/onDestroy)描述界面从创建到销毁;进程生命周期(onSaveInstanceState/onRestoreInstanceState/onNewIntent)描述"Activity 对象可能还在但系统随时会杀掉进程"这件事带来的状态保存问题。面试里绝大多数人只会背第一组,第二组才是分水岭。

机制背后的执行路径

先把八个回调的语义与"该做什么/不该做什么"钉清楚:

| 回调 | 触发时机 | 该做 | 不该做 | |---|---|---|---| | onCreate | 实例首次创建 | 找控件、初始化成员、绑定数据(一次性) | 注册监听、开始动画 | | onStart | 变为可见 | 恢复"可见期才需要"的资源(注册观察者) | 重网络请求 | | onResume | 变为前台可交互 | 连接相机、注册传感器 | 大量初始化 | | onPause | 失去前台 | 暂停动画、释放独占资源(相机) | 提交未保存数据 | | onStop | 完全不可见 | 取消网络请求、注销观察者 | 释放 View 引用 | | onSaveInstanceState | 即将被销毁前 | 保存"下次恢复需要"的状态 | 保存 View 层级(系统自动做) | | onRestoreInstanceState | 由非空 Bundle 恢复 | 恢复已保存状态 | 在此做网络请求 | | onDestroy | 实例销毁 | 释放自持资源(viewLifecycleOwner 协程等) | 释放绑定到 Activity 生命周期的资源(已在 onStop 释放) |

有三个必须能说清的机制点:

第一,onSaveInstanceState 只在"该 Activity 真的可能被杀掉"时才调用。 具体条件是它在 onStop 之后被调用,且仅当 Activity 处于"已停止且不再返回前台"(正常返回上一页、屏幕关闭)或者在 onPause 之后进程被系统杀掉。用户主动按返回键销毁时不会调用——因为没必要恢复。

第二,进程被回收后的恢复流程。 系统杀进程时会保存 View 层级与 onSaveInstanceState 的 Bundle;用户从最近任务回来时,系统重建 Activity → 走完 onCreate(此时 savedInstanceState 非 null)→ onStart → onRestoreInstanceState → onResume。关键:onRestoreInstanceState 排在 onStart 之后,而 onCreate 里也能读到同一个 Bundle。两者都能恢复,只是时机不同。

第三,Fragment 与 Activity 的生命周期耦合。 Fragment 的 onAttach 早于 Activity 的 onCreate;onDetach 晚于 Activity 的 onDestroy。这解释了"为什么不能在 Fragment 构造参数里拿 Activity 的 View"。

真实工程场景的推演

场景一:相机/传感器的注册与释放。正确做法是成对注册释放,且释放点要早:在 onResume 注册、onPause 释放。放在 onStart/onStop 也可行,但如果应用在后台仍需定位(如导航),放 onStart/onStop 更合适。判据是"这个资源在不可见时是否还有价值"。

场景二:网络请求的位置。放在 onCreate 意味着每次旋转屏幕都重新请求(除非用 ViewModel)。规范是请求放 ViewModel + repeatOnLifecycle(STARTED) 收集,这样旋转不重复请求、不可见时停止收集。这是第 096 篇的落地。

场景三:状态保存的取舍。什么该存、什么不该存:

override fun onSaveInstanceState(outState: Bundle) {
   
    super.onSaveInstanceState(outState)
    outState.putString(KEY_QUERY, currentQuery)     // 小的标量、用户可见状态
    outState.putInt(KEY_SCROLL, list.computeVerticalScrollOffset())
    // 不存:列表数据(重新加载即可)、View 引用(大、可能已失效)、Bitmap
}

关键点:Bundle 有大小限制(TransactionTooLargeException 典型阈值约 1MB,且走 Binder 传输),所以只存"恢复界面必需且体积小"的东西。这条是高频追问。

场景四:进程被杀与 ViewModel 的关系。ViewModel 活不过进程被杀。所以"屏幕旋转不丢数据"靠 ViewModel,"应用被系统回收后回来还有数据"靠 onSaveInstanceState + 数据层(Room/网络)。把两者混淆是常见错误。

最常见的坑是

把重逻辑放在 onResume,每次弹窗返回都触发,性能被反复消耗。 表现是打开一个权限弹窗、返回后界面重新加载数据、动画重播。修法:onResume 只做"回到前台"的轻量处理;数据加载交给 ViewModel + onViewCreated 触发一次;必要的事后刷新用 registerForActivityResult 在结果回调里精确触发。

其次是在 onCreate 里注册监听、在 onDestroy 里才释放。表现是 Activity 不可见时监听仍回调,持有 View 导致泄漏。修法:注册与释放成对且放在相邻的回调(onStart/onStop 或 onResume/onPause)。

还有一个更隐蔽的坑:把大对象(含列表数据、Bitmap)塞进 onSaveInstanceState。表现是低内存设备上后台被杀时恢复崩溃(TransactionTooLargeException),或保存耗时明显。修法:只存标量与必要的 id,列表重新从数据层加载。

现场手写这一段就够了

class DetailActivity : AppCompatActivity() {
   

    private lateinit var binding: ActivityDetailBinding
    private val vm: DetailViewModel by viewModels()
    private var sensorManager: SensorManager? = null

    // 1) onCreate:只做一次性初始化
    override fun onCreate(savedInstanceState: Bundle?) {
   
        super.onCreate(savedInstanceState)
        binding = ActivityDetailBinding.inflate(layoutInflater)
        setContentView(binding.root)
        sensorManager = getSystemService(SENSOR_SERVICE) as SensorManager
        setupListeners()
        // 恢复状态:注意 onCreate 也能读到同一个 Bundle
        savedInstanceState?.getString(KEY_QUERY)?.let {
    vm.setQuery(it) }
    }

    // 2) onStart / onStop:资源成对,"不可见时还有价值"的资源放这里
    override fun onStart() {
   
        super.onStart()
        lifecycleScope.launch {
                    // 收集只在可见时进行
            repeatOnLifecycle(Lifecycle.State.STARTED) {
   
                vm.ui.collect {
    render(it) }
            }
        }
    }

    // 3) onResume / onPause:独占资源与动画
    override fun onResume() {
   
        super.onResume()
        sensorManager?.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_UI)
    }

    override fun onPause() {
   
        sensorManager?.unregisterListener(listener)   // 必须成对
        super.onPause()
    }

    // 4) 状态保存:只存小而必要的东西
    override fun onSaveInstanceState(outState: Bundle) {
   
        super.onSaveInstanceState(outState)
        outState.putString(KEY_QUERY, vm.currentQuery)
        outState.putInt(KEY_TAB, binding.tab.selectedTab)
        // ❌ outState.putSerializable(KEY_LIST, hugeList)   → TransactionTooLargeException
    }

    // 5) 恢复:onRestoreInstanceState 在 onStart 之后
    override fun onRestoreInstanceState(savedInstanceState: Bundle) {
   
        super.onRestoreInstanceState(savedInstanceState)
        binding.tab.selectedTab = savedInstanceState.getInt(KEY_TAB)
    }

    override fun onDestroy() {
   
        binding.recycler.adapter = null            // 断开 Adapter 持有 View
        super.onDestroy()
    }

    // 6) 用 Activity Result API 代替 onActivityResult,拿结果更精确
    private val pickCity = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
    r ->
        if (r.resultCode == RESULT_OK) {
   
            vm.selectCity(r.data?.getStringExtra("city"))   // 只在这里处理,不污染 onResume
        }
    }
    private fun launchPicker() = pickCity.launch(Intent(this, CityPickerActivity::class.java))

    companion object {
   
        private const val KEY_QUERY = "query"
        private const val KEY_TAB = "tab"
    }
}

关键行解读:onCreate 只做一次性初始化、避免旋转重复请求;onStart 里用 repeatOnLifecycle 收集(回到前台重启、不重复请求上游);onResume/onPause 成对管理独占资源;onSaveInstanceState 只存标量、避免 TransactionTooLargeException;registerForActivityResult 把"结果处理"从 onResume 里摘出来。

面试追问四连

"onSaveInstanceState 什么时候会被调用?" 答:在 onStop 之后调用,通常只在"该 Activity 可能被系统回收"时——即被其它 Activity 覆盖(之后可能回来)或进程被系统杀死。用户主动按返回键销毁时不调用,因为无需恢复。

"进程被杀后回来,Activity 怎么恢复的?" 答:系统保存了 View 层级与 Bundle;重建时走 onCreate(savedInstanceState 非 null)→ onStart → onRestoreInstanceState → onResume。View 层级由系统自动恢复,自定义状态从 Bundle 取。

"ViewModel 能跨进程被杀吗?" 答:不能。ViewModel 只活在进程内,进程死即消失。旋转屏幕不丢数据靠 ViewModel;应用被系统回收后恢复靠 onSaveInstanceState + 数据层。

"Bundle 大小限制是多少?" 答:通过 Binder 传递,有 TransactionTooLargeException 风险,实践上按 1MB 以内控制。原则是只存"恢复界面必需的标量与 id",列表、Bitmap 一律重新加载。

落地建议

1. 团队规范:注册与释放成对且放在相邻回调(资源放 onStart/onStop 或 onResume/onPause,看不可见时是否仍有价值);onResume 保持轻量,不做数据加载。 2. 网络与耗时任务统一放 ViewModel + repeatOnLifecycle(STARTED);一次性结果统一用 ActivityResult API,不用 onActivityResult 旧写法。 3. onSaveInstanceState 建立"白名单"(标量、id、选中项),其余一律重新加载;把"不往 Bundle 放大对象"写进 code review checklist。 4. 自动化预防:用 SavedStateRegistry 或 SavedStateHandle 让状态保存可测(ViewModel + SavedStateHandle 自动处理);给关键页面写"进程被杀后恢复"的测试(ActivityScenario + recreate())验证状态不丢。

给正在准备面试的你 把这题画成"两条并行的状态轴"。上面一条横轴画视图生命周期(六个回调,标出"这里该做什么"的一行小字);下面一条横轴画进程生命周期(onSaveInstanceState → 进程被系统杀掉 → 重建 → onRestoreInstanceState),在中间画一个断裂符号标"进程死亡"。两条轴之间用虚线连接对应位置,并标一句:"视图生命周期由代码控制,进程生命周期由系统决定——这是所有困惑的来源"。

再补一个高分追问答案:"为什么旋转屏幕会导致 Activity 重建?如何避免?" 原因有两个:①configChanges 未声明时系统默认重建(旋转属于配置变更);②进程被回收后回来。避免重建的方式是声明 android:configChanges="orientation|screenSize|keyboardHidden",但现代做法不推荐——因为手动处理配置变更需要自己重建状态、重置布局,反而更容易出错。推荐方案是允许重建 + ViewModel 保存数据(ViewModel 的设计本意就是为此),必要时用 onSaveInstanceState 补小状态。能讲出"重建不可怕,可怕的是没有正确保存状态",通常就是这题的高分点。

复习时别孤立刷题:tailrec 与尾递归——上一节是 Kotlin 语言线的收尾,本节进入 Android 框架层,两条线的共同点是"都要知道机制而不只是结论"。


如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。你的支持就是这个系列持续更新的动力。

「Android软件开发面试·从入门到精通」连载系列

上一篇:tailrec-与尾递归:递归优化的编译期支持

下一篇预告:Activity-启动模式:standard-到-singleInstance

有任何问题欢迎在评论区留言交流。

相关文章
|
2天前
|
缓存 编译器 PHP
第113篇 Compose 与 Kotlin 特性:为什么 Compose 离不开 Kotlin
本文深入解析 Jetpack Compose 的底层机制,揭示其“声明式 UI”背后的三大支柱:带接收者 Lambda、编译器插件(KCP)与稳定性注解(`@Composable`/`@Stable`/`@Immutable`)。重点剖析编译器如何重写函数、实现智能跳过重组,以及常见性能陷阱(如列表无 key、lambda 不稳定)的根因与解法,助你面试直击本质。
28 3
|
2天前
|
缓存 编译器 API
第119篇 扩展属性与扩展伴生:工具方法的进阶形态
本节详解Kotlin扩展属性与伴生对象扩展:扩展属性无backing field,本质是静态getter/setter方法,每次访问均重计算;扩展伴生对象则为第三方类“添加静态方法”,语法简洁、语义清晰。二者均属编译期静态分派,不可覆盖,是检验Kotlin底层理解的关键点。
21 2
|
18小时前
|
缓存 前端开发 Java
第138篇自定义 View 进阶:onDraw 与 Paint 的进阶用法
本文深入解析自定义 View 进阶核心:Paint 三大能力(属性、Shader、Xfermode)、Canvas 变换与图层、属性动画驱动。强调效果实现原理而非死记 API,直击 onDraw 零分配、Xfermode 隔离、内存泄漏、RTL 支持等高频坑点,助你真正掌握“效果是怎么算出来的”。
18 0
|
2天前
|
API Android开发 C++
第094篇 StateFlow 与 SharedFlow:UI 状态分发的标配
本文深入解析 `StateFlow` 与 `SharedFlow` 的本质区别:前者是有状态的“当前值”容器(replay=1、自动去重),专用于UI状态;后者是无状态的“事件广播”通道(replay可配、支持缓冲与背压策略),适用于一次性事件或多方通知。结合机制、坑点、工程实践与面试高频题,助你彻底避开重复弹窗、数据丢失等典型陷阱。
17 0
|
2天前
|
JSON Java 调度
第089篇 调度器 Dispatchers:三兄弟的分工
Kotlin协程调度器核心在于“协程在哪跑”。Main(主线程,post排队)、Default(CPU密集,并行度=核数)、IO(阻塞等待,并行度≤64)共享同一线程池,仅并行度不同;切换代价微秒级,但循环内高频`withContext`会累积延迟。关键:按任务性质选调度器,数据层应自行切IO,避免调用方误用。
19 0
|
2天前
|
缓存 Java 编译器
第085篇 类委托 by:装饰器模式的一行实现
Kotlin类委托`by`是编译期语法糖,自动生成接口抽象方法的转发实现(如`delegate.foo()`),零反射、零运行时开销。但仅支持接口/抽象方法,不委托`Any`三方法、具体类方法及Java默认方法;委托对象天然共享状态,需注意隔离与可变性边界。
14 0
|
2天前
|
JSON 监控 Java
第081篇 inline 内联函数:高阶函数零开销的秘密
`inline` 本质是编译期函数体展开,消除 Lambda 分配与虚调用,但带来字节码膨胀与方法数增长。需权衡收益:仅对≤10行、纯转发、热路径的薄包装使用。`noinline`(传/存函数)、`crossinline`(禁非局部返回)、`reified`(保留泛型运行时类型)各解特定语义问题。Android 中须警惕 R8 与 dex 65536 限制。
15 0
|
1天前
|
安全 大数据 Android开发
第133篇Intent 与 IntentFilter:显式隐式跳转与匹配规则
本文深入解析Android中Intent与IntentFilter的核心机制:Intent是通信信封,IntentFilter是收件规则;重点厘清隐式Intent必须匹配CATEGORY_DEFAULT、PendingIntent同一性由requestCode+filterEquals决定等高频误区,并涵盖exported声明、FLAG_IMMUTABLE强制要求、Deep Link实现等实战要点。
22 0
|
2天前
|
Android开发
第128篇Service 生命周期:started 与 bound 两条线
Service本质无“运行中/已停止”状态,仅分启动模式(startService)与绑定模式(bindService)。前者靠onStartCommand响应,支持START_STICKY自动重启;后者依赖onBind/onUnbind,绑定全解即销毁。核心原则:不操作UI、不耗时、不持Activity引用,状态交由UI层订阅。
26 1
|
2天前
|
Android开发
第124篇Fragment 生命周期:与 Activity 的联动陷阱
Fragment生命周期含三套独立流程:实例、视图、上下文。关键分界是`onCreateView`至`onDestroyView`为视图生命周期,此时Fragment实例仍存活;`onDestroy`后实例才真正销毁。常见坑如重复弹窗、内存泄漏,根源在于混淆三者时机。推荐实践:视图相关操作绑定`viewLifecycleOwner`,数据交由`ViewModel`管理。
17 0

热门文章

最新文章