Administrator
发布于 2026-08-24 / 55 阅读
0
0

系列文章《Android 工程师的 100 篇进阶笔记》第 1 篇:Activity 生命周期:90% 的人都踩过这 5 个坑

写 Android 代码的第三年,我依然会在 onCreate 里犯蠢。

听起来不可思议,但这事真发生过——我曾经在 Activity 的 onCreate 里 new 了一个 Handler,然后用 postDelayed 做了一个延时任务,期望页面销毁时这个任务能"自动消失"。结果用户从 A 页面跳到 B 页面再回来时,屏幕上跳出了一个莫名其妙的 Toast。

那一刻我深刻意识到:生命周期这门课,永远值得重学。

今天这篇文章是这个 100 篇系列的开篇,咱们就从最基础的 Activity 生命周期开始。但请放心,我不会再念一遍那张"教科书配图"——onCreate → onStart → onResume 这种倒背如流的流程。今天我想跟你聊 5 个真实工程里我见过、踩过、修过的坑。


一、先用一张图,搞清楚"用户视角"的真实流转

教科书里的生命周期图过于抽象。我们换一种画法,从用户真实操作的角度看:

用户动作

触发的回调

你以为会发生

实际上会发生

首次打开 App

onCreate → onStart → onResume

一切就绪

一切就绪(这是唯一"干净"的路径)

按 Home 键回到桌面

onPause → onStop

App 完全死了

App 进程还活着,只是进入了 cached 状态

从最近任务切回来

onRestart → onStart → onResume

一切都和走之前一样

View 状态被系统保存了,但 非配置变更的数据不一定在

旋转屏幕

onPause → onStop → onDestroy → onCreate → ...

"我没保存数据"

Android 默认帮你重建 Activity,靠 onSaveInstanceState 救场

来电弹出

onPause(短暂)→ onResume

后台停了

只是 pause,onStop 不一定触发

记住这张表,比记住任何源码解读都有用——因为它是从真实事故里提炼出来的。


二、坑 1:在 onCreate 里做"重型操作"

事故现场

某个首页 Activity,onCreate 里同步读取了 SharedPreferences 里的一份 1.2MB 配置 JSON。冷启动时发现,从桌面图标点击到首屏可交互,足足卡了 800ms。

原因

onCreate 是主线程同步执行的。在 onCreate 里做任何 IO、文件解析、Bitmap 解码,都会直接拖慢首帧。

正确姿势

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    // 启动后再异步加载
    lifecycleScope.launch {
        val config = withContext(Dispatchers.IO) {
            loadConfigFromDisk()
        }
        applyConfig(config)
    }
}

这里用到 lifecycleScope,它会在 Activity 销毁时自动取消所有启动的协程——这就是我们接下来第 3 篇要讲的协程的妙处。

一个判断标准

onCreate 里只做"渲染 UI 必须做的事"。任何可以异步、可以延后、可以缓存的事,都不要放在 onCreate。


三、坑 2:横竖屏切换导致用户输入丢失

事故现场

一个填写表单的页面,用户填了 5 个字段,切了一次横屏,结果全没了。QA 复现了 5 次,每次都复现。

原因

默认情况下,配置变更(Configuration Change)会重建 Activity。EditText 的文本会自动保存,但业务数据不会——比如你把表单数据放在一个 Activity 字段里,重建之后那个字段是空的。

三种解决方案

方案 A:自己处理 onSaveInstanceState

class FormActivity : AppCompatActivity() {
    private var formDraft: FormDraft? = null

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        formDraft?.let { outState.putParcelable("draft", it) }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        formDraft = savedInstanceState?.getParcelable("draft")
            ?: FormDraft()
    }
}

方案 B:用 ViewModel(推荐)

class FormViewModel : ViewModel() {
    val formState = MutableStateFlow(FormDraft())
}

class FormActivity : AppCompatActivity() {
    private val viewModel: FormViewModel by viewModels()
    // 配置变更时,ViewModel 不会被重建,formState 自动保留
}

方案 C:禁止重建(不推荐,除非有特殊理由) 在 manifest 里加 android:configChanges="orientation|screenSize"。 代价:旋转后你得自己重新设置 View,且不能用 onConfigurationChanged 干重活——会卡顿。

个人建议:方案 B。配置变更本来就是 Android 的设计,不要绕过它。


四、坑 3:把"初始化 SDK"放在 onResume

事故现场

某 IM SDK 需要在用户进入聊天页时建立长连接。原本写在 onResume 里,结果发现用户从聊天列表快速滑过聊天页(点开又立即返回),SDK 的 connect/disconnect 来回打了好几次。

原因

onResume 每次可见都触发,包括从其他页面返回时。如果把"重型副作用"放在 onResume,行为会被用户操作频繁触发。

正确姿势:用 lifecycle-aware 组件

class ChatActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        lifecycle.addObserver(object : DefaultLifecycleObserver {
            override fun onResume(owner: LifecycleOwner) {
                // 轻量级:UI 状态刷新
                refreshUnreadBadge()
            }
        })

        // 重型副作用放在 STARTED 而非 RESUMED,可以用协程:
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                imClient.connect()
            }
        }
    }
}

repeatOnLifecycle(STARTED) 会在 STARTED 时启动、在 STOPPED 时取消——比 onStart/onStop 手动配对优雅得多。


五、坑 4:忽略后台时的"onStop 才做事"原则

事故现场

一个视频 App 把播放器释放逻辑放在了 onPause。结果从评论页切回视频页时,画面已经先冻住了 200ms,体验很差。

原因

onPause 触发时机非常早——另一层半透明 Activity 弹起来时也会触发。真要做"释放",得在 onStop 里做。

区分原则

你想做的是

应该放哪

暂停动画、释放相机、暂停视频

onPause(早释放)

断开长连接、保存草稿、停止定位

onStop(真离开)

显示一个 Dialog、临时让出焦点

onPause(不要停掉后台任务)

口诀:

onPause 管"还看得见",onStop 管"完全看不见"。


六、坑 5:相信"用户按返回时一定会走 onDestroy"

事故现场

某次崩溃统计里看到一批 IllegalStateException,全是从我们自己写的"清理代码"里抛出来的——我们假设 Activity 销毁时一定会走 onDestroy,然后清理里访问了一个早就被回收的对象。

真相

Activity 不一定走 onDestroy。比如:

  • 用户按 Home,进程被杀后再回来——onDestroy 根本不会走

  • 系统内存紧张时直接回收进程——onDestroy 也不会走

  • 用户只是切换了其他 Activity——onStop 之后进程被杀

正确姿势:把"必须做的清理"放进 ViewModel.onCleared 或 use case 的 finalize 里

class HeavyWorkViewModel : ViewModel() {
    private val job = SupervisorJob()

    fun startHeavyTask() {
        viewModelScope.launch(job) {
            // ...
        }
    }

    override fun onCleared() {
        super.onCleared()
        job.cancel() // ViewModel 销毁时一定会触发
    }
}

ViewModel 的 onCleared 是 Activity finish() 时一定会触发的(除非整个进程被杀,但那种情况本来就不用清理)。


小结

Activity 生命周期看似是入门第一课,但90% 的 Android 工程师在工作 3 年后还会被它咬一口。今天聊的 5 个坑,我特意挑的是工程中最高频的:

  1. onCreate 只做"渲染必需"——其他都异步。

  2. 配置变更用 ViewModel 兜底——别自己玩 Bundle。

  3. 重型副作用用 repeatOnLifecycle——别往 onResume 里塞。

  4. 释放资源看 onStop——onPause 管太宽。

  5. 清理放 ViewModel.onCleared——别赌 onDestroy。


下篇预告

下一篇是《第 2 篇:为什么 Android 工程师都在转 Compose?》。 我们将从 View 与 XML 的老故事讲起,聊 Compose 为什么是真正的范式转移,而不是又一个 UI 框架。


参考阅读

  • Android 官方文档:Activity Lifecycle

  • "Jetpack ViewModel 与 SavedStateHandle 最佳实践"

  • 《Android 编程权威指南》(第 4 版)第 3 章


评论