代码级优化技巧:避免不必要的内存拷贝、使用对象池减少GC、数据结构选择(ArrayMap vs HashMap)、锁优化(读写锁、CAS)
各位同学,今天我们来聊聊Camera HAL开发中那些“看不见”的性能杀手。说实话,我见过太多团队把精力花在算法调优上,结果却被内存拷贝和GC拖垮了帧率。嗯,咱们直接切入正题。
1. 避免不必要的内存拷贝
在Camera HAL里,每一帧数据都是真金白银。我早期做过一个项目,预览帧率死活上不去,最后发现是每次回调都深拷贝了一份NV12数据。你想想看,30fps下每秒多拷贝30次,带宽和CPU都扛不住。
核心原则:能用引用就别用拷贝,能用共享内存就别用堆内存。
反面教材:
// 每次回调都拷贝一份,GC频繁触发
byte[] frameData = Arrays.copyOf(rawFrame, rawFrame.length);
processFrame(frameData);
优化方案:
// 复用缓冲区,避免拷贝
ByteBuffer buffer = ByteBuffer.allocateDirect(frameSize);
// 在回调中直接使用buffer,不要复制
processFrame(buffer);
我个人习惯用DirectByteBuffer,它在native层直接操作,省去了JNI拷贝的开销。
还有一个常见坑:用String拼接日志。我曾经在线上环境发现,每次拍照都拼接一个超长的debug字符串,结果GC次数暴涨。后来改成条件日志,只在debug模式下才拼接。
避坑指南:我曾经在某个项目中,因为频繁调用new String(bytes)导致内存抖动。后来改用new String(bytes, 0, length)指定长度,减少了临时对象创建。
2. 使用对象池减少GC
GC是Android性能的隐形杀手。在Camera HAL里,每一帧都可能创建Metadata、Rect、Matrix等小对象。频繁GC会导致丢帧,甚至ANR。
对象池的原理:说白了就是复用对象,避免频繁创建和回收。我常用的实现方式有两种:
- ThreadLocal对象池:适合单线程场景,比如预览线程
- 同步对象池:适合多线程场景,比如拍照和预览并行
一个简单的对象池实现:
public class FrameMetadataPool {
private final LinkedList<FrameMetadata> pool = new LinkedList<>();
private static final int MAX_POOL_SIZE = 30;
public FrameMetadata acquire() {
synchronized (pool) {
if (!pool.isEmpty()) {
return pool.removeFirst();
}
}
return new FrameMetadata();
}
public void release(FrameMetadata metadata) {
synchronized (pool) {
if (pool.size() < MAX_POOL_SIZE) {
metadata.reset(); // 重置状态
pool.addLast(metadata);
}
}
}
}
你可能会问:池子设多大合适?我一般按帧率的1.5倍来算。比如30fps,池子设45个左右。太小了不够用,太大了浪费内存。
个人经验:我在做HDR多帧合成时,每个帧都需要一个Metadata对象。用对象池后,GC次数从每秒20次降到了几乎为0。效果立竿见影。
3. 数据结构选择:ArrayMap vs HashMap
这个选择看似简单,但坑不少。我见过有人用HashMap存Camera特性列表,结果每次遍历都卡顿。为什么?因为HashMap的Entry对象太多了。
| 特性 | ArrayMap | HashMap |
|---|---|---|
| 内存占用 | 低(无额外Entry对象) | 高(每个键值对都有Entry) |
| 查找速度 | O(log n) 二分查找 | O(1) 哈希查找 |
| 插入/删除 | O(n) 数组移动 | O(1) 平均 |
| 适用场景 | 数据量小(<1000)、频繁遍历 | 数据量大、频繁增删 |
我的建议:在Camera HAL里,大部分配置数据量都在几十到几百条。比如AE/AWB策略、Sensor模式列表。这时候用ArrayMap更合适,内存省一半,遍历还快。
注意:如果你需要频繁插入和删除,比如动态更新Camera特性,那还是用HashMap吧。ArrayMap的数组移动开销不小。
举个例子,我在做Camera特性查询时,用ArrayMap存储所有支持的场景模式:
ArrayMap<Integer, String> sceneModes = new ArrayMap<>();
sceneModes.put(CameraMetadata.CONTROL_SCENE_MODE_PORTRAIT, "人像");
sceneModes.put(CameraMetadata.CONTROL_SCENE_MODE_LANDSCAPE, "风景");
// 遍历时性能优于HashMap
for (int i = 0; i < sceneModes.size(); i++) {
int key = sceneModes.keyAt(i);
String value = sceneModes.valueAt(i);
}
4. 锁优化:读写锁与CAS
锁是并发编程的必需品,但用不好就是性能毒药。在Camera HAL里,多个线程同时访问共享数据是常态。比如预览线程和拍照线程同时读写参数。
读写锁(ReentrantReadWriteLock):适合读多写少的场景。我做过一个统计,在Camera HAL里,读操作占比超过90%。这时候用读写锁,读线程之间不互斥,性能提升明显。
读写锁示例:
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
// 读操作:多个线程可以同时读
public CameraMetadata getMetadata() {
readLock.lock();
try {
return currentMetadata;
} finally {
readLock.unlock();
}
}
// 写操作:独占
public void updateMetadata(CameraMetadata metadata) {
writeLock.lock();
try {
currentMetadata = metadata;
} finally {
writeLock.unlock();
}
}
CAS(Compare And Swap):无锁编程的利器。适合简单的状态更新,比如计数器、标志位。我常用AtomicInteger或AtomicBoolean来实现。
个人经验:我曾经用synchronized保护一个状态标志,结果在高帧率下性能瓶颈明显。换成AtomicBoolean后,性能提升了30%。
private final AtomicBoolean isCapturing = new AtomicBoolean(false);
public boolean tryStartCapture() {
// CAS操作,无锁
return isCapturing.compareAndSet(false, true);
}
public void stopCapture() {
isCapturing.set(false);
}
避坑指南:CAS不是万能的。如果更新逻辑复杂,或者需要保护多个变量的一致性,还是老老实实用锁吧。我曾经在某个项目中过度使用CAS,结果出现了ABA问题,排查了两天才找到原因。
知识体系总览
下面这张图总结了本章的核心知识点和它们之间的关系:
好了,这一章的内容就到这里。代码级优化看似琐碎,但积少成多,效果惊人。下次你在调试Camera HAL时,不妨先看看这些基础环节有没有优化空间。