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抑制)没有算进去。但核心的优化点,就是上面这四个。

核心逻辑流程图

下面这张图,展示了整个优化思路的脉络:

相机启动优化核心逻辑 第一步:问题定位(2.5s) 延迟加载SDK Application层优化 预加载Camera openCamera优化 并行创建Session 预览Session优化 降低首帧分辨率 首帧渲染优化 按需初始化 异步加载 减少150ms 预判用户行为 提前打开Camera 减少600ms 提前准备Surface 并行创建Session 减少420ms 720p预览 SurfaceView替代 减少300ms 最终效果:2.5s → 0.8s(优化68%) 总优化耗时:150+600+420+300 = 1470ms 注:实际优化效果因设备和场景略有差异

避坑指南

优化过程中,我也踩过一些坑。分享出来,希望大家别走弯路。

  • 预加载Camera要注意生命周期:我曾经在某个版本里,预加载Camera后忘记释放,导致用户退出相机页后,Camera一直占用。结果其他App调用相机时直接崩溃。后来加了一个超时释放机制——如果30秒内用户没有进入相机页,自动释放Camera。
  • 降低首帧分辨率要平滑过渡:一开始我们直接切分辨率,导致画面闪烁。后来用了渐变过渡,先显示低分辨率,再平滑切换到高分辨率。用户完全感知不到。
  • 并行创建Session要处理好竞态条件:如果Surface还没准备好,Session创建会失败。我们加了一个等待队列,确保Surface准备就绪后再创建Session。

我的建议:相机启动优化,不要只盯着应用层代码。多看看底层日志,和硬件团队沟通。很多时候,一个HAL层的参数调整,比你在应用层折腾半天效果都好。

好了,这个案例就复盘到这里。0.8s的启动速度,虽然不是行业最快,但对于这个App来说,已经足够让用户满意了。优化没有终点,但每一步都值得。