本文主要结合实际项目讲解内存优化方面的内容。
性能优化
体系建立
- 不管是C、Java、JavaScript、Kotlin等其他任何语言,当你开发出一个应用程序的时候,都会有可能要进行性能优化的情况,而一般市面上听到的都是从应用程序的知识层面来讲的,这往往导致我们看问题的局限性。如果想要在遇到性能问题的时候,从容有节奏的深入优化应用程序的性能问题,那么我们的知识体系需要从硬件,操作系统底层开始了解和构建。
操作系统和硬件
- 从计算机早期开始,应用程序和操作系统操作的都是物理内存,【应用程序调用操作系统的API,实际上多个应用程序都可能调用操作系统API,这部分物理内存都是共享的】。这里会衍生出新的问题?多个应用程序访问物理内存的时候,可能会相互覆盖,导致异常。Why?因为两个应用程序直接操作物理内存的时候,没有什么中间层去控制管理他们了。
- 为了解决上面物理内存被随意修改的问题,后面出了虚拟内存的路子,从上面说的原因你也可以猜到,这其实是个中间层,也就是说,程序操作的是虚拟内存,虚拟内存映射物理内存,比如两个程序运行时申请两块不同的虚拟内存,这样就达到了隔离的目的。
内存描述
- 使用adb命令获取当前进程占用的内存情况,会看核心指标,会用工具。
结合项目讲解
使用中内存增长设备挂掉
使用场景
- 超高频人脸识别业务,涉及解析身份证图片和人脸特征值提取等。
性能监测
- 使用AYA工具监测内存,CPU,FPS等指标,这个工具本质是使用adb命令包装做的GUI的工具,但是功能很全,很方便。
定位问题
- 现场反馈问题之后,用监测工具定位到内存缓慢增长,其他指标正常,首先考虑到使用场景,因为频率很高,然后从这块业务切入,可以从审查代码+增加日志+注释代码,通过减少变量方法排查;另外是通过工具去排查,集成LeakCanry观察;
- 从审查代码发现,发现一些问题,存在bitmap使用后没有回收的情况。
- 但是加了回收的代码之后,实际使用还是会不断增长,然后看到保存图片中使用人脸识别sdk方法去获取图片转base64字符串,这里的话使用之后没有销毁,另外使用人脸识别sdk方法获取图像数据还有其他问题,看下个问题会展开来讲。
日志观察到GC频繁
自从上面的问题之后,观察日志就不在于看应用日志,会看所有的日志,帮助我们更好提升性能,然后通过频繁测试刷身份证场景中,观测到日志中隔几秒就有gc相关的日志,有并发gc的日志。
就产生了好奇,到底是什么产生了这个gc?
- 通过上面场景和jvm gc的机制里,看到一定是是new了大量的对象到堆里了。而通过场景猜测,人脸识别相关的对象有很大概率了。另外,通过仔细观察日志发现,gc产生的时候,往往是一次识别结束,这也可以佐证上面的猜测了。
- 但是问题来了,我们如何找到源头呢?这个时候,AndroidStudio中的软件Android Profiler工具就派上用场了。
- 用Android Profiler工具分析内存占用情况,可以看大内存占用类型分布情况,有些内存可以看到产生该内存的方法堆栈信息,那就更方便了。
- 从上图中可以看到产生byte数据类型占用最大,并且它们的大小很多都是一样的,由此可以分析是摄像头原始数据,而这个原始数据没有复用之前内存,而是不断产生。
- 这个时候正好想起之前看到的开源相机项目,里面从SurfaceView,TextureView,以及CameraAPI1,CameraApi2使用讲解等,看了这个项目使用CameraApi1中发现,取摄像头数据的方法是使用了setPreviewCallbackWithBuffer而不是setPreviewCallback。
- 上面两个方法点进去可以看下方法说明,也可以使用deepseek等大模型分析下上面的两个方法。
- 下面内容节选自deepseek的回答,讲的挺细的,简单来说的话setPreviewCallback没有复用内存,setPreviewCallbackWithBuffer则复用内存,不会创建大量字节数组内存。
在 Android 相机框架中,setPreviewCallback
和 setPreviewCallbackWithBuffer
都用于接收相机预览数据,但它们在内存管理机制和性能上有本质区别:
1. setPreviewCallback
- 工作原理:
系统为每一帧预览数据动态分配新内存,并将数据通过onPreviewFrame(byte[] data, Camera camera)
回调返回。 - 内存行为:
- 每次回调都创建新的
byte[]
数组。 - 旧数组依赖 GC 回收,频繁分配/释放易引发内存抖动。
- 每次回调都创建新的
- 优点:使用简单,无需额外配置。
- 缺点:
- 高频帧率下内存分配开销大,可能导致卡顿或丢帧。
- 不适合连续实时处理(如视频分析)。
2. setPreviewCallbackWithBuffer
- 工作原理:
开发者预先分配固定缓冲区池,相机复用这些缓冲区传递数据。 - 使用步骤:
- 计算帧大小:
1
2
3int width = camera.getParameters().getPreviewSize().width;
int height = camera.getParameters().getPreviewSize().height;
int bufferSize = width * height * ImageFormat.getBitsPerPixel(ImageFormat.NV21) / 8; - 初始化缓冲区池(通常 2-3 个):
1
2
3
4for (int i = 0; i < 3; i++) {
byte[] buffer = new byte[bufferSize];
camera.addCallbackBuffer(buffer); // 注册缓冲区
} - 设置回调:
1
camera.setPreviewCallbackWithBuffer(this);
- 在回调中归还缓冲区:
1
2
3
4
5
public void onPreviewFrame(byte[] data, Camera camera) {
// 处理 data...
camera.addCallbackBuffer(data); // 关键:归还缓冲区复用!
}
- 计算帧大小:
- 内存行为:
- 缓冲区重复利用,避免频繁内存分配。
- 显著减少 GC 压力。
- 优点:
- 内存开销稳定,适合高帧率或长时间预览。
- 减少性能抖动,提升实时性。
- 缺点:
- 需手动管理缓冲区(注册、归还)。
- 逻辑稍复杂。
关键区别总结
特性 | setPreviewCallback |
setPreviewCallbackWithBuffer |
---|---|---|
内存分配 | 系统自动分配(每帧新内存) | 开发者预分配缓冲区池 |
内存开销 | 高(频繁 GC) | 低(缓冲区复用) |
性能 | 低帧率可用,高频易卡顿 | 高帧率稳定,适合实时处理 |
使用复杂度 | 简单(一行设置) | 中等(需管理缓冲区) |
适用场景 | 低频抓帧(如拍照瞬间) | 视频流处理、AR、实时识别 |
选择建议
- 若需逐帧处理预览数据(如二维码扫描、人脸识别),务必使用
setPreviewCallbackWithBuffer
避免性能瓶颈。 - 仅在极低频或测试场景中使用
setPreviewCallback
。
注意:现代 Android 开发推荐使用 CameraX 或 Camera2 API 替代已废弃的
Camera
类。例如 CameraX 的ImageAnalysis
抽象了底层优化,无需手动管理缓冲区。
总结
- 总的来说,对于面向高频场景的性能优化往往需要分析场景中产生内存的情况,包括字符串拼接打印,图片等大对象。
- 对于性能问题来说,知识体系升级可以帮助我们更全面的分析问题,游刃有余,