第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

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

相关文章
|
23天前
|
缓存 编译器 PHP
第113篇 Compose 与 Kotlin 特性:为什么 Compose 离不开 Kotlin
本文深入解析 Jetpack Compose 的底层机制,揭示其“声明式 UI”背后的三大支柱:带接收者 Lambda、编译器插件(KCP)与稳定性注解(`@Composable`/`@Stable`/`@Immutable`)。重点剖析编译器如何重写函数、实现智能跳过重组,以及常见性能陷阱(如列表无 key、lambda 不稳定)的根因与解法,助你面试直击本质。
86 3
|
消息中间件 安全 Java
第145篇 Handler 内存泄漏:为什么匿名内部类最危险
Handler内存泄漏本质是“引用链未断”:GC Root → MessageQueue(永不销毁)→ Message → Handler → Activity。关键在Message.target强引用Handler,而内部类Handler又隐式持有Activity。清理核心是移除队列中消息,而非仅置空Handler。
97 0
|
API Android开发 开发者
第127篇 ViewPager2 与 Fragment:懒加载的正确实现
ViewPager2 内核是 RecyclerView,由 FragmentStateAdapter 托管 Fragment 生命周期,依赖 `getItemId`/`containsItem` 实现稳定复用。旧版 `setUserVisibleHint` 已废弃,应统一用 `repeatOnLifecycle(STARTED)` 控制可见性逻辑。关键在理解“复用机制+生命周期托管”,避免数据错乱、重复加载与内存泄漏。
97 0
|
23天前
|
Android开发
第122篇 Activity 启动模式:standard 到 singleInstance
本节详解Activity启动模式,直击“最容易出玄学bug”的栈管理机制。核心指出:行为由**系统、任务栈、launchMode、Intent flags四者共同决定**,仅记四个枚举值远远不够。重点剖析四种任务栈本质及flags优先级,并给出通知跳转、登录回退、启动图等真实场景的避坑方案与代码范式。
85 1
|
23天前
|
安全 Java 定位技术
第117篇 Kotlin 阶段面试通关地图:高频题怎么连成网
这是Kotlin面试的结构化复习地图,非知识点罗列,而是将60+篇内容按「六大知识块×三层深度」(会用/懂机制/能取舍)分层归因,直击面试区分度核心。助你快速定位保底分与拉分点,掌握高频题答题骨架、交叉跳转逻辑及三大避坑指南,真正把知识织成可检索、可推演、可落地的活网络。
79 1
|
Android开发
第128篇 Service 生命周期:started 与 bound 两条线
Service本质无“运行中/已停止”状态,仅分启动模式(startService)与绑定模式(bindService)。前者靠onStartCommand响应,支持START_STICKY自动重启;后者依赖onBind/onUnbind,绑定全解即销毁。核心原则:不操作UI、不耗时、不持Activity引用,状态交由UI层订阅。
69 0
|
23天前
|
调度 Android开发 容器
第126篇 Fragment 事务与回退栈:add、replace、show、hide
Fragment事务异步队列执行,非调用即生效;回退栈存操作记录而非实例。`show/hide` 快但常驻内存、不可回退;`replace`+栈可回退但重建视图。选型关键:是否需回退?是否敏感内存?——是判断题,非知识点。
76 1
|
存储 缓存 大数据
第123 篇状态保存与恢复:onSaveInstanceState 的时机
本文深入解析Android状态保存与恢复的核心逻辑:关键不在“存”,而在“判”——明确区分视图状态(View)、界面状态(Activity Bundle)与业务数据(ViewModel/Repository)三层,严守1MB Binder事务限制,规避TransactionTooLargeException;强调onSaveInstanceState不保证调用,关键状态须冗余存储;提供可落地的工程实践与面试高分要点。
77 0
|
存储 缓存 监控
第149篇 图片加载原理:三级缓存与解码采样
图片加载框架核心解决三大问题:IO异步化、内存可控解码、复用场景下的请求-View精准绑定。其中第三点最易被忽视,却最易引发线上错图、闪白、崩溃等事故。关键在于理解完整链路与双重校验机制。
94 0
|
22天前
|
编解码 自然语言处理 数据可视化
第137篇 MeasureSpec 与测量模式:三种模式的取舍
MeasureSpec 是 Android 测量机制的核心:32 位整数,高2位表模式(EXACTLY/AT_MOST/UNSPECIFIED),低30位表尺寸。关键要理解——AT_MOST 是上限非下限,UNSPECIFIED 常见于 ScrollView 导致高度为0。自定义 View 必须完整处理三模式,配合参数化单测,方能避坑。
73 0

热门文章

最新文章