性能优化之启动优化:冷启动与热启动、启动任务依赖、懒加载

启动优化,说白了就是让用户少等几秒。

我见过太多App,功能做得挺牛,但一打开就是白屏两三秒。用户哪有耐心等你?直接划走,去隔壁了。今天咱们就聊聊启动优化这件事。我会结合自己踩过的坑,把冷启动、热启动、任务依赖、懒加载这几个核心点讲透。

冷启动 vs 热启动:区别在哪?

先搞清楚概念。冷启动,就是进程从零开始创建。用户点图标,系统创建进程,加载Application,创建MainActivity。整个过程,所有资源都得重新加载。

热启动呢?App已经在后台了,只是被切到前台。Activity可能还在内存里,甚至都不用重新走onCreate。说白了,热启动就是“唤醒”,冷启动是“重生”。

核心差异:冷启动需要重新创建进程、加载Application、初始化各种SDK。热启动只需要恢复Activity状态。所以冷启动的优化空间更大,也是我们重点关注的。

我在项目中遇到过一个问题:用户反馈App启动慢,但测试同学说“我测了,很快啊”。后来发现,测试同学每次都是热启动,而用户是冷启动。嗯,这就是典型的“测试环境与用户环境脱节”。从那以后,我要求所有启动性能测试,必须基于冷启动。

启动任务依赖:别让初始化变成“串行排队”

很多App的Application.onCreate里,是一长串的初始化代码:

// 糟糕的写法:所有任务串行执行
public class MyApp extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        initCrashReport();      // 1. 崩溃收集
        initAnalytics();        // 2. 统计
        initPush();             // 3. 推送
        initImageLoader();      // 4. 图片加载
        initDatabase();         // 5. 数据库
        initNetwork();          // 6. 网络库
        initLocation();         // 7. 定位
        initShare();            // 8. 分享
    }
}

你想想看,这8个任务一个接一个跑,就算每个只花50ms,加起来也400ms了。用户就在那干等着。

正确的做法是什么?找出任务之间的依赖关系,能并行的就并行。

我习惯先画一张依赖图。比如:

启动任务依赖关系图 Application.onCreate 崩溃收集 统计SDK 网络库 定位SDK 推送SDK(依赖网络) 图片加载(依赖网络) 数据库(无依赖) 分享SDK(依赖统计+网络) 首页渲染完成

看到没?崩溃收集、统计SDK、网络库、定位SDK,这四个没有依赖关系,完全可以并行初始化。推送和图片加载依赖网络库,得等网络库初始化完才能跑。分享SDK依赖统计和网络,得等两个都完成。数据库没有依赖,也可以并行。

我建议用有向无环图(DAG)来管理这些依赖。Android上可以用androidx.startup库,或者自己写一个轻量级的任务调度器。

我的习惯:我会把启动任务分成三类:

  • 必须同步的:比如崩溃收集,必须在其他任务之前初始化,否则出了问题抓不到日志。
  • 可以异步的:比如统计、推送,不阻塞主线程就行。
  • 可以懒加载的:比如分享、定位,用户不一定马上用到,完全可以等首页渲染完再初始化。

懒加载:把“非必要”任务往后推

懒加载,说白了就是“用到的时候再加载”。

我见过很多App,一启动就把所有功能都初始化了。用户可能只是打开看一眼就退出了,结果你帮他把定位、推送、分享全准备好了。这不是浪费吗?

举个例子,分享功能。用户可能打开App后,浏览了好几个页面才点分享。那分享SDK完全可以在用户点击分享按钮的那一刻再初始化。这样冷启动时就少了一个任务。

// 懒加载示例:分享SDK
class ShareManager {
    private var sdkInitialized = false
    
    fun share(content: String) {
        if (!sdkInitialized) {
            initShareSDK()  // 第一次使用时才初始化
            sdkInitialized = true
        }
        // 执行分享逻辑
    }
    
    private fun initShareSDK() {
        // 初始化分享SDK,耗时约100ms
    }
}

嗯,这里要注意:懒加载不是万能的。有些SDK初始化需要时间,如果用户点击分享后要等200ms才能弹出分享面板,体验也不好。所以我的做法是:在首页渲染完成后,利用空闲时间提前初始化那些“即将可能用到”的SDK。

我曾经踩过的坑:有一次我把定位SDK做了懒加载,结果用户进入地图页面时,定位还没初始化完成,导致地图上显示“定位中...”转了好几秒。后来我改成:首页渲染完成后,立即在后台线程初始化定位SDK。这样用户进入地图页面时,定位已经准备好了。

实战:一个完整的启动优化方案

说了这么多,咱们来点实际的。我一般会这样设计启动流程:

阶段 执行内容 线程 说明
阶段一:Application.onCreate 崩溃收集、日志系统 主线程 必须最先初始化,确保能捕获后续所有异常
阶段二:并行初始化 网络库、统计SDK、数据库、定位SDK 子线程 无依赖的任务,用线程池并行执行
阶段三:依赖初始化 推送SDK、图片加载 子线程 等待网络库初始化完成后执行
阶段四:首页渲染 加载首页布局、数据 主线程 此时只渲染首页,不做其他事情
阶段五:空闲加载 分享SDK、支付SDK、其他功能 子线程 首页渲染完成后,利用IdleHandler在空闲时加载

你看,这样设计后,冷启动时主线程只做了两件事:崩溃收集和首页渲染。其他任务都在子线程并行执行。我实测过,一个原本需要1.2秒冷启动的App,优化后降到了400ms以内。

核心原则:主线程只做“必须马上做的事”,其他能异步就异步,能懒加载就懒加载。启动优化的本质,就是把“串行”变成“并行”,把“同步”变成“异步”,把“提前”变成“按需”。

测量与验证:没有数据就没有优化

最后说一句:没有数据支撑的优化,都是耍流氓。

我习惯在App里埋点,记录冷启动的每个阶段耗时。比如:

  • Application.onCreate开始时间
  • 每个SDK初始化完成时间
  • 首页onResume时间
  • 首页第一帧渲染时间

有了这些数据,你才能知道瓶颈在哪。是网络库初始化太慢?还是数据库创建表太耗时?对症下药,才能药到病除。

我曾经遇到一个案例:数据显示首页渲染耗时300ms,但用户感知的启动时间却是800ms。后来发现,是首页的某个图片加载阻塞了主线程。嗯,这就是典型的“你以为优化了,其实没有”。

启动优化没有银弹。每个App的情况都不一样。但只要你掌握了冷启动与热启动的区别、任务依赖的管理、懒加载的策略,再加上数据驱动的验证方法,就一定能找到最适合自己App的优化方案。


公众号:蓝海资料掘金营,微信deep3321