Android性能项目实践

本文主要结合实际项目讲解内存优化方面的内容。

性能优化

体系建立

  • 不管是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等大模型分析下上面的两个方法。

setPreviewCallbackWithBuffer
setPreviewCallback

  • 下面内容节选自deepseek的回答,讲的挺细的,简单来说的话setPreviewCallback没有复用内存,setPreviewCallbackWithBuffer则复用内存,不会创建大量字节数组内存。

在 Android 相机框架中,setPreviewCallbacksetPreviewCallbackWithBuffer 都用于接收相机预览数据,但它们在内存管理机制和性能上有本质区别:


1. setPreviewCallback

  • 工作原理
    系统为每一帧预览数据动态分配新内存,并将数据通过 onPreviewFrame(byte[] data, Camera camera) 回调返回。
  • 内存行为
    • 每次回调都创建新的 byte[] 数组。
    • 旧数组依赖 GC 回收,频繁分配/释放易引发内存抖动。
  • 优点:使用简单,无需额外配置。
  • 缺点
    • 高频帧率下内存分配开销大,可能导致卡顿或丢帧。
    • 不适合连续实时处理(如视频分析)。

2. setPreviewCallbackWithBuffer

  • 工作原理
    开发者预先分配固定缓冲区池,相机复用这些缓冲区传递数据。
  • 使用步骤
    1. 计算帧大小
      1
      2
      3
      int width = camera.getParameters().getPreviewSize().width;
      int height = camera.getParameters().getPreviewSize().height;
      int bufferSize = width * height * ImageFormat.getBitsPerPixel(ImageFormat.NV21) / 8;
    2. 初始化缓冲区池(通常 2-3 个):
      1
      2
      3
      4
      for (int i = 0; i < 3; i++) {
      byte[] buffer = new byte[bufferSize];
      camera.addCallbackBuffer(buffer); // 注册缓冲区
      }
    3. 设置回调
      1
      camera.setPreviewCallbackWithBuffer(this);
    4. 在回调中归还缓冲区
      1
      2
      3
      4
      5
      @Override
      public void onPreviewFrame(byte[] data, Camera camera) {
      // 处理 data...
      camera.addCallbackBuffer(data); // 关键:归还缓冲区复用!
      }
  • 内存行为
    • 缓冲区重复利用,避免频繁内存分配。
    • 显著减少 GC 压力。
  • 优点
    • 内存开销稳定,适合高帧率或长时间预览。
    • 减少性能抖动,提升实时性。
  • 缺点
    • 需手动管理缓冲区(注册、归还)。
    • 逻辑稍复杂。

关键区别总结

特性 setPreviewCallback setPreviewCallbackWithBuffer
内存分配 系统自动分配(每帧新内存) 开发者预分配缓冲区池
内存开销 高(频繁 GC) 低(缓冲区复用)
性能 低帧率可用,高频易卡顿 高帧率稳定,适合实时处理
使用复杂度 简单(一行设置) 中等(需管理缓冲区)
适用场景 低频抓帧(如拍照瞬间) 视频流处理、AR、实时识别

选择建议

  • 若需逐帧处理预览数据(如二维码扫描、人脸识别),务必使用 setPreviewCallbackWithBuffer 避免性能瓶颈。
  • 仅在极低频或测试场景中使用 setPreviewCallback

注意:现代 Android 开发推荐使用 CameraXCamera2 API 替代已废弃的 Camera 类。例如 CameraX 的 ImageAnalysis 抽象了底层优化,无需手动管理缓冲区。

总结

  • 总的来说,对于面向高频场景的性能优化往往需要分析场景中产生内存的情况,包括字符串拼接打印,图片等大对象。
  • 对于性能问题来说,知识体系升级可以帮助我们更全面的分析问题,游刃有余,

参考

Android性能优化

Author: golike
Link: http://jianhui1012.github.io/2025/06/09/Android性能项目实践/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.