本文通过一类 Android 机型上相机拍摄过程中的 native 内存 OOM 的问题展开,借助内存快照裁剪回捞和 Native 内存监控工具的赋能,来深入剖析此类问题 。背景Raphael 是西瓜视频 Android 团队开发的一款 native 内存监控工具,在字节跳动内部产品(如西瓜、抖音、头条等)上广泛用于监控 native 内存泄漏问题 。在抖音 7.8.0-8.3.0 上搜集到大量因虚拟内存触顶而 crash 的内存日志现场(如 pthread_create、GL error、EGL_BAD_ALLOC),其中 60%以上都是 camera 相关的内存泄漏,占整体 crash 的 15%以上(JAVA & Native) 。同时也收到 OPPO 等厂商反馈抖音 App 在其新机型上 native crash 比其他机型高了 3 倍以上,分析厂商提供的日志发现基本都是虚拟内存触顶导致的 carsh,这其中 80%以上都有 camera 相关的内存分配失败的日志 。
问题通过对 native 内存监控搜集到的日志进行堆栈聚合和 so 级的内存占用统计,可以发现截止到 OOM 时工具拦截到的 native 内存总量已经达到了 1.3G 左右(32 位下应用可直接使用的 native 内存上限约 2G),这其中占比最大的是 CameraMetaData 对象间接引用的内存,native 内存泄漏十分严重 。
文章插图
由于 native 内存分配的频率过高,获取 Java 层堆栈又比较耗时,在拦截 native 内存分配时并不适合直接频繁抓取 Java 堆栈 。Native 内存不同于 Java 内存,单从拦截到的数据很难直观给出结论 。通常对于内存等资源不合理使用导致的资源不足而引发的问题都很难归因,从拦截到的数据来看,CameraMetaData 所引用的内存最大,嫌疑也最大,基于此决定剖析一下这个问题
初步分析分析 native 内存的分配和释放
通过拦截到的堆栈可以看出,CameraMetaData 的创建堆栈的上层是 Java 调用,最终在 native 层进行的内存分配(boot-framework.oat & libandroid_runtime.so) 。CameraMetaData 对象有两部分内存,对象本身 & mBuffer 指向的 camera_metadata_t 所引用的内存;通过源码可知,每个 CameraMetadata 对象的 mBuffer 所指向的 camera_metadata_t 是独立的,彼此是不重叠的 。
文章插图
文章插图
既然工具能拦截到这么多的未释放的内存分配,一定是因为这些内存的释放逻辑出问题导致的,我们需要优先调查清楚 CameraMetadata.mBuffer 的释放逻辑 。通过分析 CameraMetadata.cpp 的源码可知,CameraMetadata::release()并未释放 mBuffer 所指向的内存,而是把 mBuffer 所指向的内存赋值给了另一个 CameraMetadata 对象;CameraMetadata::clear()是真释放,而 clear 的调用有两个场景:一个是在 camera_metadata_t 复用时,另一个是 CameraMetadata 对象析构时 。
文章插图
前述结论可知 CameraMetadata.mBuffer 所指向的 camera_metadata_t 是彼此独立的 。通过工具拦截到的堆栈和分配数量猜测,Native OOM 时内存中一定存在大量的 CameraMetadata 实例 。C++对象的析构通常是调用 delete 来实现的,AOSP 里想搜索哪里 delete 了一个 CameraMetaData 对象是很难的,因为很难知道 delete 时的变量名 。根据一个基本的 C++编程规范,内存通常在哪里创建的,应该就在那里释放,我们全局搜索 new CameraMetaData 字符串就可以很轻松的发现 CameraMetaData 对象的创建和释放均是在/frameworks/base/core/jni/android_hardware_camera2_CameraMetadata.cpp里实现的 。
文章插图
文章插图
文章插图
通过 android_hardware_camera2_CameraMetadata.cpp 里的注册清单可以看到与这些函数关联的 Java 层 class 是android/hardware/camera2/impl/CameraMetadataNative,CameraMetadata_close 函数在 Java 对应的是 nativeClose 函数 。可以进一步发现 CameraMetaDataNative 里 nativeClose 函数是在 close 函数里调用的,而 close 函数又是在 finalize 函数调用的 。
文章插图
文章插图
通过上述分析可知只有在 CameraMetaDataNative 对象执行 finalize 方法时才会回收与之对应的 native 内存,而 finalize 方法又是在 FinalizerDaemon 线程里执行的,猜测到如果发生了上述堆栈的 native OOM,Java 层一定存在大量还没有执行 finalize 方法的 CameraMetaDataNative 对象 。
推荐阅读
- 铠侠 极至光速系列内存卡评测:经典红白复刻,唯有品质依旧
- Android Jetpack架构组件Navigation管理Fragment框架
- 多进程编程 - 共享内存
- IDE 最佳Android应用程序开发工具
- 虚拟内存 和 page fault 的解释
- 对于内存结构的简单理解
- 电脑卡慢时升级内存是最低级做法,正确顺序是这样
- 高频内存对性能影响到底有几何?试试看就知道了
- Android WebView 优化梳理
- 定位Flutter内存问题很难么?