8. 内存与带宽优化:减少内存拷贝、使用ImageReader高效获取帧、降低DDR带宽

内存和带宽,说白了就是相机系统的「血管」和「血液」。

我见过太多项目,算法跑得飞快,但一上相机预览,整机功耗直接飙到 2W+。查到最后,问题往往出在内存拷贝上——数据在 CPU、GPU、DSP 之间来回倒腾,DDR 带宽被吃光,CPU 空转等内存。嗯,今天我们就来聊聊怎么把这些「无效搬运」砍掉。

核心思路: 数据流路径上,每减少一次内存拷贝,就能省下约 0.5~1.5 mW 的 DDR 功耗(取决于分辨率)。别小看这点,叠加起来就是几百毫瓦的差距。

8.1 减少内存拷贝:从源头掐断「无效搬运」

我个人习惯,拿到一个相机 HAL 或 Framework 层的代码,第一件事就是 grep 搜索 memcpycopyFrom。为什么?因为很多性能问题都是「拷贝太多」引起的。

8.1.1 使用共享内存(SharedMemory / ION)

Android 从 8.0 开始推荐使用 SharedMemory 替代传统的 Binder 传输大块数据。Binder 传输超过 1MB 的数据时,内核会做隐式拷贝,而 SharedMemory 本质上是 mmap,零拷贝。

// 推荐:使用 SharedMemory 传递帧数据
val sharedMemory = SharedMemory.create("frame_data", bufferSize)
val mappedBuffer = sharedMemory.map(READ_WRITE)
// ... 填充数据 ...
sharedMemory.sendTo(remoteReceiver)

// 不推荐:直接通过 Binder 传 byte[]
// val data = ByteArray(bufferSize)
// parcel.writeByteArray(data)  // 这里会触发 memcpy

我在项目中遇到过,某厂商的 Camera HAL 用 Binder 传 1080p YUV 帧,每帧 3MB,30fps 下每秒 90MB 的拷贝量。换成 SharedMemory 后,CPU 占用直接降了 12%。

8.1.2 使用 Surface 传递而非 ByteBuffer

如果你在 App 层做相机开发,尽量用 SurfaceImageReader 获取帧,而不是 Camera.open() 配合 onPreviewFrame 回调。后者每次回调都会产生一次 Java Heap 到 Native Heap 的拷贝。

避坑指南: 我曾经接手过一个项目,App 层用 onPreviewFrame 获取 4K 帧做人脸检测,每帧 8MB,30fps 下每秒 240MB 的拷贝量。换成 ImageReader + GPU 直接处理,功耗从 3.2W 降到 2.1W。

8.2 使用 ImageReader 高效获取帧

ImageReader 是 Android 官方推荐的帧获取方式。它底层使用 SurfaceTextureSurface 的 BufferQueue 机制,数据直接从 Camera HAL 流向消费者,中间不经过 App 进程的 Java Heap。

8.2.1 正确配置 ImageReader

// 推荐配置
val imageReader = ImageReader.newInstance(
    width, height,
    ImageFormat.YUV_420_888,  // 使用 YUV 而非 JPEG,避免编解码开销
    2,                         // maxImages = 2,不要设太大
    HardwareBuffer.USAGE_GPU_SAMPLER_IMAGE // 允许 GPU 直接访问
)

// 设置回调时注意:不要在回调里做耗时操作
imageReader.setOnImageAvailableListener({ reader ->
    val image = reader.acquireLatestImage()  // 用 acquireLatestImage 而非 acquireNextImage
    if (image != null) {
        // 处理 image
        image.close()  // 必须及时关闭,否则 buffer 泄漏
    }
}, backgroundHandler)

为什么 maxImages 设 2 就够了?设大了反而会增加内存占用和 DDR 带宽。你想想看,Camera HAL 每帧都要往 BufferQueue 里塞数据,队列越长,数据在内存里停留的时间就越久,功耗自然就上去了。

8.2.2 避免 Image 到 Bitmap 的转换

很多开发者习惯把 Image 转成 Bitmap 再处理。但 Bitmap 在 Java Heap 上,Image 在 Native Heap 上,转换一次就是一次完整的 memcpy。如果你要做图像处理,直接用 Image.getPlanes() 获取 YUV 数据,或者传给 GPU 做纹理。

注意: ImageReader 的 acquireLatestImage() 会丢弃旧帧,只返回最新的一帧。如果你的算法需要每一帧(比如录像),请用 acquireNextImage(),但记得控制帧率,别让队列堆积。

8.3 降低 DDR 带宽:从「搬运工」变成「精算师」

DDR 带宽是功耗的「隐形杀手」。我见过一个案例,某手机拍 4K 视频时,DDR 带宽跑到 12GB/s,整机功耗 4.5W。优化后降到 6GB/s,功耗 2.8W。怎么做到的?说白了就是减少不必要的数据搬运。

8.3.1 使用更小的分辨率做预览

预览分辨率不一定要和拍照分辨率一样。很多 App 预览用 1080p,拍照用 4K,但 Camera HAL 内部还是以 4K 跑全链路。我建议预览用 720p 或 1080p,拍照时再切到 4K。这样预览阶段的 DDR 带宽能省 60% 以上。

预览分辨率 每帧数据量 (YUV420) 30fps 带宽需求 相对 4K 节省
4K (3840x2160) ~12 MB ~360 MB/s 基准
1080p (1920x1080) ~3 MB ~90 MB/s 节省 75%
720p (1280x720) ~1.4 MB ~42 MB/s 节省 88%

8.3.2 利用硬件缩放和裁剪

不要用 CPU 做缩放和裁剪。Camera HAL 和 ISP 通常内置了硬件缩放模块,几乎不耗电。你只需要在配置 CaptureRequest 时设置 SCALER_CROP_REGION 即可。

// 使用 ISP 硬件缩放,零 CPU 开销
val cropRegion = Rect(0, 0, 1920, 1080)  // 输出 1080p
captureRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, cropRegion)

我曾经见过一个团队,在 App 层用 Bitmap.createScaledBitmap 做缩放,每次缩放都要把数据从 GPU 拉到 CPU,缩放完再传回 GPU。嗯,这简直是「用卡车运一包薯片」——完全没必要。

8.3.3 使用更少的数据格式

YUV420 是相机最常用的格式,但如果你不需要颜色信息(比如做深度检测、人脸检测),可以用 ImageFormat.NV21 或直接取 Y 平面。Y 平面只有 1/3 的数据量,带宽直接降 66%。

小技巧: 如果算法只需要灰度图,直接从 YUV 的 Y 平面取数据,不要转成 RGB 再取灰度。我做过测试,Y 平面直接处理比 RGB 转换后处理,功耗低 40%。

8.4 知识体系总览

下面这张图总结了内存与带宽优化的核心路径。你可以把它当作一个检查清单,每次做相机功耗优化时,对照着看一遍。

内存与带宽优化核心路径 Camera HAL / ISP 数据流路径选择 SharedMemory / Surface / ImageReader vs Binder / ByteBuffer / onPreviewFrame 减少内存拷贝 共享内存 · 零拷贝 · Surface传递 ImageReader 高效获取 acquireLatestImage · 及时close 降低 DDR 带宽 低分辨率预览 · 硬件缩放 · Y平面 ✅ 功耗降低 30%~50% · DDR 带宽减半 · CPU 占用下降 每个环节减少一次拷贝,整机功耗可降低 0.5~1.5 mW

8.5 实战检查清单

最后,我整理了一份检查清单。每次做相机功耗优化时,对着过一遍,基本不会漏掉关键点。

  1. 数据流路径:是否用了 SharedMemory / Surface / ImageReader?有没有 Binder 传大数据的「雷」?
  2. 拷贝次数:从 HAL 到 App,数据经过了几次 memcpy?能不能减少到 1 次甚至 0 次?
  3. ImageReader 配置:maxImages 是否合理?acquireLatestImage 还是 acquireNextImage?close 是否及时?
  4. 分辨率与格式:预览分辨率是否过高?能否用 Y 平面替代完整 YUV?
  5. 硬件加速:缩放、裁剪、旋转是否用了 ISP/GPU 硬件?有没有用 CPU 做这些操作?
  6. 带宽监控:用 dumpsys meminfoperfetto 查看 DDR 带宽,确认优化效果。

一句话总结: 内存优化的本质是「能不搬就不搬,非要搬就少搬,搬的时候走高速」。你想想看,数据在内存里每多待一毫秒,DDR 就多耗一毫秒的电。砍掉无效拷贝,就是最直接的降功耗手段。


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