写点什么

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

  • 2020-03-04
  • 本文字数:3213 字

    阅读完需:约 11 分钟

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-03-04 14:481700

评论

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

首发!OceanBase社区版入门教程开课啦!

OceanBase 数据库

数据库 开源 新闻 课程 oceanbase

Hadoop 生态里,为什么 Hive 活下来了?

大数据技术指南

11月日更

3分钟搞定 web人脸识别登录,这样式爱了

程序员小富

Java 编程 人脸识别 springboot 毕业设计

驴行千里不洗沙尘,尚硅谷Spark性能调优教程发布

编程江湖

大数据 spark

Redis为什么需要强一致?技术揭秘秒杀活动如何限流

华为云开发者联盟

redis 开源 华为云 强一致 MySQL组件

Apache ShardingSphere 企业行|走进转转

SphereEx

ShardingSphere SphereEx Apache ShardingSphere 转转

阿里云发布云原生加速器,携手生态企业拥抱数字时代

阿里巴巴云原生

阿里云 云原生 企业 合作伙伴 创投

青海西宁市正规等保测评公司名单汇总-行云管家

行云管家

网络安全 等级保护 等保测评 过等保

恒源云(GPUSHARE)_云GPU服务器如何使用VSCode?

恒源云

人工智能 深度学习

几个高效做事的法则,让你的一天有 25 小时

程序员鱼皮

Java c++ 效率 大前端 高效

Linux一学就会之Linux环境搭建并安装VMware虚拟机

学神来啦

Linux centos 运维 vmware

“低代码”是什么?低代码平台如何助力企业实现数字化转型?

优秀

低代码

我们如何实现“业务 100% 云原生化,让阿里中间件全面升级到公共云架构”?

阿里巴巴中间件

阿里云 云原生 中间件 三位一体

Apollo生产环境整合springboot

小鲍侃java

11月日更

解决 Serverless 落地困难的关键,是给开发者足够的“安全感”

阿里巴巴中间件

阿里云 Serverless 技术 云原生 中间件

百度Q3财报:百度智能云同比增长73%,稳居中国四朵云之一

百度大脑

人工智能

智能云分支(Cloud Intelligent Branch)方案正式发布!

阿里云 云网络 智能化 发布会

HBase 的预分区及 rowkey 设计技巧

五分钟学大数据

11月日更

安全稳定高效节约的云运维软件哪个好?咨询电话多少?

行云管家

云计算 公有云 混合云 云管平台 云运维

微信和QQ这么多群,该如何管理好友关系?

Tom弹架构

Java 架构 设计模式

用户增速与体验质量并存,博睿数据携阿里云发布双十一电商网站用户体验报告

博睿数据

Python代码阅读(第62篇):列表是否包含相同元素判断

Felix

Python 编程 列表 阅读代码 Python初学者

The Data Way Vol.6|我不是开发者,但我依然向往开源

SphereEx

开源 开发者 播客 ShardingSphere SphereEx

微服务的灾难:拆的很爽,但服务太小...

热爱java的分享家

Java 架构 程序人生 编程语言 经验分享

数据可视化界的小公主:cutecharts,入门+实战应用

老表

Python 数据可视化 11月日更 实战案例 cutecharts

LifseaOS 悄然来袭,一款为云原生而生的 OS

阿里巴巴云原生

阿里云 云原生 操作系统 LifseaOS

JAVA应用生产问题排查步骤

热爱java的分享家

Java 架构 程序人生 编程语言 经验分享

CRM与ERP之争,谁能在“企业数字化转型”的趋势中胜出?

优秀

低代码 CRM ERP

带你了解AKG正反向算子注册+关联流程

华为云开发者联盟

算子 AKG 正向算子 反向算子 算子注册

微帧Film Grain编码技术,致敬电影胶片颗粒的独特魅力

微帧Visionular

视频编解码

群雄“逐鹿”风采显露:2021信创“大比武”鲲鹏赛道总决赛火热来袭

科技热闻

Android Native 内存泄漏系统化解决方案_文化 & 方法_高德技术_InfoQ精选文章