7、Memory优化:Camera应用内存占用分析、Bitmap内存管理、YUV数据内存复用、内存泄漏检测与修复

说到Camera应用的性能优化,内存这块儿绝对是绕不开的硬骨头。我见过太多应用,功能做得很花哨,结果一打开相机,内存直接飙到几百兆,然后就被系统给杀了。说白了,Camera本身就是个内存大户,预览流、拍照帧、算法处理,哪个环节都在吃内存。如果不精心管理,内存泄漏和OOM就是家常便饭。

这一章,我就把我在Camera内存优化上踩过的坑、总结的经验,掰开了揉碎了讲给你听。

7.1 Camera应用的内存占用分析

我们先搞清楚一件事:Camera应用的内存到底花在哪儿了?

我个人习惯把Camera的内存占用分成三大块:

  • 图像数据缓冲区:这是大头。预览帧、拍照帧、YUV数据,每一帧都是几兆甚至十几兆。1080p的YUV420数据,一帧就要3MB左右。如果同时缓存几帧,内存就上去了。
  • Bitmap对象:拍照后生成缩略图、显示预览、做滤镜处理,都会创建Bitmap。一张1200万像素的照片,解码成ARGB_8888的Bitmap,内存占用轻松超过48MB。
  • 算法与中间数据:人脸检测、HDR合成、夜景多帧降噪,这些算法都有自己的临时缓冲区。如果管理不当,这些临时内存会一直占用着不释放。

我在项目中遇到过一个问题:某款手机在连续拍照时,内存以每秒20MB的速度增长,拍个十几张就OOM了。后来一查,发现是预览流的SurfaceTexture里积压了未消费的帧,导致GraphicBuffer一直没被回收。你看,问题往往就出在这些细节上。

核心思路:优化内存的第一步,不是写代码,而是先搞清楚内存去哪儿了。用Profiler工具抓一下内存快照,看看Heap dump里哪些对象占得最多,再对症下药。

7.2 Bitmap内存管理

Bitmap是Camera应用里最容易被滥用的对象。我见过有人直接把一张4000x3000的照片加载到内存里,然后显示在一个200x150的ImageView上。这不是浪费,这是犯罪。

Bitmap优化的几个关键点,我列一下:

  • 按需采样:用BitmapFactory.Options的inSampleSize,根据目标显示尺寸做降采样。比如要显示200x150的缩略图,原图4000x3000,采样率设成16就足够了。内存直接从48MB降到200KB左右。
  • 选择合适的色彩格式:ARGB_8888每个像素占4字节,RGB_565只占2字节。如果不需要透明度,用RGB_565能省一半内存。Camera预览的缩略图,我一般用RGB_565。
  • 及时回收:在Android 2.3之前,Bitmap的内存是分配在Native堆上的,需要手动调用recycle()。虽然现在GC会处理,但如果你确定某个Bitmap不再使用,主动recycle()仍然是个好习惯。尤其是在Camera这种高频场景下。
  • 复用Bitmap:用BitmapFactory.Options的inBitmap,可以让解码器复用已有的Bitmap内存,避免频繁分配和回收。我在做连拍功能时,就用这个技巧把内存抖动降到了最低。
// 按需采样示例
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(path, options);

int targetWidth = 200;
int targetHeight = 150;
int sampleSize = Math.max(
    options.outWidth / targetWidth,
    options.outHeight / targetHeight
);
options.inSampleSize = sampleSize;
options.inJustDecodeBounds = false;
options.inPreferredConfig = Bitmap.Config.RGB_565;

Bitmap bitmap = BitmapFactory.decodeFile(path, options);

小技巧:如果你用Glide或Coil这类图片加载库,它们内部已经做了采样和缓存管理。但如果你直接操作Camera的YUV数据转Bitmap,那这些优化就得自己动手了。

7.3 YUV数据内存复用

Camera的预览数据,默认是YUV420格式的。每次onPreviewFrame回调,都会给你一个byte数组。如果你每次都new一个byte[]来接收,那内存分配和GC的压力会非常大。

为什么会这样?你想想看,30fps的预览,每秒30帧,每帧3MB,如果每次分配新内存,一秒就是90MB的分配量。GC根本来不及回收,内存抖动就来了。

我的做法是:复用缓冲区

  • 在Camera1中,用addCallbackBuffer()把预先分配好的byte数组注册进去。系统会循环使用这些缓冲区,而不是每次都创建新的。
  • 在Camera2中,用ImageReader的setOnImageAvailableListener,配合Image的getPlanes()获取数据。Image对象本身是可以复用的,只要你不手动close()它。
// Camera1 缓冲区复用
byte[] buffer = new byte[previewWidth * previewHeight * 3 / 2];
camera.addCallbackBuffer(buffer);

Camera.PreviewCallback callback = new Camera.PreviewCallback() {
    @Override
    public void onPreviewFrame(byte[] data, Camera camera) {
        // 处理data...
        // 处理完后,把缓冲区还回去
        camera.addCallbackBuffer(data);
    }
};

我记得有一次,一个同事没做缓冲区复用,结果在低端机上预览卡顿得不行。我帮他加上addCallbackBuffer之后,帧率直接从15fps回到了30fps。嗯,有时候优化就是这么简单,但你不做就是不行。

注意:YUV数据转Bitmap时,尽量避免每次都创建新的Bitmap。可以用inBitmap复用,或者直接用RenderScript/OpenGL做GPU加速转换,减少CPU内存占用。

7.4 内存泄漏检测与修复

内存泄漏是Camera应用的隐形杀手。它不像OOM那么明显,但会慢慢蚕食你的可用内存,最终导致应用被系统杀死。

常见的Camera内存泄漏场景,我总结了几种:

  • Camera对象未释放:Activity销毁时,没有调用camera.release()。Camera会一直占用硬件资源和内存缓冲区。
  • 回调持有Activity引用:Camera.PreviewCallback是匿名内部类,隐式持有外部Activity的引用。如果Activity销毁时没有取消注册,Activity就无法被GC回收。
  • SurfaceView/TextureView未清理:Surface的Callback没有在onPause或onDestroy中移除,导致View持有Camera的引用链。
  • Handler/Thread泄漏:在子线程中处理Camera数据,但线程没有正确退出,导致整个线程栈上的对象都无法释放。

我曾经在一个项目里,发现每次打开相机再退出,内存就涨个十几MB,反复操作几次就OOM了。用LeakCanary一查,发现是CameraHandler这个静态内部类持有了Activity的引用。修复方法很简单:在onDestroy里把Handler的Looper退掉,或者用WeakReference引用Activity。

// 使用WeakReference避免泄漏
private static class CameraHandler extends Handler {
    private final WeakReference<CameraActivity> mActivityRef;

    CameraHandler(CameraActivity activity) {
        mActivityRef = new WeakReference<>(activity);
    }

    @Override
    public void handleMessage(Message msg) {
        CameraActivity activity = mActivityRef.get();
        if (activity == null) return;
        // 处理消息...
    }
}

检测工具推荐:LeakCanary是首选,集成简单,能直接告诉你泄漏的引用链。配合Android Studio的Memory Profiler,可以看Heap dump里哪些对象不该存在。我一般会在CI流程里加上LeakCanary的自动化检测,防止新代码引入泄漏。

修复内存泄漏,说白了就是一件事:确保生命周期结束的对象,不再被任何强引用持有。Camera的release()、Callback的注销、Handler的清理、线程的退出,一个都不能少。

避坑指南:我曾经在Camera2的API里踩过一个坑——ImageReader的Image对象,如果你不调用close(),它内部的缓冲区就不会被回收。每次onImageAvailable回调里,记得在finally块中调用image.close()。这个坑,我至少见过三个同事掉进去过。

7.5 本章知识体系

下面这张图,我把Camera内存优化的核心逻辑梳理了一下。你可以把它当作一个检查清单,做优化时对照着看,基本不会漏掉什么。

Camera内存优化知识体系 Camera内存优化 内存占用分析 图像缓冲区 Bitmap对象 算法中间数据 Bitmap内存管理 按需采样 色彩格式选择 inBitmap复用 YUV数据内存复用 addCallbackBuffer ImageReader复用 GPU加速转换 内存泄漏检测与修复 LeakCanary检测 WeakReference 生命周期管理 核心原则:减少分配、及时释放、复用缓冲区

这张图把Camera内存优化的四个核心方向串起来了。从分析内存占用开始,到Bitmap管理、YUV复用,再到泄漏检测,每一步都环环相扣。你按照这个框架去做优化,基本能把Camera应用的内存问题控制住。


好了,这一章的内容就到这里。内存优化是个细活,急不得。你只要记住:能不分配就不分配,能复用就复用,用完了一定要释放。做到这三点,Camera应用的内存问题就解决了一大半。

如果你在实际项目中遇到什么奇怪的内存问题,欢迎来公众号找我聊聊。有些坑,光看文档是看不出来的,得亲手踩过才知道。

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