代码级优化技巧:避免不必要的内存拷贝、使用对象池减少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问题,排查了两天才找到原因。

知识体系总览

下面这张图总结了本章的核心知识点和它们之间的关系:

代码级优化技巧 避免内存拷贝 • 使用DirectByteBuffer • 避免String拼接 • 复用缓冲区 对象池减少GC • ThreadLocal池 • 同步池 • 池大小 = 帧率×1.5 数据结构选择 ArrayMap vs HashMap 数据量<1000用ArrayMap 锁优化 读写锁 / CAS 读多写少用读写锁

好了,这一章的内容就到这里。代码级优化看似琐碎,但积少成多,效果惊人。下次你在调试Camera HAL时,不妨先看看这些基础环节有没有优化空间。

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