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

时间:2023-01-28 10:14:35

导读: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的调用。这两个函数中可以获取到调用点地址,通过对这些地址的记录就可以随时获取函数调用栈了。

插桩后效果示例:

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

这里需要格外注意,某些不需要插桩的函数可以使用__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_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中:

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;
}

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

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右移即可。

逻辑示意图:

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

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

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

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单线程

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

  • TLS方式单线程获取

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

  • libunwind 10个线程

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

  • TLS方式 10个线程

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

从上面几个统计图可以看出单线程模式下该方式是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端通过内存曲线及调用栈信息来排查问题了。

系统效果示例:

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

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

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

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

关注高德技术,找到更多出行技术领域专业内容

Android Native 内存泄漏系统化解决方案的更多相关文章

  1. &lbrack;原理&rsqb; Android Native内存泄漏检测原理解析

    转载请注明出处:https://www.cnblogs.com/zzcperf/articles/11615655.html 上一篇文章列举了不同版本Android OS内存泄漏的检测操作(传送门), ...

  2. &lbrack;教程&rsqb; Android Native内存泄漏检测方法

    转载请注明出处:https://www.cnblogs.com/zzcperf/p/9563389.html Android 检测 C/C++内存泄漏的方法越来越简便了,下面列举一下不同场景下检测C/ ...

  3. Android开发——常见的内存泄漏以及解决方案(二)

    )Android2.3以后,SoftReference不再可靠.垃圾回收期更容易回收它,不再是内存不足时才回收软引用.那么缓存机制便失去了意义.Google官方建议使用LruCache作为缓存的集合类 ...

  4. Android开发——常见的内存泄漏以及解决方案(一)

    0. 前言   转载请注明出处:http://blog.csdn.net/seu_calvin/article/details/52333954 Android的内存泄漏是Android开发领域永恒的 ...

  5. Android防止内存泄漏以及MAT的使用

    Android发生内存泄漏最普遍的一种情况就是长期保持对Context,特别是Activity的引用,使得Activity无法被销毁.这也就意味着Activity中所有的成员变量也没办法销毁.本文仅介 ...

  6. 利用Android Studio、MAT对Android进行内存泄漏检测

    利用Android Studio.MAT对Android进行内存泄漏检测 Android开发中难免会遇到各种内存泄漏,如果不及时发现处理,会导致出现内存越用越大,可能会因为内存泄漏导致出现各种奇怪的c ...

  7. android性能测试内存泄漏

    1.什么是内存泄漏?     适用于该系统的内存使用内存泄漏,未回复(释放),该内存可以没有事业,也不能被其他人使用使用自己. 2.出有什么差别?    内存泄漏是分配出去的内存无法回收.    内存 ...

  8. android 常见内存泄漏原因及解决办法

    android常见内存泄漏主要有以下几类: 一.Handler 引起的内存泄漏. 在Android开发中,我们经常会使用Handler来控制主线程UI程序的界面变化,使用非常简单方便,但是稍不注意,很 ...

  9. Android应用内存泄漏的定位、分析与解决策略

    什么是内存泄漏 对于不同的语言平台来说,进行标记回收内存的算法是不一样的,像 Android(Java)则采用 GC-Root 的标记回收算法.下面这张图就展示了 Android 内存的回收管理策略( ...

随机推荐

  1. Laravel 之Service Providers

    Service providers are the central place of all Laravel application bootstrapping. Your own applicati ...

  2. iOS 3D touch 使用技巧

    第一个 在桌面中3d Touch 打开菜单 由于本人纯属代码党,本次实现方法也只使用代码实现 到达到这个效果并不难,只需要在appdelegate中实现以下代码即可 ,当然也有缺点,就是这个app没运 ...

  3. 【BZOJ 1001】狼抓兔子 对偶图&plus;SPFA

    这道题是求图的最小割,也就是用最大流.但因为边太多,最大流算法会T,因此不能用最大流算法. 因为这是个平面图,所以求平面图的最小割可以使用特殊的技巧就是求对偶图然后求对偶图的最短路.把每个面看成一个点 ...

  4. 关于git服务器的搭建

    Git 服务器可搭建在多个系统平台上. 本篇以 Windows 和 Ubuntu 系统为例,简单介绍 Git 服务器的构建. 最后使用 eclipse 的 egit 插件和 git clone 这两种 ...

  5. CSS中&excl;important的使用 转

    本篇文章使用最新的IE10以及firefox与chrome测试(截止2013年5月27日22::) CSS的原理: 我们知道,CSS写在不同的地方有不同的优先级, .css文件中的定义 < 元素 ...

  6. 个人认为最好的Mac端的视频播放软件&lowbar;&lowbar;&lowbar;movist

    http://pan.baidu.com/s/1kVm0Zmn password : y9rn 点击打开链接 http://pan.baidu.com/s/1i4ABval password :kt3 ...

  7. TCPlayer web切换播放问题

    遇到播放的视频无法切换的问题,,即便是清除标签重新生成也不行~~ 需要使用自带的API ~别无他法 demo: http://imgcache.qq.com/open/qcloud/video/tcp ...

  8. cuda 配置要点

    1. 安装驱动 :sudo apt-get install nvidia- 2. 安装cuda : cuda 文件中包含驱动程序,因此在安装过程中当被问及是否安装驱动时,选择no 3. 安装cudnn ...

  9. Java的简单类型不能够精确的对浮点数进行运算

    由于Java的简单类型不能够精确的对浮点数进行运算,这个工具类提供精确的浮点数运算,包括加减乘除和四舍五入. import java.math.BigDecimal; /** * 由于Java的简单类 ...

  10. microsoft visual c&plus;&plus;与microsoft visual net 版本对应关系

    7.1 -> 2003 8.0 -> 2005 9.0 -> 2008 10.0 -> 2010 11.0 -> 2012 12.0 -> 2013 14.0 -& ...