4、冷启动、热启动与温启动的概念及区别
启动类型这个概念,说白了就是区分你的 App 从「死」到「活」到底经历了多少步骤。不同场景下,Activity 的创建成本天差地别。我个人习惯在埋点分析时先分清楚是哪种启动,不然你盯着一个 5 秒的启动耗时在发愁,结果发现是冷启动——其实已经算优化得很好了。
嗯,我们先看一张总览图,把三种启动的核心路径画出来。我管它叫「启动三兄弟」。
冷启动:从零开始
冷启动,就是用户点击图标时,你的 App 进程根本就不存在。需要系统先 fork 一个进程,加载 Application,再走一遍 Activity 的完整生命周期。
我曾经排查过一个线上的长启动问题,发现冷启动里 Application 的 onCreate 竟然干了将近 800ms 的 IO 操作。你想想看,用户从点击到看到首页,光 Application 就占了快一秒,后面的 Activity 还得再走一遍。后来我把那些操作扔到子线程,冷启动直接砍掉 40%。
冷启动的典型特征:
- 进程不存在,需要新建
- Application 的 onCreate 完整执行
- Activity 全部生命周期(构造 → onCreate → onStart → onResume)
- View 树、布局、资源首次加载
实测数据参考(来自我之前的项目):
| 阶段 | 耗时范围(毫秒) |
|---|---|
| 进程创建 + Application | 200~800 |
| Activity onCreate | 150~500 |
| onStart + onResume | 50~150 |
| 首帧渲染完成 | 50~200 |
注意:这里还没算 Application 里库的初始化,有些第三方 SDK 能吃掉 1 秒以上。
热启动:几乎零成本
热启动是什么?用户按了 Home 键,你的 Activity 还活在后端,系统没杀掉它。再点图标时,系统直接把 Activity 从栈里拉到前台,只走 onRestart → onStart → onResume。不会重新创建 Activity,更不会重建进程。
说白了,就是最快的一种启动。我见过很多同学把热启动的耗时也统计到「启动优化」里,其实没必要,热启动的 onResume 通常就几十毫秒,甚至比用户点感还快。重点要关注的永远是冷启动。
触发场景:
- 用户按 Home 键后立即再次点击图标
- 从最近任务列表切换回来
- 通知栏点击后返回(如果 Activity 未被回收)
一个小技巧:埋点时用 procStats 或者 ActivityManager.getProcessMemoryInfo() 判断进程是否存活,这样就能区分冷启动和热启动了。我曾经用这个办法发现某款手机在 6GB 内存下,后台 15 个应用后就会杀掉我们的进程,导致热启动概率骤降。
温启动:最容易被忽略的中间态
温启动,就是进程还在,但 Activity 已经被回收了。比如用户切到其他 App 玩了一段时间,系统内存紧张,把你的 Activity 对象回收(destory),但进程没杀。这时再回来,系统会重建 Activity——走一遍 onCreate,但 Application 不用重新初始化。
嗯,这里要注意:温启动的 onCreate 耗时和冷启动几乎一样!因为 Activity 的构造、布局加载、数据恢复都得重来。但 Application 不重新走,所以总耗时比冷启动少 200~600ms 左右。
我碰到过一个坑:在温启动场景下,有些同学在 Activity 的 onCreate 里做了「首次启动引导」之类的判断,但温启动时 Application 的某些全局变量可能已经变了(比如用户登录态),导致引导逻辑出错。我曾经因为这个踩过雷——温启动时用户明明已经登录了,还弹出登录引导页。后来加了进程内标记才解决。
温启动的典型特征:
- 进程存活
- Activity 实例已被销毁
- 重新走 onCreate → onStart → onResume
- Application 的 onCreate 不执行
三种启动的对比总结
| 比较项 | 冷启动 | 温启动 | 热启动 |
|---|---|---|---|
| 进程是否存在 | 否 | 是 | 是 |
| Application 是否重新初始化 | 是 | 否 | 否 |
| Activity 是否重建 | 是 | 是(重新创建) | 否(直接恢复) |
| 走哪些生命周期 | 完整:create→start→resume | 完整:create→start→resume | onRestart→onStart→onResume |
| 典型耗时(不包含首帧) | 0.5~2秒 | 0.2~1秒 | 20~100毫秒 |
| 埋点参考指标 | 进程创建到 onResume 结束 | Activity 创建到 onResume 结束 | onRestart 到 onResume 结束 |
避坑提示:我曾经犯过一个错误——在埋点上报时,把冷启动和温启动混在一起计算「平均启动耗时」。结果冷启动多的时候平均值很高,温启动多的时候就偏低,数据参考意义不大。正确做法是分开统计,或者只关注冷启动。你想想看,如果用户每次打开都走热启动,你却拿冷启动的标准去考核他,那优化方向就偏了。
如何准确识别这三种启动?
埋点代码里,我习惯用两个标志:一个进程存活标志(比如在 Application 的 attachBaseContext 里 set 一个全局变量),一个 Activity 是否被重建的标志(通过 savedInstanceState 是否为空,或者加一个 Application 级别的计数器)。
// 示例代码:Application 中记录进程启动时间
public class MyApp extends Application {
public static boolean isProcessCold = false;
public static long appCreateTime;
@Override
public void onCreate() {
super.onCreate();
isProcessCold = true;
appCreateTime = SystemClock.elapsedRealtime();
// 初始化其他库 ...
}
}
// Activity 中判断启动类型
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (MyApp.isProcessCold) {
// 冷启动
} else if (savedInstanceState == null) {
// 温启动(进程存活,但 Activity 首次创建)
} else {
// 旋转屏幕等重建,不算启动
}
MyApp.isProcessCold = false; // 重置
}
}
注意:这个方案有个小瑕疵——如果 App 在后台待了很久,系统回收了 Activity 但没杀进程,此时 savedInstanceState 可能是 null(取决于系统是否保存)。更严谨的做法是监听 ActivityLifecycleCallbacks 并记录 Activity 个数。
我个人更推荐用 ActivityManager.getProcessRunningInfo() 或者直接读 /proc/self/status 里的进程启动时间来判断。但为了简单,上面的代码已经够应付 90% 的场景了。