24. 相机性能优化:预览帧率优化、内存管理、Bitmap复用、SurfaceView vs TextureView选择

做Android相机开发,性能优化是个绕不开的坎。我刚开始做这个方向时,总觉得能打开相机、看到画面就完事了。直到有一次,我在一个低端机上跑预览,画面卡得像幻灯片,用户反馈说“这App是不是死机了?”——嗯,从那以后,我再也不敢忽视性能优化了。

说白了,相机性能优化就四个核心方向:预览帧率内存管理Bitmap复用SurfaceView与TextureView的选择。今天咱们一个一个聊透。

核心观点:相机性能优化的本质,是在“流畅度”和“内存占用”之间找到平衡点。别盲目追求高帧率,也别为了省内存把画面搞成马赛克。

相机性能优化知识体系 相机性能优化 预览帧率优化 内存管理 Bitmap复用 SurfaceView vs TextureView setPreviewFpsRange 帧回调降频 及时释放 避免泄漏 inBitmap复用 inSampleSize SurfaceView TextureView 目标:流畅预览 + 低内存占用 + 无卡顿

24.1 预览帧率优化

帧率这东西,说白了就是一秒钟能刷多少帧画面。人眼看起来流畅的底线是24fps,但相机预览我建议至少跑到30fps。低于这个数,用户就能感觉到“顿挫感”。

我个人习惯在初始化相机时,先查一下设备支持的帧率范围:

CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId);
Range<Integer>[] fpsRanges = characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES);
// 选一个范围,比如 [30, 30] 表示固定30fps
// 或者 [15, 30] 表示自适应

然后通过 CaptureRequest 设置:

previewRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, new Range<>(30, 30));

这里有个坑——不是所有设备都能稳定跑30fps。我在项目中遇到过一台老款平板,设置30fps后画面反而更卡。后来发现是它的ISP处理能力跟不上。解决办法是降一档,用25fps。

小技巧:如果预览帧率上不去,可以试试降低预览分辨率。比如从1920x1080降到1280x720,帧率往往能提升一大截。你想想看,每帧要处理的数据量少了一半多,能不流畅吗?

另外,ImageReader 的回调频率也要控制。如果你每帧都去回调处理,CPU肯定扛不住。我一般会设置一个跳帧逻辑:

private int frameSkip = 2; // 每3帧处理1帧
private int frameCount = 0;

imageReader.setOnImageAvailableListener(reader -> {
    frameCount++;
    if (frameCount % frameSkip != 0) {
        reader.acquireLatestImage().close(); // 直接丢弃
        return;
    }
    // 处理当前帧
    Image image = reader.acquireLatestImage();
    // ... 业务逻辑
}, backgroundHandler);

24.2 内存管理

相机模块是内存消耗大户。一张1920x1080的图片,如果按ARGB_8888算,占多少内存?1920 * 1080 * 4 = 约8MB。如果连续拍几张,内存就炸了。

我见过最夸张的一次,有个同事在回调里每次都 new 一个 Bitmap,结果OOM崩溃。其实,及时释放是基本原则:

  • Image.close() 释放每一帧
  • Bitmap.recycle() 释放不再用的Bitmap
  • WeakReference 持有大对象,避免强引用泄漏

还有一个容易被忽略的点——CameraManager 的 openCamera 调用。每次打开相机都会分配一些native内存。如果你频繁开关相机,记得在 CameraDevice.close() 之后,把相关对象置为null,方便GC回收。

注意:不要在相机回调中做耗时操作,比如网络请求、数据库写入。这些操作会阻塞主线程,导致预览卡顿。我曾经踩过这个坑,后来把所有耗时操作都扔到子线程里,预览瞬间丝滑了。

24.3 Bitmap复用

Bitmap复用,说白了就是别频繁创建新Bitmap,而是重用已有的内存空间。Android 3.0之后提供了 inBitmap 选项,可以告诉解码器“你用这块内存就行,别重新分配”。

具体做法:

BitmapFactory.Options options = new BitmapFactory.Options();
options.inMutable = true; // 必须设为true
options.inBitmap = reusableBitmap; // 传入之前创建的Bitmap

// 解码时就会复用reusableBitmap的内存
Bitmap bitmap = BitmapFactory.decodeFile(path, options);

这里有个限制——复用的Bitmap尺寸必须大于等于新解码的Bitmap。否则会报错。我习惯维护一个Bitmap池,按尺寸分类存放:

public class BitmapPool {
    private final Map<Integer, Bitmap> pool = new HashMap<>();
    
    public Bitmap get(int width, int height) {
        int key = width * height;
        Bitmap bmp = pool.remove(key);
        if (bmp == null) {
            bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        }
        return bmp;
    }
    
    public void put(Bitmap bmp) {
        int key = bmp.getWidth() * bmp.getHeight();
        pool.put(key, bmp);
    }
}

另外,inSampleSize 也是个好东西。如果你不需要原图分辨率,可以采样缩小:

options.inSampleSize = 2; // 宽高各缩小一半,内存降到1/4

嗯,这里要注意:inSampleSize 必须是2的幂,否则解码器会向下取整。

24.4 SurfaceView vs TextureView选择

这两个东西,很多新手搞不清楚区别。我简单说:

特性 SurfaceView TextureView
渲染方式 独立窗口,独立Surface 与View共享,在GPU中合成
性能 高,延迟低 稍低,有额外合成开销
动画/变换 不支持(需额外处理) 天然支持(平移、缩放、旋转)
截图 不支持直接截图 支持 getBitmap()
适用场景 纯预览、视频录制 需要动画、滤镜、截图

我个人习惯是:能用SurfaceView就用SurfaceView。为什么?因为它性能更好,延迟更低。相机预览这种高频刷新的场景,SurfaceView是首选。

但如果你需要做美颜、滤镜、或者让预览画面跟着手势缩放旋转,那就得用TextureView。它本质上是把相机画面当成一个纹理,交给GPU处理,灵活性高很多。

我的建议:如果只是“打开相机看画面”,选SurfaceView。如果需要“对画面做各种花式操作”,选TextureView。别为了省事全用TextureView,低端机上掉帧掉到你怀疑人生。

我记得有一次,一个项目里用了TextureView做预览,在小米8上跑得挺好,换到红米Note5上就卡成PPT。后来换成SurfaceView,帧率直接从18fps飙到30fps。你想想看,这差距有多大。

最后提一句,SurfaceView的Z轴顺序问题。它默认在所有普通View之上,如果你需要在预览画面上叠加控件,记得用 setZOrderOnTop(false) 或者把SurfaceView放在布局最底层。


好了,相机性能优化这块,核心就是这四个点。帧率要稳,内存要省,Bitmap要复用,视图要选对。把这些做到位了,你的相机App基本就稳了。

一句话总结:别让用户感觉到“卡”,别让App因为“内存”崩溃。做到这两点,你就是合格的相机开发工程师。

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