24. 相机性能优化:预览帧率优化、内存管理、Bitmap复用、SurfaceView vs TextureView选择
做Android相机开发,性能优化是个绕不开的坎。我刚开始做这个方向时,总觉得能打开相机、看到画面就完事了。直到有一次,我在一个低端机上跑预览,画面卡得像幻灯片,用户反馈说“这App是不是死机了?”——嗯,从那以后,我再也不敢忽视性能优化了。
说白了,相机性能优化就四个核心方向:预览帧率、内存管理、Bitmap复用、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因为“内存”崩溃。做到这两点,你就是合格的相机开发工程师。