微信号:cocoachinabbs

介绍:CocoaChina苹果开发中文社区官方微信,提供教程资源、app推广营销、招聘、外包及培训信息、各类沙龙交流活动以及更多开发者服务.

[iOS] 线上内存泄漏检测方案与结果

2018-06-14 09:00 ck2016

场景


监控线上用户的内存泄漏。用户无感知的情况下监控,不影响用户体验,包括内存,CPU,磁盘,网络流量。都有严格限制,用量过大用户一般都会发现的。


比如流量消耗过多会给用户感觉打开你这个APP,流量一下就没了,就不愿意再打开。CPU消耗过高,手机发烫掉电快。磁盘过多,在设置-用量页面可以看到每个APP的磁盘占用。


监控哪些内存


1.不监控 OC 对象的 alloc 方法。因为现在都是 ARC 方式管理内存,并且不打算支持 MRC,所以不监控 alloc 方法。而循环引用属于特殊的泄漏在下个版本做,经实验 alloc 底层没调用 malloc。


2.监控 malloc / free / malloc_zone_malloc 系列方法。


3.C++ 的 new / delete 运算符暂时没找到办法 hook。有人能hook new运算符麻烦告诉我,谢谢。


所以暂时只监控C层的 malloc 等16个内存管理的函数。


已知的手段:


  • 1.fishhook 能 hook C函数,那么能拿到所有 malloc / free 的参数和返回值


  • 2.腾讯OOMDetector 能遍历 堆,栈,寄存器 的指针变量的值(如果一块内存不是泄漏的,那么在内存中就能找到一个指针变量指向它,泄漏的内存当然就找不到指针指向它了)


  • 3.OOMDetector 在扫描泄漏的时候要挂起所有子线程2秒,线上会影响到用户体验。


比如用户的网络请求出去了,恰好触发泄漏检测,界面要多转2秒的菊花,网络返回的数据才能刷新UI。退到后台才检测的话,有些实时性的业务数据也在后台上报,那么可能会导致这些数据整体偏后2秒。还有内存检测本身耗的内存和CPU都要尽可能低才行。


所以需要一个能检测线上内存泄漏且不影响业务和用户体验的方案。不挂起任何线程。


泄漏检测算法


我设计优化后的泄漏检测算法大致原理如下:


  • 1.hook malloc / free 等16个内存管理函数,malloc 调用是非常频繁的,一旦 hook 后能形成非常高速的 malloc / free 流。


  • 2.用个哈希表记录已经分配的内存块(key : 地址,value : (调用栈,块大小,计数器等等))


  • 3.在hook后的 malloc / free 方法中能拿到申请和释放的地址。如果遇到 malloc 申请,向哈希表中插入一个key为该地址的元素。如果遇到 free 释放,在哈希表中删除一个key为该地址的元素。那么这个哈希表中就记录着当前进程中所有申请的内存块。


  • 4.发起内存泄漏检测的时候,遍历内存中所有指针指向的地址,然后在哈希表中查,如果有该地址,那么对应的value的计数器加一,如果没有则跳过。遍历完了之后,查哈希表中所有元素的计数器,显然计数器为 0 的内存块就是泄漏的,没有一个指针指向他,因为如果有指针指向他,他的计数器会被加一。发现泄漏后,把value中的调用栈,地址等等上报到后台,程序员根据调用栈就能找到相关代码进行泄漏修复。


几个重要的问题


  • 1.发起内存泄漏检测的时候怎么办?


一个空项目的所有指针变量的地址大约有100多万个,遍历一次大约需要2秒,与此同时 malloc / free 流是非常高速的。


腾讯OOMDetector的做法是挂起所有子线程,其实就是停止 malloc 流,然后再做泄漏检测。


如果不停止 malloc 流会有这样的问题。内存遍历扫过A区域后,正好在A区域有个 指针P 新 malloc 了一块内存,该指针P也没泄漏,哈希表中也有这块内存,但新创建的内存在哈希表中的计数器是0,遍历已扫过了A区域不会再回来,那么后面判断的时候就会把这块内存判定为泄漏,但其实没有泄漏,他是你内存遍历扫过后才创建的。造成误判。所以他得挂起所有线程。


为了线上使用,肯定不能挂起线程去停止 malloc 流。我的做法是:利用两个哈希表,hashA 和 hashB,交替使用。


初始 malloc 流都进入 hashA 内,在触发内存泄漏检测前,先把 malloc 流切到 hashB 内,然后在 hashA 内判断泄漏的内存块,同时 free 流也要同时流入 hashA,hashB,因为可能在内存遍历的时候也有释放 hashA 中的内存块,要删除 hashA 中对应元素,不然会误判了。


检测方法还是看元素的计数器是不是 0,0则意味泄漏。检测完后把 hashA 中没有泄漏的块倒入hashB中,泄漏的块上报相关信息到后台。


下次再发起内存检测的时候,则把 malloc 流从 hashB 切到 hashA,A/B 交替切换使用即可。这做法无需挂起任何线程,可在异步线程执行,不干扰主线程。


  • 2.多线程访问处理


是在调用 malloc 的线程插入哈希表还是统一 dispatch_async 到一个串行 queue 里插入哈希表?


在malloc原有线程获取数据并插入到哈希表可能会减慢原有的malloc流程,dispatch_async 到串行 queue 里操作哈希表几乎不影响原有线程的执行速度。

另外内存遍历的线程也要查哈希表,malloc / free 的线程也要查哈希表,那么只在统一的"串行queue"中操作哈希表,在多线程下就不会有问题。


  • 3.地址太多导致内存只增不减怎么办


假设内存块有100万个,那么哈希表中就有100万个元素,这些元素本身就占很多内存。


在这里我设个最大值,假设线上只能用10MB内存来检测泄漏。一个哈希表元素大小是 300 byte,那么限制哈希表的最大元素个数为 10MB / 300byte = 35000 个,超过35000个元素后的内存块直接丢弃,不再记录。


那么会导致有些泄漏被错过,线上还怎么能用呢?


对于一个泄漏X,A用户错过了,B用户能捕捉到就行,灰度10万个用户,有一个用户能捕捉到泄漏X并上报到后台就行。不要求单个用户能捕捉完所有的泄漏,10万个用户有一个能捕捉到就行。


万一真有一个泄漏,10万个用户都没捕捉到怎么办?一个版本能捕捉到 200 个泄漏,已经够程序员修复一段时间发版本了,修复完这200个后,排在后面的泄漏自然会捕捉到,并排到前面来的。


测试开发期,配置不限制内存使用,那么所有泄漏都能捕捉到。线上期,配置最大 10MB,那么可能会有错过的,依赖多用户,最终都能捕捉到。


  • 4. dispatch_async调用100w次内存暴增怎么办

在内存地址遍历的时候,一个空项目的所有指针变量的地址大约有100多万个,一个地址要查询一次哈希表,所以要dispatch_async一次。dispatch_async 底层调了 calloc 导致内存增长,实测调100w次增加100MB左右,所以不能用GCD的dispatch_async了,另外尝试了 performSelector 到异步线程,这个方法导致的内存增加比 GCD 还高。


在找不到其他框架的情况下,自己手动实现了一个简单的 dispatch_async,创建一个pthread线程,线程内部写个死循环,用信号量阻塞,不占CPU时间,有任务的时候唤醒,执行任务,无任务的时候阻塞。任务加到 std::list 中,然后唤醒线程去执行。性能达标,不会有内存只增不减的问题。


结果


经过上述实现,已经在后台捕捉到了内存泄漏,内存控制在5MB之内,在遍历内存地址的时候有几秒CPU是100%的,但都是在异步线程。


更多推荐:


 
Cocoa开发者社区 更多文章 iOS 12 新内容 开发者所需要知道的 WWDC 2018 新特性 iOS 流量监控分析 为什么开发者在 iOS 上就廉价,在 GitHub 上就值钱? Xcode10新内容
猜您喜欢 如何避免知识碎片化所带来的弊端? 技术 | Hadoop 面试,有它就够了 异步修饰器tomorrow之简介 中国最穷上市公司:账上仅178元,网站都供不起 来点儿鸡汤:俞敏洪说我和马云就差了8个字。