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
有任何问题欢迎在评论区留言交流。