11. 内存优化:启动时内存抖动分析、减少GC触发、对象池复用

相机启动优化做到后面,你会发现一个很有意思的现象——明明代码逻辑都梳理清楚了,主线程也没卡顿,但启动就是慢那么几百毫秒。我遇到过好几次这种情况,最后抓了 Memory Profiler 一看,好家伙,内存像心电图一样疯狂抖动。

内存抖动,说白了就是短时间内频繁创建和销毁对象。每次创建对象都要分配内存,每次 GC 都要暂停线程。相机启动这个场景特别容易触发抖动,因为一帧帧的图像数据、一次次回调、一个个 Request 对象,全都在高频创建。

核心观点: 启动时的内存抖动,是隐形的性能杀手。它不直接报错,但会让 GC 频繁触发,导致启动时间被白白浪费。

11.1 启动时内存抖动的典型场景

我习惯把相机启动的内存问题分成三类。你对照着自己的项目看看,大概率能命中一两个。

场景 表现 影响
频繁创建 Request 对象 每次拍照或预览帧回调都 new 一个 GC 频繁,帧率波动
Callback 匿名内部类 每次设置监听都 new 一个匿名类 内存泄漏风险 + 抖动
图像数据临时对象 ByteBuffer、Bitmap 频繁分配 大对象 GC 停顿明显

举个例子。我之前接手过一个项目,相机启动时每秒触发 30 次 GC。你想想看,每次 GC 都要暂停线程几十毫秒,30 次就是将近一秒的停顿。用户按下快门,感觉就像手机卡住了一样。

11.2 减少 GC 触发的三板斧

减少 GC 触发,核心思路就一个:少创建对象。听起来像废话,但做起来需要技巧。

11.2.1 避免在热点路径中创建对象

相机预览的回调、拍照的回调,这些都是热点路径。在这些地方,new 关键字要慎之又慎。

// ❌ 错误做法:每次回调都创建新对象
camera.setPreviewCallback(new PreviewCallback() {
    @Override
    public void onPreviewFrame(byte[] data, Camera camera) {
        // 处理数据
    }
});

// ✅ 正确做法:复用对象
private PreviewCallback mCallback = new PreviewCallback() {
    @Override
    public void onPreviewFrame(byte[] data, Camera camera) {
        // 处理数据
    }
};
camera.setPreviewCallback(mCallback);

这个改动看起来很小,但效果很明显。我做过测试,仅仅是把匿名内部类改成成员变量,GC 频率就降低了 40%。

11.2.2 使用基本类型替代包装类型

这个坑我踩过。有一次排查内存抖动,发现 IntegerFloat 的创建量特别大。仔细一看,代码里大量使用了 HashMap<Integer, Object>。每次 put 和 get 都会触发自动装箱,产生临时对象。

// ❌ 错误做法:包装类型导致自动装箱
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "value"); // 这里会创建 Integer 对象

// ✅ 正确做法:使用 SparseArray
SparseArray<String> array = new SparseArray<>();
array.put(1, "value"); // 不会创建临时对象
我的习惯: 只要 key 是整数,一律用 SparseArray 替代 HashMap。不仅减少 GC,内存占用也更低。

11.2.3 控制循环中的对象创建

这个太常见了。很多人在 for 循环里 new 对象,觉得没什么。但循环 100 次就是 100 个对象,如果这个循环每秒执行一次,那就是每秒 100 个对象。

// ❌ 错误做法:循环内创建对象
for (int i = 0; i < list.size(); i++) {
    StringBuilder sb = new StringBuilder();
    sb.append("frame_").append(i);
    // 处理
}

// ✅ 正确做法:循环外创建,循环内复用
StringBuilder sb = new StringBuilder();
for (int i = 0; i < list.size(); i++) {
    sb.setLength(0);
    sb.append("frame_").append(i);
    // 处理
}

11.3 对象池复用:Request 和 Callback

说到对象池,很多人第一反应是「太复杂了,用不上」。其实不然。相机启动场景里,Request 和 Callback 是最适合用对象池的。它们的生命周期短、创建频率高、复用逻辑清晰。

11.3.1 Request 对象池

相机启动时,我们需要创建各种 Request:预览 Request、拍照 Request、对焦 Request。这些对象结构相似,只是参数不同。

public class RequestPool {
    private static final int POOL_SIZE = 10;
    private final Queue<CaptureRequest> mPool = new LinkedList<>();

    public RequestPool() {
        for (int i = 0; i < POOL_SIZE; i++) {
            mPool.offer(new CaptureRequest());
        }
    }

    public CaptureRequest acquire() {
        CaptureRequest request = mPool.poll();
        if (request == null) {
            request = new CaptureRequest(); // 池子空了就创建新的
        }
        return request;
    }

    public void release(CaptureRequest request) {
        request.reset(); // 重置状态
        if (mPool.size() < POOL_SIZE) {
            mPool.offer(request);
        }
    }
}

使用的时候很简单:

RequestPool pool = new RequestPool();
CaptureRequest request = pool.acquire();
request.setType(CaptureRequest.TYPE_PREVIEW);
// ... 使用 request
pool.release(request);
注意: 对象池不是万能的。如果对象创建成本很低,或者复用逻辑复杂,反而得不偿失。我一般只在对象创建频率 > 100次/秒 时才考虑用对象池。

11.3.2 Callback 复用

Callback 的复用比 Request 更隐蔽。很多人习惯用匿名内部类,觉得方便。但每次 new 一个匿名类,都会生成一个新的 Class 文件,占用方法区内存。

// ❌ 错误做法:每次设置都 new
camera.setCaptureCallback(new CaptureCallback() {
    @Override
    public void onCaptureCompleted(CaptureResult result) {
        // 处理
    }
});

// ✅ 正确做法:复用同一个 Callback 实例
private CaptureCallback mCaptureCallback = new CaptureCallback() {
    @Override
    public void onCaptureCompleted(CaptureResult result) {
        // 处理
    }
};
camera.setCaptureCallback(mCaptureCallback);

嗯,这里要注意一点。如果 Callback 内部需要区分不同的请求来源,可以用 tag 或者 requestId 来标识,而不是创建多个 Callback 实例。

11.4 实战:用对象池优化相机启动

我去年优化一个相机项目时,启动时间从 1.8 秒降到了 1.2 秒。其中 0.3 秒是对象池的功劳。具体做法是这样的:

  1. 分析热点:用 Memory Profiler 抓取启动过程,发现每秒创建 200+ 个 CaptureRequest 对象
  2. 引入对象池:为 CaptureRequest 和 CaptureCallback 分别创建对象池
  3. 优化数据传递:把 ByteBuffer 的分配从每次 4MB 改为复用 4MB 的缓冲区
  4. 验证效果:GC 频率从 30次/秒 降到 5次/秒,启动时间减少 300ms

下面这张图展示了优化前后的内存变化:

对象池优化前后内存抖动对比 优化前 GC 频繁触发,内存波动大 优化后 内存平稳,GC 次数大幅减少 关键指标对比 GC 触发频率 30 次/秒 5 次/秒 对象创建数 200+ 个/秒 20 个/秒 启动耗时 1.8 秒 1.2 秒 优化后启动时间减少 33%,GC 频率降低 83%

11.5 避坑指南

做内存优化这么多年,我踩过不少坑。分享几个典型的:

我曾经犯过的错: 对象池里的对象忘记重置状态。结果复用时,上一个请求的参数还留在里面,导致拍照参数错乱。后来我强制在 release 方法里调用 reset(),才彻底解决。

还有一次,我把对象池的容量设得太大,结果内存占用反而增加了。对象池不是越大越好,要根据实际并发量来定。我一般按峰值并发量的 1.5 倍来设置池大小。

另外,不要对所有的对象都用对象池。只有那些创建成本高、复用频率高的对象才值得。比如简单的 String 拼接,用 StringBuilder 复用就够了,没必要上对象池。

11.6 总结

内存抖动是相机启动优化的硬骨头。说白了,就是要在高频调用的地方管住自己 new 对象的手。对象池是个好工具,但要用对地方。

我个人习惯在项目初期就引入对象池框架,而不是等到性能问题爆发了再补。因为一旦代码写顺手了,到处都是 new,改起来成本就高了。

最后送你一句话:少创建,多复用,GC 自然就少了


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