AICon 上海站|日程100%上线,解锁Al未来! 了解详情
写点什么

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:481513

评论

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

KubeFATE: 用云原生技术赋能联邦学习(二)

亨利笔记

Kubernetes 云原生 k8s FATE KUBEFATE

为AndroidApk添加系统级签名

Howe

Java android

技术人员加薪二三事

南方

管理 职场 技术管理 加薪 劈空掌

iOS Release 版本开启调试功能

liu_liu

ios release 调试

Java并发编程系列——锁顺序

孙苏勇

Java Java并发 并发编程 多线程

如何梳理画出牛逼的、高大上的架构图?

狂师

程序员 企业架构 开发者 软件测试 软件开发

Nacos 1.1.4 与微服务的实践经验记录

itfinally

Java 微服务 nacos

Redis学习笔记(概述)

编程随想曲

redis

Dubbo 概述

会飞的猪

Spring Cloud概述

会飞的猪

聊聊数据库原理和索引结构:1000万条数据优化后为什么能提升1500倍

牧码哥

MySQL 数据库 数据结构 性能优化 索引结构

游戏夜读 | 2020周记(4.3-4.10)

game1night

制作Unknown Pleasures效果图的3种方法

张云金_GISer

设计 T恤 GIS 地图

Java新技术:文字块

X.F

Java 编程语言

Spring中的测试类~简洁方便

程序员的时光

spring

Boyer-Moore 算法

Kenn

算法 数组 Boyer-Moore

认识数据产品经理(一 数据产品经理的细分)

马踏飞机747

大数据 数据中台 数据分析 产品经理

我愿沉迷于学习,无法自拔(三)

孙瑜

深度思考 程序员 感悟

为什么每个软件人都要懂点系统架构?

刘华Kenneth

架构 DevOps 高可用 敏捷 高并发

程序员陪娃漫画系列——上学路上

孙苏勇

程序员 生活 陪伴 漫画

JAVA中Base64加密与解密

Howe

Java base64 加密解密

Kafka系列第4篇:消息发送时,网络“偷偷”帮忙做的那点事儿

z小赵

kafka 推荐 实时计算

缓存的五种设计模式

Rayjun

缓存

动态规划问题的思路和技巧

Kenn

算法 动态规划

找工作不得不知道的事

熊斌

认知提升 求职

聊聊测试工程师的价值

软件测试 质量 测试工程师产出 测试的价值

20 大类,100+ 网络副业兼职平台汇总推荐

一尘观世界

程序员 自由职业 副业 赚钱

记录自有意义

彭宏豪95

人生 写作 感悟 记录

职场“35岁现象”:焦虑 or 出路?是时候说出真相了!

狂师

职场 成长 软件测试 测试 软件开发

动画设计的十个原则

养牛致富带头人

设计 动画

从Integer开始阅读JDK源码

指尖流逝

Java jdk源码

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