启动优化(上):冷启动、热启动、温启动的区别,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的优化,核心是减少数量、避免耗时、考虑懒加载

嗯,今天就先聊这么多。下一章咱们接着讲启动优化的下半部分:闪屏页优化、多进程启动优化、还有启动耗时监控。到时候再分享一些我踩过的坑。