11. 内存优化:启动时内存抖动分析、减少GC触发、对象池复用
相机启动优化做到后面,你会发现一个很有意思的现象——明明代码逻辑都梳理清楚了,主线程也没卡顿,但启动就是慢那么几百毫秒。我遇到过好几次这种情况,最后抓了 Memory Profiler 一看,好家伙,内存像心电图一样疯狂抖动。
内存抖动,说白了就是短时间内频繁创建和销毁对象。每次创建对象都要分配内存,每次 GC 都要暂停线程。相机启动这个场景特别容易触发抖动,因为一帧帧的图像数据、一次次回调、一个个 Request 对象,全都在高频创建。
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 使用基本类型替代包装类型
这个坑我踩过。有一次排查内存抖动,发现 Integer 和 Float 的创建量特别大。仔细一看,代码里大量使用了 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"); // 不会创建临时对象
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);
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 秒是对象池的功劳。具体做法是这样的:
- 分析热点:用 Memory Profiler 抓取启动过程,发现每秒创建 200+ 个 CaptureRequest 对象
- 引入对象池:为 CaptureRequest 和 CaptureCallback 分别创建对象池
- 优化数据传递:把 ByteBuffer 的分配从每次 4MB 改为复用 4MB 的缓冲区
- 验证效果:GC 频率从 30次/秒 降到 5次/秒,启动时间减少 300ms
下面这张图展示了优化前后的内存变化:
11.5 避坑指南
做内存优化这么多年,我踩过不少坑。分享几个典型的:
还有一次,我把对象池的容量设得太大,结果内存占用反而增加了。对象池不是越大越好,要根据实际并发量来定。我一般按峰值并发量的 1.5 倍来设置池大小。
另外,不要对所有的对象都用对象池。只有那些创建成本高、复用频率高的对象才值得。比如简单的 String 拼接,用 StringBuilder 复用就够了,没必要上对象池。
11.6 总结
内存抖动是相机启动优化的硬骨头。说白了,就是要在高频调用的地方管住自己 new 对象的手。对象池是个好工具,但要用对地方。
我个人习惯在项目初期就引入对象池框架,而不是等到性能问题爆发了再补。因为一旦代码写顺手了,到处都是 new,改起来成本就高了。
最后送你一句话:少创建,多复用,GC 自然就少了。
公众号:蓝海资料掘金营,微信deep3321