摘要
Raphael [1]是西瓜视频基础技术团队开发的一款 native 内存泄漏检测工具,广泛用于字节跳动旗下各大 App 的 native 内存泄漏治理,收益显著。工具现已开源,本文将通过原理、方案和实践来剖析 Raphael 的相关细节。
背景
Android 平台上的内存问题一直是性能优化和稳定性治理的焦点和痛点,Java 堆内存因为有比较成熟的工具和方法论,加上 hprof 快照作为补充,定位和治理都很方便。而 native 内存问题一直缺乏稳定、高效的工具,仅有的 malloc debug [6]不仅性能和稳定性难以满足需要,还存在 Android 版本兼容的问题。
现状
事实上,native 内存泄漏治理一直不乏优秀的工具,已知的可用于调查 native 内存泄漏问题的工具主要有:LeakTracer、MTrace、MemWatch、Valgrind-memcheck、TCMalloc、LeakSanitizer 等。但由于 Android 平台的特殊性,这些工具要么不兼容,要么接入成本过高,很难在 Android 平台上落地。这些工具的原理基本都是:先代理内存分配/释放相关的函数(如:malloc/calloc/realloc/memalign/free),再通过 unwind 回溯调用堆栈,最后借助缓存管理过滤出未释放的内存分配记录。因此,这些工具的主要差异也就体现在代理实现、栈回溯和缓存管理三个方面。根据这些工具代理实现的差异,大致可以分为 hook 和 LD_PRELOAD 两大类,典型的如 malloc debug [5] 和 LeakTracer。
malloc debug
malloc debug 是 Android 系统自带的内存调试工具(官方 Native 内存调试 有相关介绍 ) ,虽然没有额外的接入代码,但开启方式和核心功能等都受 Android 版本限制。
我们在线下尝试使用 malloc debug 监控西瓜视频 App(配置 wrap.sh)时发现,正常启动时间小于 1s 的机型(Pixel 2 & Android 10),其冷启动时间被拉长到了 11s+。而且在正常使用过程中滑动时的卡顿感非常明显,页面切换时耗时难以接受,监控过程中应用的使用体验极差。不仅如此,西瓜视频在 malloc debug 监控过程中还会遇到必现的栈回溯 crash(堆栈如下,《libunwind llvm 编年史》[8] 有相关分析)。

LeakTracer
LeakTracer 是另一个比较知名的内存泄漏监控工具,其原理是:通过 LD_PRELOAD 机制抢先加载一个定义了 malloc/calloc/realloc/memalign/free 等同名函数的代理库,这样就全局代理了应用层内存的分配和释放,通过 unwind 回溯调用栈并过滤出疑似的内存泄漏信息。Android 平台上的 LD_PRELOAD 是被严格限制的,因为其没有独立的 unwind 实现,依赖系统的 unwind 能力,也会遇到 malloc debug 遇到的栈帧兼容问题;如果把 LeakTracer 集成到目标 so 里通过 override 方式实现代理,只能拦截到本 so 里显式的内存分配/释放,无法拦截到其他 so 和跨 so 调用的内存分配/释放。通过 native 插桩的方式也是如此,只能监控局部单纯的内存泄漏,无法全局监控内存使用。
综合以上分析和接入体验,我们不难发现,这些内存泄漏监控工具在 Android 平台上实际接入时基本都存在以下三个比较典型的问题:
流程繁琐:需要配置 wrap.sh/root permission/setprop 等,受 Android 版本限制
兼容问题:unwind 库存在严重的兼容性问题,libunwind_llvm 无法正确回溯 GNU 编译的栈帧
性能问题:官方的 malloc debug 性能数据是损失 10 倍以上,实测西瓜开启后在中高端机上不可用
我们的需求
西瓜视频 App 是一个汇集了视频播放、特效拍摄、视频剪辑辑、P2P 加速等 native 代码非常多的中大型应用,每个 native 代码相关的模块背后都有一个专业团队在高速迭代,加上日人均使用时长超过 100 分钟的影响,西瓜视频 App 的 native 内存问题治理难度非常大。事实上,单纯的内存泄漏问题相对较少,更多的是因为业务逻辑不合理带来的内存使用问题,需要工具渗透到 App 运行的过程中进行监控,无形中提高了对工具性能和稳定性的要求。
线上 native 内存问题基本都是以虚拟内存触顶的形式暴露出来的。在西瓜视频 App 里,虚拟内存的消耗除了上述几大模块外,还有其他几个消耗大户,如线程、webview、Flutter、硬件加速、显存等。事实上,malloc/calloc/realloc/memalign 等相对于 mmap/mmap64 直接分配出的内存在整个虚拟内存空间中通常占比比较小。因为内存问题通常以虚拟内存耗尽的形式表现出来,只有尽可能多的收集各种内存消耗来无限逼近虚拟内存上限,才能准确找出虚拟内存耗尽的原因。因此,像 malloc debug 这样只监控 malloc/calloc/realloc/memalign/free 等根本无法满足内存治理需要,覆盖 mmap/mmap64/munmap 等尽可能多的内存分配形式是监控工具必须要做的。
综合上面的分析可以得出,西瓜视频 App 乃至整个字节跳动旗下其他 App, 对于一个通用的 native 内存泄漏监控工具的诉求主要有以下几个方面:
接入层面:不依赖 Android 版本,无需 root,对业务渗透尽可能低
稳定性:不存在影响业务的稳定性问题,