写点什么

Android Native 内存泄漏系统化解决方案

2020 年 3 月 04 日

Android Native 内存泄漏系统化解决方案

导读:C++内存泄漏问题的分析、定位一直是 Android 平台上困扰开发人员的难题。因为地图渲染、导航等核心功能对性能要求很高,高德地图 APP 中存在大量的 C++代码。解决这个问题对于产品质量尤为重要和关键,我们在实践中形成了一套自己的解决方案。


分析和定位内存泄漏问题的 核心在于分配函数的统计和栈回溯。如果只知道内存分配点不知道调用栈会使问题变得格外复杂,增加解决成本,因此两者缺一不可。


Android 中 Bionic 的 malloc_debug 模块对内存分配函数的监控及统计是比较完善的,但是栈回溯在 Android 体系下缺乏高效的方式。随着 Android 的发展,Google 也提供了栈回溯的一些分析方法,但是这些方案存在下面几个问题:


  1. 栈回溯的环节都使用的 libunwind,这种获取方式消耗较大,在 Native 代码较多的情况下,频繁调用会导致应用很卡,而监控所有内存操作函数的调用栈正需要高频的调用 libunwind 的相关功能。

  2. 有 ROM 要求限制,给日常开发测试带来不便。

  3. 用命令行或者 DDMS 进行操作,每排查一次需准备一次环境,手动操作,最终结果也不够直观,同时缺少对比分析。


因此,如何进行高效的栈回溯、搭建系统化的 Android Native 内存分析体系显得格外重要。


高德地图基于这两点做了一些改进和扩展,经过这些改进,通过自动化测试可及时发现并解决这些问题,大幅提升开发效率,降低问题排查成本。


栈回溯加速


Android 平台上主要采用 libunwind 来进行栈回溯,可以满足绝大多数情况。但是 libunwind 实现中的全局锁及 unwind table 解析,会有性能损耗,在多线程频繁调用情况下会导致应用变卡,无法使用。


加速原理


编译器的-finstrument-functions 编译选项支持编译期在函数开始和结尾插入自定义函数,在每个函数开始插入对__cyg_profile_func_enter 的调用,在结尾插入对__cyg_profile_func_exit 的调用。这两个函数中可以获取到调用点地址,通过对这些地址的记录就可以随时获取函数调用栈了。


插桩后效果示例:



这里需要格外注意,某些不需要插桩的函数可以使用__attribute__((no_instrument_function))来向编译器声明。


如何记录这些调用信息?我们想要实现这些信息在不同的线程之间读取,而且不受影响。一种办法是采用线程的同步机制,比如在这个变量的读写之处加临界区或者互斥量,但是这样又会影响效率了。


能不能不加锁?这时就想到了线程本地存储,简称 TLS。TLS 是一个专用存储区域,只能由自己线程访问,同时不存在线程安全问题,符合这里的场景。


于是采用编译器插桩记录调用栈,并将其存储在线程局部存储中的方案来实现栈回溯加速。具体实现如下:


  1. 利用编译器的-finstrument-functions 编译选项在编译阶段插入相关代码。

  2. TLS 中对调用地址的记录采用数组+游标的形式,实现最快速度的插入、删除及获取。


定义数组+游标的数据结构:


typedef struct {    void* stack[MAX_TRACE_DEEP];    int current;} thread_stack_t;
复制代码


初始化 TLS 中 thread_stack_t 的存储 key:


static pthread_key_t sBackTraceKey;static pthread_once_t sBackTraceOnce = PTHREAD_ONCE_INIT;
static void __attribute__((no_instrument_function))destructor(void* ptr) { if (ptr) { free(ptr); }}
static void __attribute__((no_instrument_function))init_once(void) { pthread_key_create(&sBackTraceKey, destructor);}
复制代码


初始化 thread_stack_t 放入 TLS 中:


thread_stack_t* __attribute__((no_instrument_function))get_backtrace_info() {    thread_stack_t* ptr = (thread_stack_t*) pthread_getspecific(sBackTraceKey);    if (ptr)        return ptr;
ptr = (thread_stack_t*)malloc(sizeof(thread_stack_t)); ptr->current = MAX_TRACE_DEEP - 1; pthread_setspecific(sBackTraceKey, ptr); return ptr;}
复制代码


  1. 实现__cyg_profile_func_enter 和__cyg_profile_func_exit,记录调用地址到 TLS 中。


extern "C" {void __attribute__((no_instrument_function))__cyg_profile_func_enter(void* this_func, void* call_site) {    pthread_once(&sBackTraceOnce, init_once);    thread_stack_t* ptr = get_backtrace_info();    if (ptr->current > 0)        ptr->stack[ptr->current--] = (void*)((long)call_site - 4);}
void __attribute__((no_instrument_function))__cyg_profile_func_exit(void* this_func, void* call_site) { pthread_once(&sBackTraceOnce, init_once); thread_stack_t* ptr = get_backtrace_info(); if (++ptr->current >= MAX_TRACE_DEEP) ptr->current = MAX_TRACE_DEEP - 1;}}
复制代码


__cyg_profile_func_enter 的第二个参数 call_site 就是调用点的代码段地址,函数进入的时候将它记录到已经在 TLS 中分配好的数组中,游标 ptr->current 左移,待函数退出游标 ptr->current 右移即可。


逻辑示意图:



记录方向和数组增长方向不一致是为了对外提供的获取栈信息接口更简洁高效,可以直接进行内存 copy 以获取最近调用点的地址在前、最远调用点的地址在后的调用栈。


  1. 提供接口获取栈信息。


int __attribute__((no_instrument_function))get_tls_backtrace(void** backtrace, int max) {    pthread_once(&sBackTraceOnce, init_once);    int count = max;    thread_stack_t* ptr = get_backtrace_info();    if (MAX_TRACE_DEEP - 1 - ptr->current < count) {        count = MAX_TRACE_DEEP - 1 - ptr->current;    }    if (count > 0) {        memcpy(backtrace, &ptr->stack[ptr->current + 1], sizeof(void *) * count);    }    return count;}
复制代码


5.将上面逻辑编译为动态库,其他业务模块都依赖于该动态库编译,同时编译 flag 中添加-finstrument-functions 进行插桩,进而所有函数的调用都被记录在 TLS 中了,使用者可以在任何地方调用 get_tls_backtrace(void** backtrace, int max)来获取调用栈。


效果对比(采用 Google 的 benchmark 做性能测试,手机型号:华为畅想 5S,5.1 系统)


  • libunwind 单线程



  • TLS 方式单线程获取



  • libunwind 10 个线程



  • TLS 方式 10 个线程



从上面几个统计图可以看出单线程模式下该方式是 libunwind 栈获取速度的 10 倍,10 个线程情况下是 libunwind 栈获取速度的 50-60 倍,速度大幅提升。


优缺点


  • 优点: 速度大幅提升,满足更频繁栈回溯的速度需求。

  • 缺点: 编译器插桩,体积变大,不能直接作为线上产品使用,只用于内存测试包。这个问题可以通过持续集成的手段解决,每次项目出库将 C++项目产出普通库及对应的内存测试库。


体系化


经过以上步骤可以解决获取内存分配栈慢的痛点问题,再结合 Google 提供的工具,如 DDMS、adb shell am dumpheap -n pid /data/local/tmp/heap.txt 命令等方式可以实现 Native 内存泄漏问题的排查,不过排查效率较低,需要一定的手机环境准备。


于是,我们决定搭建一整套体系化系统,可以更便捷的解决此类问题,下面介绍下整体思路:


  • 内存监控沿用 LIBC 的 malloc_debug 模块。不使用官方方式开启该功能,比较麻烦,不利于自动化测试,可以编译一份放到自己的项目中,hook 所有内存函数,跳转到 malloc_debug 的监控函数 leak_xxx 执行,这样 malloc_debug 就监控了所有的内存申请/释放,并进行了相应统计。

  • 用 get_tls_backtrace 实现 malloc_debug 模块中用到的__LIBC_HIDDEN__ int32_t get_backtrace_external(uintptr_t* frames, size_t max_depth),刚好同上面说的栈回溯加速方式结合。

  • 建立 Socket 通信,支持外部程序经由 Socket 进行数据交换,以便更方便获取内存数据。

  • 搭建 Web 端,获取到内存数据上传后可以被解析显示,这里要将地址用 addr2line 进行反解。

  • 编写测试 Case,同自动化测试结合。测试开始时通过 Socket 收集内存信息并存储,测试结束将信息上传至平台解析,并发送评估邮件。碰到有问题的报警,研发同学就可以直接在 Web 端通过内存曲线及调用栈信息来排查问题了。


系统效果示例:





2020 年 3 月 04 日 14:48254

评论

发布
暂无评论
发现更多内容

2021年编排将成为DevSecOps关键推动者

啸天

DevSecOps 应用安全 开发安全

超强Android进阶路线知识图谱:Kotlin可能带来的一个深坑,持续更新中

欢喜学安卓

android 程序员 面试 移动开发

关于事务、redolog 写入的两个问题分析

程序员架构进阶

MySQL innodb 事务 28天写作

并发阻塞队列(BlockingQueue)— 生产者消费者模式核心部件

码农架构

Java 架构 jdk 设计模式

字节内部MySQL宝典意外流出!极致经典,堪称数据库的天花板

比伯

Java 编程 架构 面试 程序人生

K线成交量管理系统开发、成交量管理系统开发

W13902449729

K线成交量管理系统开发 成交量管理系统开发

细节!3部分讲明白HotSpot:运行时+编译器+垃圾回收器

996小迁

Java 架构 虚拟机 hotspot

AES/CBC/PKCS5Padding到底是什么

kof11321

加密解密

我给职场新人提个建议

石云升

创业 28天写作 职场新人

阿里面试官纯手打:金九银十跳槽必会Java核心知识点笔记整理

Java架构之路

Java 程序员 架构 面试 编程语言

区块链发展应以密码应用创新为根基

CECBC区块链专委会

区块链 密码学

盘点2020|征文大赛获奖名单公布

InfoQ写作平台官方

活动专区 盘点2020

龙归科技 |软件的成本下降

龙归科技

身份认证 企业信息化 SSO

音乐混音怎么做?教你完美制作野狼disco与周杰伦双节棍合唱!

懒得勤快

音频技术 音频制作 混音 音乐混音

1月|日更挑战活动说明

InfoQ写作平台官方

活动专区 日更挑战

厉害了!来看看这份超全面的《Android面试题及解析》,一线互联网公司面经总结

欢喜学安卓

android 程序员 面试 移动开发

nodejs事件和事件循环详解

程序那些事

nodejs 异步编程 程序那些事 事件和事件循环 nodejs event

JVM故障诊断和处理工具

Silently9527

Java JVM jvm调优

CSS14 - 元素可见性

桃夭十一里

html/css

DeFi流动性挖矿管理系统开发|去中心化金融借贷系统开发

W13902449729

去中心化金融借贷系统开发 DeFi流动性管理系统开发

甲方日常 85

句子

工作 随笔杂谈 日常

CSS15 - 界面样式&垂直对齐

桃夭十一里

html/css

Hadoop的MapReduce到底有什么问题?

hanke

大数据 hadoop spark mapreduce 开源

重学JS | 异步编程 async/await

梁龙先森

前端 编程语言 28天写作

“区块链+数字身份”,道路坎坷前途光明

CECBC区块链专委会

数字技术

工作11年,从阿里P8出来,头发也没了,人也虚了,就剩下这份笔记了!

Java架构之路

Java 程序员 架构 面试 编程语言

企业如何预防短信验证码被别人盗用

香芋味的猫丶

短信防刷 短信轰炸机 短信验证码 短信防轰炸 短信防火墙

从一场营地教育直播,看懂众盟“私域流量营销”的底层逻辑

脑极体

阿里资深架构师整理出来的一份Java核心知识点分享给大家.pdf

Crud的程序员

Java 架构 java程序员

案例研究之聊聊 QLExpress 源码 (六)

小诚信驿站

28天写作 QLExpress源码 聊聊源码

TCP波场拼系统开发|TCP波场拼软件APP开发

开發I852946OIIO

系统开发

Android Native 内存泄漏系统化解决方案-InfoQ