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。
2. 预加载过度导致内存压力
为了加快启动速度,有人会在Application.onCreate里预加载各种资源——相机参数、滤镜配置、人脸检测模型……
想法是好的,但后果很严重。预加载的东西越多,内存占用就越大。如果手机本身内存就不富裕,系统可能会直接杀掉你的进程。或者更恶心——频繁GC导致卡顿。
我记得有一次,我们在低端机上测试,预加载了一个3MB的人脸模型。结果相机启动是快了,但每次打开相机,系统都要杀掉后台的微信。用户反馈说"打开相机后微信就没了",这体验能好吗?
3. 预览Surface创建时机错乱
预览黑屏,是相机启动优化里最常见的坑。原因往往只有一个——Surface创建和Camera打开的时序乱了。
正常的流程是:
- Camera打开成功
- 配置参数(分辨率、帧率等)
- Surface创建完成
- 将Surface设置给Camera
- 开始预览
但有些人为了"优化",把第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
}
})
如何避免过度优化?
说了这么多坑,那到底该怎么把握这个度?我总结了几条原则。
原则一:优化要有"止损线"
不要为了优化而优化。设定一个合理的启动时间目标,比如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%的稳定性问题。我每次发布新版本前,都会让测试同学重点跑这几项。
核心逻辑图
下面这张图,是我对"启动优化与稳定性平衡"的理解。你可以把它当作一个决策框架:
你看,左边是安全的优化,右边是危险的优化。两者之间就差那么一点点——有没有超时、有没有降级、有没有处理好时序。但结果天差地别。
写在最后
启动优化这件事,做到60分很容易,做到90分也不难。但从90分到95分,每多1分,稳定性风险可能增加10分。
我个人习惯是:先保证稳定,再追求速度。如果某个优化方案让我心里没底,我就不会上线。因为我知道,用户不会因为你启动快了200ms而夸你,但一定会因为闪退而骂你。
嗯,这就是我今天想分享的。记住一句话:优化是手段,稳定是底线。别为了手段,丢了底线。
公众号:蓝海资料掘金营,微信deep3321