☰
Android活动生命周期详解:从崩溃到精通的实战指南
2026/10/14 3:10:51 网站建设 项目流程

1. 从一次崩溃说起:为什么必须吃透活动生命周期

刚接触安卓开发那会儿,我写过一个特别简单的记账小工具,两个页面:一个列表页,一个编辑页。功能跑起来没问题,但用着用着就发现一个诡异现象——在编辑页填了一半的数据,切到后台接了个电话再回来,数据全没了,页面还回到了列表页。当时我以为是内存不够被系统杀了,后来才明白,这跟内存没关系,是我根本没搞清楚活动(Activity)的生命周期到底是怎么运转的。

这个坑几乎每个安卓开发者都踩过。活动是安卓四大组件里最常打交道的一个,它代表一个拥有用户界面的屏幕,用户看到的每一个页面背后都是一个活动实例。而生命周期,说白了就是系统在不同时刻回调活动的一系列方法,告诉你"我现在创建你了""我现在让你显示了""我现在把你藏起来了""我现在要销毁你了"。你只有在对的回调里做对的事,页面状态才不会丢,资源才不会泄漏,用户体验才不会崩。

这篇文章我打算把活动生命周期从头到尾捋一遍,不光是列那几个回调方法的名字,而是讲清楚每个回调什么时候触发、为什么这么设计、在里面该干什么不该干什么。适合刚入门安卓、被生命周期绕晕的新手,也适合写了几年代码但一直靠"能跑就行"混过来的朋友。我会结合自己踩过的坑,把配置变更、进程被杀、状态保存这些容易翻车的地方都讲透。看完你至少能做到:知道数据该存在哪、资源该在哪释放、横竖屏切换不再丢数据。

2. 七个核心回调:活动从生到死的完整轨迹

2.1 onCreate:只做初始化,别干重活

onCreate()是活动生命周期的起点,系统创建活动实例后第一个调用的就是这个方法。它接收一个Bundle参数,这个参数就是上次被系统保存下来的状态数据,如果活动是第一次创建,它就是null。

很多人习惯把所有的初始化代码一股脑塞进onCreate(),包括网络请求、数据库查询、大量视图构建。这是个坏习惯。onCreate()执行期间活动还没显示出来,你在这里做耗时操作,用户看到的就是一个白屏或者黑屏,体验极差。正确的做法是:onCreate()里只做必须的、轻量的初始化,比如调用setContentView()加载布局、绑定视图引用、初始化必要的成员变量。耗时的数据加载应该放到后台线程,等结果回来再更新 UI。

还有一个细节:onCreate()里拿到的Bundle是系统在活动被意外销毁前通过onSaveInstanceState()存下来的。如果你在这里恢复数据,一定要先判断savedInstanceState != null,否则第一次启动时会空指针。

@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if (savedInstanceState != null) { String draft = savedInstanceState.getString("draft_text"); if (draft != null) { editText.setText(draft); } } }

注意:onCreate()里千万不要做异步回调后直接操作还没初始化的视图,顺序错了就是空指针。

2.2 onStart 与 onResume:可见和可交互是两回事

onCreate()之后,系统会依次调用onStart()和onResume()。这两个方法很容易被混为一谈,但它们描述的是活动的两种不同状态。

onStart()被调用时,活动变得可见,但还不能和用户交互。你可以把它理解成"活动已经出现在屏幕上了,但还隔着一层玻璃"。而onResume()被调用时,活动进入前台并且可以接收用户输入,玻璃没了,用户能点能滑了。

为什么要把"可见"和"可交互"分开?因为有些场景下活动确实可见但不需要交互,比如一个半透明的对话框活动盖在另一个活动上面,下面的活动还是可见的,但它已经失去了焦点,系统会调用它的onPause()而不是onStop()。理解这个区别,你才能明白为什么有些资源释放要放在onStop()而不是onPause()。

在实际开发中,onStart()里通常做的是"准备让用户看到的东西",比如注册一些只在可见时才需要的监听器;onResume()里则做"准备让用户操作的东西",比如开始动画、恢复传感器监听、刷新实时数据。

2.3 onPause:别在这里做耗时操作

onPause()是活动失去焦点时调用的,意味着用户要离开这个活动了,但它可能只是被一个对话框部分遮挡,仍然可见。这个方法有一个非常重要的特性:系统调用完当前活动的onPause()之后,才会去启动或恢复下一个活动。也就是说,如果你在onPause()里写了耗时操作,下一个活动的显示就会被卡住。

我见过有人在onPause()里保存大量数据到数据库,结果用户点返回键时明显感觉到卡顿。正确的做法是:onPause()里只做轻量的、必须立即完成的操作,比如暂停正在播放的视频、停止正在运行的动画、保存用户正在编辑的草稿到内存变量。真正耗时的持久化操作应该放到onStop()或者用后台线程处理。

onPause()之后,活动可能走向两个方向:如果用户返回了,系统会调用onResume();如果活动完全被遮挡了,系统会继续调用onStop()。

2.4 onStop 与 onDestroy:释放资源的正确时机

onStop()在活动完全不可见时调用。这时候活动还在内存里,但用户已经看不到它了。onStop()是释放较重资源的好时机,比如注销广播接收器、关闭数据库连接、停止后台服务绑定。因为这些操作如果放在onPause()里会影响下一个活动的启动速度,放在onStop()里就从容多了。

onDestroy()是活动被销毁前的最后一个回调。它有两种触发场景:一是用户主动关闭了活动(比如按了返回键),二是系统因为配置变更(如旋转屏幕)或内存不足而回收活动。在onDestroy()里,你应该做最终的资源清理,确保没有内存泄漏。

这里有个关键点:onDestroy()不保证一定会被调用。如果系统内存极度紧张,可能直接杀掉进程,活动连onDestroy()都来不及执行。所以不要把必须执行的关键逻辑放在onDestroy()里,比如"退出时上报数据"这种需求,应该放在onStop()里做。

2.5 完整回调顺序一览

把上面这些串起来,一个活动从启动到销毁的完整回调顺序是这样的:

场景回调顺序
首次启动onCreate → onStart → onResume
用户按Home键onPause → onStop
从后台返回onRestart → onStart → onResume
弹出对话框活动onPause(当前活动)
关闭对话框活动onResume(当前活动)
用户按返回键onPause → onStop → onDestroy
旋转屏幕onPause → onStop → onDestroy → onCreate → onStart → onResume

这张表建议你打印出来贴在显示器旁边,写代码的时候对照着看,时间久了自然就记住了。

3. 配置变更这道坎:旋转屏幕为什么总丢数据

3.1 旋转屏幕背后的销毁重建机制

很多新手第一次遇到旋转屏幕丢数据时,第一反应是"我的代码有bug"。其实这不是bug,是安卓的默认行为。当屏幕方向、语言、字体大小等配置发生变化时,系统默认会销毁当前活动并重新创建一个新的,因为新的配置可能需要加载不同的资源(比如横屏布局、不同语言的字符串)。

这个销毁重建的过程会走完整的生命周期:onPause()→onStop()→onDestroy()→onCreate()→onStart()→onResume()。注意,onDestroy()和onCreate()之间,系统会调用onSaveInstanceState()把当前活动的状态保存到一个Bundle里,然后在新的onCreate()中把这个Bundle传回来。

所以,如果你在活动里有一个EditText,用户输入了内容,旋转屏幕后内容丢失,原因就是你没有在onSaveInstanceState()里保存这个内容,或者没有在onCreate()里恢复它。

3.2 onSaveInstanceState 的正确用法

onSaveInstanceState()的调用时机是在onStop()之前,但它和生命周期的其他回调不是严格的顺序关系。你只需要记住:系统会在活动可能被销毁前调用它,让你有机会保存状态。

这个方法的默认实现会保存视图层级中带有id的视图的状态,比如EditText里的文字、CheckBox的勾选状态。但如果你用的是自定义视图,或者数据不在视图里(比如一个列表的数据源),就需要自己手动保存。

@Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putString("draft_text", editText.getText().toString()); outState.putInt("scroll_position", scrollView.getScrollY()); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if (savedInstanceState != null) { editText.setText(savedInstanceState.getString("draft_text", "")); final int scrollY = savedInstanceState.getInt("scroll_position", 0); scrollView.post(() -> scrollView.scrollTo(0, scrollY)); } }

注意:onSaveInstanceState()里保存的数据不宜过大,它会被序列化到系统进程中,数据太大可能引发TransactionTooLargeException。一般只保存关键的 UI 状态,大量数据应该用数据库或文件持久化。

3.3 用 ViewModel 扛住配置变更

onSaveInstanceState()能解决 UI 状态丢失的问题,但它不适合保存大量数据,而且只适用于配置变更导致的销毁重建。如果你的活动里有一个耗时的网络请求,旋转屏幕后请求重新发起,那就太浪费了。

这时候ViewModel就派上用场了。ViewModel的生命周期比活动长,它在配置变更时不会被销毁,新的活动实例可以拿到同一个ViewModel实例。这样你把数据放在ViewModel里,旋转屏幕后数据还在,不需要重新加载。

public class MyViewModel extends ViewModel { private MutableLiveData<List<Item>> items = new MutableLiveData<>(); public LiveData<List<Item>> getItems() { return items; } public void loadItems() { // 只在第一次加载时请求网络 if (items.getValue() == null) { // 发起网络请求... } } }

在活动中通过new ViewModelProvider(this).get(MyViewModel.class)获取实例,这样配置变更时数据不会丢,网络请求也不会重复发起。

3.4 声明 configChanges 是省事还是埋雷

还有一种做法是在清单文件里给活动声明android:configChanges="orientation|screenSize",这样旋转屏幕时系统不会销毁重建活动,而是回调onConfigurationChanged()方法。表面上看这样省事,数据不会丢,但我不推荐新手这么做。

原因有三:第一,声明了configChanges之后,你需要自己处理所有配置变化的适配,包括重新加载资源、调整布局,工作量反而更大;第二,横屏和竖屏可能需要完全不同的布局,自己处理容易出错;第三,这种做法会掩盖你对生命周期理解不足的问题,长期来看不利于成长。

我的建议是:优先用ViewModel+onSaveInstanceState()的组合来应对配置变更,只有在确实需要精细控制、且明确知道自己在做什么的情况下,才考虑configChanges。

4. 进程被杀与状态恢复:那些你以为不会发生的事

4.1 后台活动被系统回收的真实场景

用户按了Home键,你的活动进入后台,看起来一切正常。但如果用户接下来打开了相机、玩了一局游戏,系统内存吃紧,就可能会把后台的进程杀掉。这时候你的活动实例已经不存在了,但系统会记住这个活动曾经存在,并且保存了它的状态。

当用户再次从最近任务列表切回你的应用时,系统会重新创建活动,并且把之前保存的Bundle传给onCreate()。如果你没有正确处理这个Bundle,用户看到的就是一个空白页面,之前填的数据全没了。

这个场景在低端机上尤其常见,但很多开发者在高端机上测试时根本遇不到,上线后才收到用户反馈。所以一定要在模拟器上开启"不保留活动"选项来测试,路径是:设置 → 开发者选项 → 不保留活动。开启后,每次离开活动它都会被销毁,能帮你快速发现状态恢复的问题。

4.2 判断活动是"重建"还是"新建"

在onCreate()里,你可以通过savedInstanceState是否为null来判断活动是全新创建还是从保存的状态重建。但有时候你还需要区分"配置变更导致的重建"和"进程被杀后的重建",这两种情况的处理逻辑可能不同。

一个常用的技巧是检查savedInstanceState里是否包含你保存的特定标记。如果是配置变更,ViewModel还在,数据可以从ViewModel拿;如果是进程被杀,ViewModel已经没了,数据只能从Bundle或者持久化存储里恢复。

@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); MyViewModel viewModel = new ViewModelProvider(this).get(MyViewModel.class); if (savedInstanceState != null) { // 从保存的状态恢复 String draft = savedInstanceState.getString("draft_text"); if (draft != null) { editText.setText(draft); } } // ViewModel 里的数据在配置变更时还在,进程被杀后为空 if (viewModel.getItems().getValue() == null) { viewModel.loadItems(); } }

4.3 持久化才是终极保险

onSaveInstanceState()和ViewModel能解决大部分问题,但它们都不是万无一失的。Bundle有大小限制,ViewModel在进程被杀后也会消失。如果你的应用有"用户填了一半的表单,下次打开还要能继续填"这种需求,就必须用持久化存储。

我一般会用SharedPreferences存轻量的键值对,用数据库存结构化的数据。保存的时机可以选在onPause()或onStop(),因为这两个回调在大多数情况下都会被调用。虽然onStop()不保证一定执行,但配合onSaveInstanceState()一起用,覆盖率已经很高了。

@Override protected void onStop() { super.onStop(); SharedPreferences prefs = getSharedPreferences("draft", MODE_PRIVATE); prefs.edit().putString("draft_text", editText.getText().toString()).apply(); }

注意:apply()是异步写入,commit()是同步写入。在onStop()里用apply()就够了,不会阻塞主线程。

5. 实战中的生命周期陷阱与排查思路

5.1 内存泄漏:静态引用和匿名内部类

生命周期用不好,最常见的问题就是内存泄漏。比如你在活动里注册了一个广播接收器,但忘记在onDestroy()里注销,这个接收器就会一直持有活动的引用,导致活动无法被回收。再比如你用一个静态变量持有了活动的Context,或者用匿名内部类创建了一个耗时任务,任务没结束前活动就被销毁了,内存就泄漏了。

排查内存泄漏的利器是LeakCanary,集成后它会在应用运行时自动检测泄漏并给出引用链。我建议每个项目都加上它,尤其是新手,能帮你快速定位问题。

public class MainActivity extends AppCompatActivity { private static Context leakedContext; // 千万不要这样写 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); leakedContext = this; // 活动销毁后,这个静态引用还在,内存泄漏 } }

正确的做法是:能用Application Context的地方就用它,必须用活动Context的地方确保在onDestroy()里释放引用。

5.2 在错误的回调里更新 UI

活动已经onStop()了,你还在后台线程里更新 UI,轻则无效,重则崩溃。比如一个网络请求在活动进入后台后才返回,你直接调用textView.setText(),如果活动已经被销毁,就会抛IllegalStateException。

解决办法是在更新 UI 前检查活动是否还处于活跃状态。可以用isFinishing()和isDestroyed()来判断:

if (!isFinishing() && !isDestroyed()) { textView.setText(result); }

更好的做法是用LiveData,它自带生命周期感知能力,只有在活动处于活跃状态时才会通知观察者更新 UI,活动销毁后自动移除观察者,从根本上避免了这个问题。

5.3 生命周期感知组件的正确姿势

安卓提供了一系列生命周期感知组件,比如LiveData、ViewModel、LifecycleObserver。用它们的好处是你不需要手动在onStart()和onStop()里注册和注销监听器,组件会自动跟随活动的生命周期。

比如你要在活动可见时监听位置变化,不可见时停止监听,传统做法是在onStart()里注册、onStop()里注销。用LifecycleObserver可以这样写:

public class LocationObserver implements LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_START) public void startListening() { // 开始监听位置 } @OnLifecycleEvent(Lifecycle.Event.ON_STOP) public void stopListening() { // 停止监听位置 } }

然后在活动里getLifecycle().addObserver(new LocationObserver())就行了。这样代码更清晰,也不容易漏掉注销操作。

5.4 一个完整的排查案例

我之前遇到过一个 bug:用户在详情页点击"收藏"按钮,按钮状态变成了"已收藏",但退出详情页再进来,按钮又变回了"未收藏"。排查过程是这样的:

第一步,确认数据是否真的保存了。我在onPause()里加了日志,发现收藏操作确实写入了数据库,数据没问题。

第二步,检查详情页加载时是否读取了收藏状态。发现读取逻辑写在onCreate()里,但读取的是内存缓存,而内存缓存在活动销毁后就被清空了。

第三步,定位到问题:内存缓存的生命周期和活动绑定了,活动销毁缓存就没了。解决方案是把缓存放到ViewModel里,或者每次加载时从数据库读取。

这个案例说明,生命周期问题往往不是"代码写错了",而是"数据放错了地方"。理解每个回调的时机,才能把数据放在正确的容器里。

6. 把生命周期用成肌肉记忆

写了这么多年安卓,我的体会是:生命周期这东西,看一遍文档觉得懂了,写代码时该踩的坑一个不少。真正掌握它的标志,是你看到一个需求时,脑子里能自动浮现出"这个操作该放在哪个回调里"。

比如用户说"我要在页面不可见时停止播放视频",你立刻知道放onStop();用户说"我要在页面还能被看到但失去焦点时暂停动画",你知道放onPause();用户说"我要保证旋转屏幕后表单内容不丢",你知道用ViewModel加onSaveInstanceState()。

最后分享一个我自己的习惯:每次新建一个活动,先把所有生命周期回调都重写一遍,加上日志,跑一遍应用,观察每个操作触发了哪些回调。这个笨办法帮我建立起了对生命周期的直觉,比看十遍文档都管用。等你对回调顺序烂熟于心,再回头去看那些"为什么数据丢了""为什么崩溃了"的问题,答案往往一目了然。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询