启动优化(上):冷启动、热启动、温启动的区别,Application的onCreate优化,ContentProvider优化
启动优化,几乎是每个Android开发者都会遇到的坎儿。我记得刚入行那会儿,leader让我优化一个App的启动速度,我盯着logcat看了半天,愣是不知道从哪下手。后来踩了不少坑,才慢慢摸清楚门道。
今天咱们就聊聊启动优化的前半部分。先把基础概念理清楚,再动手干活。
冷启动、热启动、温启动,到底有啥区别?
这三个词,面试常问,实际开发也绕不开。说白了,它们的核心区别就是:进程还在不在。
| 启动类型 | 进程状态 | 触发场景 | 耗时 |
|---|---|---|---|
| 冷启动 | 进程不存在 | 首次安装、用户手动杀进程后打开 | 最长(通常1~5秒) |
| 热启动 | 进程存活,Activity可能被回收 | 按Home键后立即再打开 | 最短(几百毫秒) |
| 温启动 | 进程存活,但Activity被回收 | 按Back键退出后短时间内再打开 | 中等(1秒左右) |
冷启动为什么慢?因为系统要从零开始:加载Application、创建主线程、初始化ContentProvider、执行Application.onCreate()、创建MainActivity……这一套流程走下来,少说也得几百毫秒。
热启动就快多了。进程还在,Application对象还在内存里,甚至Activity的实例也可能还在。系统只需要重新创建Activity就行。
温启动介于两者之间。进程活着,但Activity被回收了。系统得重新创建Activity,但Application不用重新初始化。
我个人的习惯:做启动优化时,优先盯着冷启动。因为冷启动是用户感知最明显的场景。热启动和温启动,只要不搞出bug,基本不用太操心。
Application的onCreate优化——别让它成为启动瓶颈
Application.onCreate(),这地方太容易堆代码了。我见过一个项目,onCreate里干了这些事:
- 初始化十几个第三方SDK
- 加载本地配置文件
- 创建数据库连接
- 启动后台服务
- 注册广播接收器
结果呢?冷启动直接飙到3秒多。用户点开App,先看3秒白屏,你说他会不会卸载?
优化思路其实不复杂,核心就一句话:能延迟的绝不提前,能异步的绝不同步。
1. 按优先级分层初始化
把初始化任务分成三类:
- 必须同步的:比如崩溃收集SDK、性能监控SDK。这些需要在第一时间启动,否则后续的异常就抓不到了。
- 可以延迟的:比如推送SDK、广告SDK、统计SDK。这些晚几百毫秒初始化,用户根本感觉不到。
- 可以懒加载的:比如图片加载库的配置、网络库的初始化。等真正用到的时候再初始化就行。
一个小技巧:我习惯在Application.onCreate()里只做第一类初始化,剩下的全部扔到IdleHandler里。等主线程空闲了再慢慢搞。
// 示例:使用IdleHandler延迟初始化
getMainLooper().getQueue().addIdleHandler(new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
// 在这里初始化非关键SDK
initPushSDK();
initAdSDK();
initAnalyticsSDK();
// 返回false表示只执行一次
return false;
}
});
2. 避免在主线程做IO操作
SharedPreferences的读取、数据库的创建、文件的读写,这些IO操作少说几十毫秒,多则几百毫秒。放在主线程?那启动时间直接翻倍。
我曾经接手过一个项目,Application.onCreate()里调了三次SharedPreferences。每次读取一个几百K的配置文件。你想想看,三次IO,每次几十毫秒,加起来就是200多毫秒。这还只是读配置,还没算其他初始化呢。
解决方案很简单:异步IO。或者干脆把配置信息放到内存缓存里,启动时先读缓存,后台再慢慢加载完整配置。
3. 减少类加载的开销
Application.onCreate()里如果引用了大量类,这些类会在启动时被加载到内存。类加载本身不慢,但架不住数量多。几百个类一起加载,时间就上去了。
我建议:用反射代替直接引用,或者把初始化逻辑拆到单独的类里,用接口隔离。这样Application类本身就不会依赖太多东西。
注意:反射虽然能延迟类加载,但反射调用本身有性能损耗。别为了优化启动时间,把运行时性能搞崩了。权衡一下,找到平衡点。
ContentProvider优化——被忽视的启动杀手
ContentProvider,这东西很多人不重视。但你知道吗?所有ContentProvider的onCreate(),都是在Application.onCreate()之前执行的。
什么意思?就是说,你的Application.onCreate()还没跑,ContentProvider已经跑完了。如果ContentProvider里做了耗时操作,那启动时间直接就被拉长了。
我记得有一次,一个同事在ContentProvider的onCreate()里初始化了数据库,还做了数据迁移。结果冷启动时间多了800毫秒。他查了两天没找到原因,最后我一看logcat,发现ContentProvider的onCreate()耗时最长。
1. 减少ContentProvider的数量
每个第三方SDK都可能注册自己的ContentProvider。你打开AndroidManifest.xml看看,是不是有一堆?
这些ContentProvider在启动时都会被初始化。数量越多,启动越慢。我见过一个App,AndroidManifest里注册了12个ContentProvider。光初始化它们就花了1秒多。
怎么办?能合并的合并,能不用的不用。有些SDK的ContentProvider其实可以去掉,只要你不使用它的ContentProvider功能。
2. ContentProvider的onCreate()里别做耗时操作
这个道理和Application.onCreate()一样。数据库创建、文件读写、网络请求,这些统统不要放在ContentProvider的onCreate()里。
如果必须做,那就异步。但要注意:ContentProvider的onCreate()是在主线程执行的,异步操作也要小心别阻塞主线程。
// 不好的写法:直接在onCreate里做耗时操作
@Override
public boolean onCreate() {
// 创建数据库,耗时!
createDatabase();
// 加载配置文件,耗时!
loadConfig();
return true;
}
// 好的写法:异步初始化
@Override
public boolean onCreate() {
new Thread(new Runnable() {
@Override
public void run() {
createDatabase();
loadConfig();
}
}).start();
return true;
}
注意:异步初始化虽然不阻塞主线程,但如果ContentProvider的query/insert等方法在初始化完成前被调用,可能会出问题。记得加个标志位判断,或者用CountDownLatch等待初始化完成。
3. 考虑使用ContentProvider的懒加载
有些ContentProvider其实不需要在启动时初始化。比如你只在某个特定页面用到它,那完全可以等用户进入那个页面时再初始化。
怎么做?把ContentProvider从AndroidManifest里去掉,手动注册。等需要的时候再调用ContentResolver来触发初始化。
不过这个方法有点tricky,不是所有场景都适用。我一般只在自定义ContentProvider时用,第三方SDK的ContentProvider还是别动了,容易出问题。
总结一下
启动优化,说白了就是跟时间赛跑。冷启动、热启动、温启动,搞清楚它们的区别,才能对症下药。
Application.onCreate()的优化,核心是延迟、异步、减少依赖。ContentProvider的优化,核心是减少数量、避免耗时、考虑懒加载。
嗯,今天就先聊这么多。下一章咱们接着讲启动优化的下半部分:闪屏页优化、多进程启动优化、还有启动耗时监控。到时候再分享一些我踩过的坑。