24. 启动优化与稳定性:避免过度优化导致ANR、Crash、预览黑屏

启动优化做到一定阶段,你会发现一个尴尬的现实——优化本身正在成为稳定性的敌人

我见过太多团队,为了把冷启动时间压进500ms,各种奇技淫巧全用上。结果呢?ANR率飙升、Crash频发、预览界面一片漆黑。用户打开相机,等来的不是取景框,而是一个"无响应"的弹窗。这比启动慢更让人崩溃。

今天我们就来聊聊,如何在优化和稳定性之间找到那个平衡点。说白了,就是哪些优化可以做,哪些优化碰都不要碰。

过度优化的三大"坑"

先说说我踩过的坑。嗯,每一个都是真金白银换来的教训。

1. 异步初始化搞出ANR

很多人喜欢把CameraService的初始化扔到子线程,觉得这样能快。但问题是——预览Surface的创建依赖CameraService的初始化结果。如果你在子线程里初始化,主线程却在等Surface准备就绪,两者之间没有做好同步,主线程就会一直block住。

我曾经在一个项目里看到这样的代码:

// 错误示范:异步初始化但未正确处理依赖关系
Thread {
    cameraService.initialize() // 耗时操作
}.start()

// 主线程直接创建预览
surfaceView.holder.addCallback(object : SurfaceHolder.Callback {
    override fun surfaceCreated(holder: SurfaceHolder) {
        // 此时cameraService可能还没初始化完
        cameraService.startPreview(holder.surface) // 崩溃!
    }
})

结果就是,surfaceCreated回调来了,但CameraService还没准备好。轻则黑屏,重则直接Crash。更隐蔽的情况是——主线程因为等待某个锁,直接ANR。

⚠️ 避坑指南:异步初始化一定要处理好依赖链。如果A依赖B,B没初始化完,A绝对不能执行。用CountDownLatch或者回调机制,别偷懒。

2. 预加载过度导致内存压力

为了加快启动速度,有人会在Application.onCreate里预加载各种资源——相机参数、滤镜配置、人脸检测模型……

想法是好的,但后果很严重。预加载的东西越多,内存占用就越大。如果手机本身内存就不富裕,系统可能会直接杀掉你的进程。或者更恶心——频繁GC导致卡顿。

我记得有一次,我们在低端机上测试,预加载了一个3MB的人脸模型。结果相机启动是快了,但每次打开相机,系统都要杀掉后台的微信。用户反馈说"打开相机后微信就没了",这体验能好吗?

💡 我的建议:预加载要"按需加载",不是"一股脑加载"。比如人脸模型,等用户真正切换到人像模式再加载,完全来得及。启动阶段只加载必须的资源。

3. 预览Surface创建时机错乱

预览黑屏,是相机启动优化里最常见的坑。原因往往只有一个——Surface创建和Camera打开的时序乱了

正常的流程是:

  1. Camera打开成功
  2. 配置参数(分辨率、帧率等)
  3. Surface创建完成
  4. 将Surface设置给Camera
  5. 开始预览

但有些人为了"优化",把第1步和第3步并行执行。结果Surface创建好了,Camera还没打开;或者Camera打开了,Surface还没准备好。不管哪种情况,预览都是黑的

// 错误示范:时序错乱
cameraManager.openCamera(cameraId, callback, handler) // 异步打开

surfaceView.holder.addCallback(object : SurfaceHolder.Callback {
    override fun surfaceCreated(holder: SurfaceHolder) {
        // 此时Camera可能还没打开
        // 直接设置Surface会失败
        camera?.setPreviewSurface(holder.surface) // 可能为null
    }
})
⚠️ 注意:Camera打开是异步的,Surface创建也是异步的。两个异步操作必须同步。我的做法是——等两者都就绪了,再开始预览。多等几十毫秒,比黑屏强一万倍。

如何避免过度优化?

说了这么多坑,那到底该怎么把握这个度?我总结了几条原则。

原则一:优化要有"止损线"

不要为了优化而优化。设定一个合理的启动时间目标,比如1秒以内。达到了,就收手。别非要压到500ms,那多出来的500ms可能要用稳定性去换。

我习惯的做法是:

  • 先测量当前启动时间
  • 设定一个合理的目标(参考竞品、结合硬件能力)
  • 优化到目标值后,做一轮稳定性测试
  • 如果稳定性有问题,适当回退优化

原则二:异步操作必须"有界"

所有异步操作,都要设置超时。别让主线程无限期等待。

// 推荐做法:设置超时
val latch = CountDownLatch(1)
var isInitialized = false

thread {
    cameraService.initialize()
    isInitialized = true
    latch.countDown()
}

// 主线程等待,但最多等500ms
if (latch.await(500, TimeUnit.MILLISECONDS)) {
    // 初始化完成,继续
} else {
    // 超时了,走降级方案
    handleTimeout()
}

为什么要设超时?因为你永远不知道子线程里会发生什么。万一遇到死锁、死循环,主线程就跟着遭殃。设个超时,至少能保证App不ANR。

原则三:降级方案是"保命符"

优化做得再好,也要准备好"万一"的情况。比如:

  • 预加载失败 → 走实时加载
  • 异步初始化超时 → 走同步初始化
  • Surface创建失败 → 重新创建

我见过一个做得好的例子:某个App在Camera打开失败后,不是直接Crash,而是显示一个"相机不可用"的提示,同时尝试重新打开。用户虽然不能马上拍照,但至少App没挂掉。

📌 核心思想:优化是"锦上添花",稳定性是"雪中送炭"。任何时候,稳定性优先级高于优化。

稳定性测试清单

每次做完优化,我都会跑一遍这个清单:

测试项 测试方法 预期结果
快速打开/关闭相机 连续10次快速打开关闭 无Crash、无黑屏
低内存场景 后台跑大量App,再打开相机 启动正常,预览正常
异步操作超时 模拟CameraService初始化卡住 走降级方案,不ANR
Surface创建失败 模拟Surface创建异常 重试或提示用户
多线程竞争 同时触发多个异步操作 无死锁、无数据错乱

这个清单看起来简单,但能覆盖90%的稳定性问题。我每次发布新版本前,都会让测试同学重点跑这几项。

核心逻辑图

下面这张图,是我对"启动优化与稳定性平衡"的理解。你可以把它当作一个决策框架:

启动优化与稳定性平衡决策框架 启动优化目标 ✅ 安全的优化手段 异步初始化(有超时) 按需预加载(轻量级) 懒加载(非核心功能) 缓存复用(参数/配置) ❌ 过度优化(危险) 异步初始化(无超时) 预加载所有资源 时序错乱的并行操作 无降级方案的优化 启动快 + 稳定 ANR / Crash / 黑屏

你看,左边是安全的优化,右边是危险的优化。两者之间就差那么一点点——有没有超时、有没有降级、有没有处理好时序。但结果天差地别。

写在最后

启动优化这件事,做到60分很容易,做到90分也不难。但从90分到95分,每多1分,稳定性风险可能增加10分

我个人习惯是:先保证稳定,再追求速度。如果某个优化方案让我心里没底,我就不会上线。因为我知道,用户不会因为你启动快了200ms而夸你,但一定会因为闪退而骂你。

嗯,这就是我今天想分享的。记住一句话:优化是手段,稳定是底线。别为了手段,丢了底线。


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