28. 启动优化案例分析:某App启动从2.5s优化到0.8s的实战复盘
说实话,做了这么多年性能优化,最让我头疼的就是相机启动。为什么?因为相机启动涉及的东西太多了——硬件初始化、预览流建立、算法加载、权限申请……任何一个环节卡住,用户看到的就是黑屏。
今天我就拿一个真实案例来复盘。这个App是我们之前做的社交产品,主打拍照分享。用户反馈最多的就是「打开相机太慢了」。我们一测,冷启动到首帧显示,平均2.5秒。嗯,这个数据在2024年的市场,基本等于劝退用户。
经过三轮优化,最终压到了0.8秒。怎么做到的?往下看。
先定位:到底慢在哪?
我个人的习惯是,拿到性能问题先不急着改代码。先问一个问题:这2.5秒都花在哪了?
我们用Systrace + Camera2的日志,把启动流程拆成了几个阶段:
| 阶段 | 耗时(ms) | 占比 |
|---|---|---|
| Application.onCreate | 320 | 12.8% |
| Activity.onCreate | 280 | 11.2% |
| CameraManager.openCamera | 650 | 26% |
| 创建预览Session | 720 | 28.8% |
| 首帧渲染 | 530 | 21.2% |
你看,最耗时的两个大头:openCamera 和 创建预览Session,加起来占了55%。说白了,就是Camera HAL层和底层硬件交互太慢了。
核心结论:相机启动优化的瓶颈,往往不在应用层代码,而在Camera HAL层和底层驱动的初始化流程。
第一刀:砍掉不必要的初始化
我们先看Application.onCreate。这里有个坑——很多第三方SDK的初始化都堆在这里。我检查了一下,发现有个美颜SDK的初始化占了150ms。但问题是,用户第一次打开相机时,根本不会立刻用到美颜功能。
我的做法很简单:延迟加载。把美颜SDK的初始化放到用户第一次点击美颜按钮时再触发。这一刀下去,直接省了150ms。
// 优化前:Application.onCreate 中初始化
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
BeautySDK.init(this); // 耗时150ms
}
}
// 优化后:按需加载
public class BeautyManager {
private static volatile boolean initialized = false;
public static void lazyInit() {
if (!initialized) {
synchronized (BeautyManager.class) {
if (!initialized) {
BeautySDK.init(App.getInstance());
initialized = true;
}
}
}
}
}
小技巧:不只是SDK初始化。所有「用户不一定马上用到」的资源,都可以考虑延迟加载。比如相机滤镜、人脸检测模型、AR贴纸等。
第二刀:CameraManager.openCamera 的优化
这个阶段最头疼。openCamera 的耗时,很大程度上取决于底层驱动。我们没法改HAL层代码,但可以换个思路——提前打开Camera。
具体怎么做?我们在用户点击「拍照」按钮之前,就提前执行 CameraManager.openCamera。比如用户从首页滑动到相机页时,我们预判用户行为,提前打开相机。
这里要注意一个细节:Camera的打开和关闭必须成对出现。如果用户最终没有进入相机页,你得把打开的Camera释放掉,否则会占用Camera资源,影响其他App。
// 预加载Camera
public class CameraPreloader {
private CameraDevice pendingCamera;
public void preOpenCamera(Context context) {
CameraManager manager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
try {
manager.openCamera("0", new CameraDevice.StateCallback() {
@Override
public void onOpened(CameraDevice camera) {
pendingCamera = camera;
}
@Override
public void onDisconnected(CameraDevice camera) {
camera.close();
}
@Override
public void onError(CameraDevice camera, int error) {
camera.close();
}
}, null);
} catch (CameraAccessException e) {
// 处理异常
}
}
// 如果用户没有进入相机页,释放资源
public void releaseIfNotUsed() {
if (pendingCamera != null) {
pendingCamera.close();
pendingCamera = null;
}
}
}
这一刀下去,openCamera 的耗时从650ms降到了几乎为0。为什么?因为Camera已经在后台打开了,用户进入相机页时直接复用。
注意:预加载Camera会额外消耗电量。建议只在用户大概率会进入相机页的场景下使用。比如社交App的「拍照」入口,用户点击率超过80%,就值得预加载。
第三刀:预览Session创建的并行化
创建预览Session为什么慢?因为它涉及Surface的配置、Stream的创建、底层硬件的初始化。我检查了代码,发现原来的实现是串行的——先配置TextureView,再创建Session。
其实这两个步骤可以并行。我们提前准备好Surface,在openCamera的回调中直接创建Session。这样,当用户看到预览界面时,Session已经准备好了。
// 优化前:串行执行
textureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
// 等Surface准备好了,才创建Session
createPreviewSession(surface);
}
});
// 优化后:提前准备Surface,并行创建Session
public void prepareSurface() {
SurfaceTexture texture = textureView.getSurfaceTexture();
if (texture != null) {
// 提前创建Surface
Surface surface = new Surface(texture);
// 在openCamera的回调中直接创建Session
cameraDevice.createCaptureSession(Arrays.asList(surface), ...);
}
}
这一刀,把创建Session的耗时从720ms降到了300ms。为什么还有300ms?因为底层硬件的初始化是绕不开的。但相比之前,已经快了一倍多。
第四刀:首帧渲染的加速
首帧渲染530ms,这个时间主要花在SurfaceFlinger合成和GPU渲染上。我们做了两件事:
- 降低首帧分辨率:预览分辨率从1920x1080降到1280x720。首帧渲染快了200ms。
- 使用SurfaceView代替TextureView:SurfaceView有独立的合成层,不需要经过View的绘制流程。首帧渲染快了100ms。
优化效果:首帧渲染从530ms降到了230ms。代价是首帧分辨率稍低,但用户几乎察觉不到。等首帧显示后,再动态切换到全分辨率。
最终效果:2.5s → 0.8s
三轮优化下来,我们看看数据:
| 阶段 | 优化前(ms) | 优化后(ms) | 优化手段 |
|---|---|---|---|
| Application.onCreate | 320 | 170 | 延迟加载SDK |
| Activity.onCreate | 280 | 200 | 布局优化、异步inflate |
| CameraManager.openCamera | 650 | 50 | 预加载Camera |
| 创建预览Session | 720 | 300 | 并行化、提前准备Surface |
| 首帧渲染 | 530 | 230 | 降低分辨率、SurfaceView |
| 总计 | 2500 | 950 |
实际线上数据是0.8s左右,因为还有系统级的一些优化(比如内存预分配、GC抑制)没有算进去。但核心的优化点,就是上面这四个。
核心逻辑流程图
下面这张图,展示了整个优化思路的脉络:
避坑指南
优化过程中,我也踩过一些坑。分享出来,希望大家别走弯路。
- 预加载Camera要注意生命周期:我曾经在某个版本里,预加载Camera后忘记释放,导致用户退出相机页后,Camera一直占用。结果其他App调用相机时直接崩溃。后来加了一个超时释放机制——如果30秒内用户没有进入相机页,自动释放Camera。
- 降低首帧分辨率要平滑过渡:一开始我们直接切分辨率,导致画面闪烁。后来用了渐变过渡,先显示低分辨率,再平滑切换到高分辨率。用户完全感知不到。
- 并行创建Session要处理好竞态条件:如果Surface还没准备好,Session创建会失败。我们加了一个等待队列,确保Surface准备就绪后再创建Session。
我的建议:相机启动优化,不要只盯着应用层代码。多看看底层日志,和硬件团队沟通。很多时候,一个HAL层的参数调整,比你在应用层折腾半天效果都好。
好了,这个案例就复盘到这里。0.8s的启动速度,虽然不是行业最快,但对于这个App来说,已经足够让用户满意了。优化没有终点,但每一步都值得。