微信团队原创分享:iOS版微信的内存监控系统技术实践

本文来自微信开发团队yangyang的技术分享。

一、前言

FOOM(Foreground Out Of Memory),是指App在前台因消耗内存过多引起系统强杀。对用户而言,表现跟crash一样。Facebook早在2015年8月提出FOOM检测办法,大致原理是排除各种情况后,剩余的情况是FOOM,具体链接:https://code.facebook.com/posts/1146930688654547/reducing-fooms-in-the-facebook-ios-app/

微信自15年年底上线FOOM上报,从最初数据来看,每天FOOM次数与登录用户数比例接近3%,同期crash率1%不到。而16年年初某东老大反馈微信频繁闪退,在艰难拉取2G多日志后,才发现kv上报频繁打log引起FOOM。接着16年8月不少外部用户反馈微信启动不久后闪退,分析大量日志还是不能找到FOOM原因。微信急需一个有效的内存监控工具来发现问题。

(本文同步发布于:http://www.52im.net/thread-1422-1-1.html

二、实现原理

微信内存监控最初版本是使用Facebook的FBAllocationTracker工具监控OC对象分配,用fishhook工具hook malloc/free等接口监控堆内存分配,每隔1秒,把当前所有OC对象个数、TOP 200最大堆内存及其分配堆栈,用文本log输出到本地。该方案实现简单,一天内完成,通过给用户下发TestFlight,最终发现联系人模块因迁移DB加载大量联系人导致FOOM。

不过这方案有不少缺点:

1)监控粒度不够细,像大量分配小内存引起的质变无法监控,另外fishhook只能hook自身app的C接口调用,对系统库不起作用;

2)打log间隔不好控制,间隔过长可能丢失中间峰值情况,间隔过短会引起耗电、io频繁等性能问题;

3)上报的原始log靠人工分析,缺少好的页面工具展现和归类问题。

所以二期版本以Instruments的Allocations为参考,着重四个方面优化,分别是数据收集、存储、上报及展现。

2.1、数据收集

16年9月底为了解决ios10 nano crash,研究了libmalloc源码,无意中发现这几个接口:

当malloc_logger和__syscall_logger函数指针不为空时,malloc/free、vm_allocate/vm_deallocate等内存分配/释放通过这两个指针通知上层,这也是内存调试工具malloc stack的实现原理。有了这两个函数指针,我们很容易记录当前存活对象的内存分配信息(包括分配大小和分配堆栈)。分配堆栈可以用backtrace函数捕获,但捕获到的地址是虚拟内存地址,不能从符号表dsym解析符号。所以还要记录每个image加载时的偏移slide,这样符号表地址=堆栈地址-slide。

另外为了更好的归类数据,每个内存对象应该有它所属的分类Category,如上图所示。对于堆内存对象,它的Category名是“Malloc ”+分配大小,如“Malloc 48.00KiB”;对于虚拟内存对象,调用vm_allocate创建时,最后的参数flags代表它是哪类虚拟内存,而这个flags正对应于上述函数指针__syscall_logger的第一个参数type,每个flag具体含义可以在头文件找到;对于OC对象,它的Category名是OC类名,我们可以通过hook OC方法+[NSObject alloc]来获取:

但后来发现,NSData创建对象的类静态方法没有调用+[NSObject alloc],里面实现是调用C方法NSAllocateObject来创建对象,也就是说这类方式创建的OC对象无法通过hook来获取OC类名。最后在苹果开源代码CF-1153.18找到了答案,当__CFOASafe=true并且__CFObjectAllocSetLastAllocEventNameFunction!=NULL时,CoreFoundation创建对象后通过这个函数指针告诉上层当前对象是什么类型:

通过上面方式,我们的监控数据来源基本跟Allocations一样了,当然是借助了私有API。如果没有足够的“技巧”,私有API带不上Appstore,我们只能退而求其次。修改malloc_default_zone函数返回的malloc_zone_t结构体里的malloc、free等函数指针,也是可以监控堆内存分配,效果等同于malloc_logger;而虚拟内存分配只能通过fishhook方式。

2.2、数据存储

2.2.1 存活对象管理

APP在运行期间会大量申请/释放内存。以上图为例,微信启动10秒内,已经创建了80万对象,释放了50万,性能问题是个挑战。另外在存储过程中,也尽量减少内存申请/释放。所以放弃了sqlite,改用了更轻量级的平衡二叉树来存储。

伸展树(Splay Tree),也叫分裂树,是一种二叉排序树,不保证树是平衡,但各种操作平均时间复杂度是O(logN),可近似看作平衡二叉树。相比其他平衡二叉树(如红黑树),其内存占用较小,不需要存储额外信息。伸展树主要出发点是考虑到局部性原理(某个刚被访问的结点下次又被访问,或者访问次数多的结点下次可能被访问),为了使整个查找时间更少,被频繁查询的结点通过“伸展”操作搬移到离树根更近的地方。大部分情况下,内存申请很快又被释放,如autoreleased对象、临时变量等;而OC对象申请内存后紧接着会更新它所属Category。所以用伸展树管理最适合不过了。

传统二叉树是用链表方式实现,每次添加/删除结点,都会申请/释放内存。为了减少内存操作,可以用数组实现二叉树。具体做法是父结点的左右孩子由以往的指针类型改成整数类型,代表孩子在数组的下标;删除结点时,被删除的结点存放上一个被释放的结点所在数组下标。

2.2.2 堆栈存储

据统计,微信运行期间,backtrace的堆栈有成百万上千万种,在捕获最大栈长64情况下,平均栈长35。如果36bits存储一个地址(armv8最大虚拟内存地址48bits,实际上36bits够用了),一个堆栈平均存储长度157.5bytes,1M个堆栈需要157.5M存储空间。但通过断点观察,实际上大部分堆栈是有共同后缀,例如下面的两个堆栈后7个地址是一样的:

为此,可以用Hash Table来存储这些堆栈。思路是整个堆栈以链表的方式插入到table里,链表结点存放当前地址和上一个地址所在table的索引。每插入一个地址,先计算它的hash值,作为在table的索引,如果索引对应的slot没有存储数据,就记录这个链表结点;如果有存储数据,并且数据跟链表结点一致,hash命中,继续处理下一个地址;数据不一致,意味着hash冲突,需要重新计算hash值,直到满足存储条件。举个例子(简化了hash计算):

1)Stack1的G、F、E、D、C、A、依次插入到Hash Table,索引1~6结点数据依次是(G, 0)、(F, 1)、(E, 2)、(D, 3)、(C, 4)、(A, 5)。Stack1索引入口是6;

2)轮到插入Stack2,由于G、F、E、D、C结点数据跟Stack1前5结点一致,hash命中;B插入新的7号位置,(B, 5)。Stack2索引入口是7;

3)最后插入Stack3,G、F、E、D结点hash命中;但由于Stack3的A的上一个地址D索引是4,而不是已有的(A, 5),hash不命中,查找下一个空白位置8,插入结点(A, 4);B上一个地址A索引是8,而不是已有的(B, 5),hash不命中,查找下一个空白位置9,插入结点(B, 9)。Stack3索引入口是9。

经过这样的后缀压缩存储,平均栈长由原来的35缩短到5不到。而每个结点存储长度为64bits(36bits存储地址,28bits储存parent索引),hashTable空间利用率60%+,一个堆栈平均存储长度只需要66.7bytes,压缩率高达42%。

2.2.3 性能数据

经过上述优化,内存监控工具在iPhone6Plus运行占用CPU占用率13%不到,当然这是跟数据量有关,重度用户(如群过多、消息频繁等)可能占用率稍微偏高。而存储数据内存占用量20M左右,都用mmap方式把文件映射到内存。有关mmap好处可自行google之。

2.3、数据上报

由于内存监控是存储了当前所有存活对象的内存分配信息,数据量极大,所以当出现FOOM时,不可能全量上报,而是按某些规则有选择性的上报。

首先把所有对象按Category进行归类,统计每个Category的对象数和分配内存大小。这列表数据很少,可以做全量上报。接着对Category下所有相同堆栈做合并,计算每种堆栈的对象数和内存大小。对于某些Category,如分配大小TOP N,或者UI相关的(如UIViewController、UIView之类的),它里面分配大小TOP M的堆栈才做上报。上报格式类似这样:

2.4、页面展现

页面展现参考了Allocations,可看出有哪些Category,每个Category分配大小和对象数,某些Category还能看分配堆栈。

为了突出问题,提高解决问题效率,后台先根据规则找出可能引起FOOM的Category(如上面的Suspect Categories),规则有:

● UIViewController数量是否异常

● UIView数量是否异常

● UIImage数量是否异常

● 其它Category分配大小是否异常,对象个数是否异常

接着对可疑的Category计算特征值,也就是OOM原因。特征值是由“Caller1”、“Caller2”和“Category, Reason”组成。Caller1是指申请内存点,Caller2是指具体场景或业务,它们都是从Category下分配大小第一的堆栈提取。Caller1提取尽量是有意义的,并不是分配函数的上一地址。例如:

所有report计算出特征值后,可以对它们进行归类了。一级分类可以是Caller1,也可以是Category,二级分类是与Caller1/Category有关的特征聚合。效果如下。

一级分类:

二级分类:

2.5、运营策略

上面提到,内存监控会带来一定的性能损耗,同时上报的数据量每次大概300K左右,全量上报对后台有一定压力,所以对现网用户做抽样开启,灰度包用户/公司内部用户/白名单用户做100%开启。本地最多只保留最近三次数据。

三、降低误判

1)先回顾Facebook如何判定上一次启动是否出现FOOM:

a) App没有升级; b) App没有调用exit()或abort()退出; c) App没有出现crash; d) 用户没有强退App; e) 系统没有升级/重启; f) App当时没有后台运行; g) App出现FOOM。

1、2、4、5比较容易判断,3依赖于自身CrashReport组件的crash回调,6、7依赖于ApplicationState和前后台切换通知。

微信自上线FOOM数据上报以来,出现不少误判,主要情况有下面几种。

2)ApplicationState不准:

部分系统会在后台短暂唤起app,ApplicationState是Active,但又不是BackgroundFetch;执行完didFinishLaunchingWithOptions就退出了,也有收到BecomeActive通知,但很快也退出;整个启动过程持续5~8秒不等。解决方法是收到BecomeActive通知一秒后,才认为这次启动是正常的前台启动。这方法只能减少误判概率,并不能彻底解决。

3)群控类外挂:

这类外挂是可以远程控制iPhone的软件,通常一台电脑可以控制多台手机,电脑画面和手机屏幕实时同步操作,如开启微信,自动加好友,发朋友圈,强制退出微信,这一过程容易产生误判。解决方法只能通过安全后台打击才能减少这类误判。

4)CrashReport组件出现crash没有回调上层:

微信曾经在17年5月底爆发大量GIF crash,该crash由内存越界引起,但收到crash信号写crashlog时,由于内存池损坏,组件无法正常写crashlog,甚至引起二次crash;上层也无法收到crash通知,因此误判为FOOM。目前改成不依赖crash回调,只要本地存在上一次crashlog(不管是否完整),就认为是crash引起的APP重启。

5)前台卡死引起系统watchdog强杀:

也就是常见的0x8badf00d,通常原因是前台线程过多,死锁,或CPU使用率持续过高等,这类强杀无法被App捕获。为此我们结合了已有卡顿系统,当前台运行最后一刻有捕获到卡顿,我们认为这次启动是被watchdog强杀。同时我们从FOOM划分出新的重启原因叫“APP前台卡死导致重启”,列入重点关注。

四、成果显著

微信自2017年三月上线内存监控以来,解决了30多处大大小小内存问题,涉及到聊天、搜索、朋友圈等多个业务,FOOM率由17年年初3%,降到目前0.67%,而前台卡死率由0.6%下降到0.3%,效果特别明显。

五、常见问题

1)UIGraphicsEndImageContext:

UIGraphicsBeginImageContext和UIGraphicsEndImageContext必须成双出现,不然会造成context泄漏。另外XCode的Analyze也能扫出这类问题。

2)UIWebView:

无论是打开网页,还是执行一段简单的js代码,UIWebView都会占用APP大量内存。而WKWebView不仅有出色的渲染性能,而且它有自己独立进程,一些网页相关的内存消耗移到自身进程里,最适合取替UIWebView。

3)autoreleasepool:

通常autoreleased对象是在runloop结束时才释放。如果在循环里产生大量autoreleased对象,内存峰值会猛涨,甚至出现OOM。适当的添加autoreleasepool能及时释放内存,降低峰值。

4)互相引用:

比较容易出现互相引用的地方是block里使用了self,而self又持有这个block,只能通过代码规范来避免。另外NSTimer的target、CAAnimation的delegate,是对Obj强引用。目前微信通过自己实现的MMNoRetainTimer和MMDelegateCenter来规避这类问题。

5)大图片处理:

举个例子,以往图片缩放接口是这样写的:

但处理大分辨率图片时,往往容易出现OOM,原因是-[UIImage drawInRect:]在绘制时,先解码图片,再生成原始分辨率大小的bitmap,这是很耗内存的。解决方法是使用更低层的ImageIO接口,避免中间bitmap产生:

6)大视图:

大视图是指View的size过大,自身包含要渲染的内容。超长文本是微信里常见的炸群消息,通常几千甚至几万行。如果把它绘制到同一个View里,那将会消耗大量内存,同时造成严重卡顿。最好做法是把文本划分成多个View绘制,利用TableView的复用机制,减少不必要的渲染和内存占用。

六、推荐几个iOS内存技术相关的链接

● Memory Usage Performance Guidelines

https://developer.apple.com/library/content/documentation/Performance/Conceptual/ManagingMemory/ManagingMemory.html#//apple_ref/doc/uid/10000160-SW1

● No pressure, Mon!

http://www.newosxbook.com/articles/MemoryPressure.html

附录:微信、QQ文章汇总

[1] QQ、微信团队原创技术文章:微信团队原创分享:iOS版微信的内存监控系统技术实践》 《让互联网更快:新一代QUIC协议在腾讯的技术实践分享》 《iOS后台唤醒实战:微信收款到账语音提醒技术总结》 《腾讯技术分享:社交网络图片的带宽压缩技术演进之路》 《微信团队分享:视频图像的超分辨率技术原理和应用场景》 《微信团队分享:微信每日亿次实时音视频聊天背后的技术解密》 《QQ音乐团队分享:Android中的图片压缩技术详解(上篇)》 《QQ音乐团队分享:Android中的图片压缩技术详解(下篇)》 《腾讯团队分享:手机QQ中的人脸识别酷炫动画效果实现详解》 《腾讯团队分享 :一次手Q聊天界面中图片显示bug的追踪过程分享》 《微信团队分享:微信Android版小视频编码填过的那些坑》  《微信手机端的本地数据全文检索优化之路》  《企业微信客户端中组织架构数据的同步更新方案优化实战》 《微信团队披露:微信界面卡死超级bug“15。。。。”的来龙去脉》 《QQ 18年:解密8亿月活的QQ后台服务接口隔离技术》 《月活8.89亿的超级IM微信是如何进行Android端兼容测试的》 《以手机QQ为例探讨移动端IM中的“轻应用”》 《一篇文章get微信开源移动端数据库组件WCDB的一切!》 《微信客户端团队负责人技术访谈:如何着手客户端性能监控和优化》 《微信后台基于时间序的海量数据冷热分级架构设计实践》 《微信团队原创分享:Android版微信的臃肿之困与模块化实践之路》 《微信后台团队:微信后台异步消息队列的优化升级实践分享》 《微信团队原创分享:微信客户端SQLite数据库损坏修复实践》  《腾讯原创分享(一):如何大幅提升移动网络下手机QQ的图片传输速度和成功率》  《腾讯原创分享(二):如何大幅压缩移动网络下APP的流量消耗(下篇)》  《腾讯原创分享(二):如何大幅压缩移动网络下APP的流量消耗(上篇)》  《微信Mars:微信内部正在使用的网络层封装库,即将开源》  《如约而至:微信自用的移动端IM网络层跨平台组件库Mars已正式开源》  《开源libco库:单机千万连接、支撑微信8亿用户的后台框架基石 [源码下载]》  《微信新一代通信安全解决方案:基于TLS1.3的MMTLS详解》  《微信团队原创分享:Android版微信后台保活实战分享(进程保活篇)》  《微信团队原创分享:Android版微信后台保活实战分享(网络保活篇)》  《Android版微信从300KB到30MB的技术演进(PPT讲稿) [附件下载]》  《微信团队原创分享:Android版微信从300KB到30MB的技术演进》  《微信技术总监谈架构:微信之道——大道至简(演讲全文)》 《微信技术总监谈架构:微信之道——大道至简(PPT讲稿) [附件下载]》  《如何解读《微信技术总监谈架构:微信之道——大道至简》》 《微信海量用户背后的后台系统存储架构(视频+PPT) [附件下载]》 《微信异步化改造实践:8亿月活、单机千万连接背后的后台解决方案》  《微信朋友圈海量技术之道PPT [附件下载]》  《微信对网络影响的技术试验及分析(论文全文)》  《一份微信后台技术架构的总结性笔记》  《架构之道:3个程序员成就微信朋友圈日均10亿发布量[有视频]》  《快速裂变:见证微信强大后台架构从0到1的演进历程(一)》 《快速裂变:见证微信强大后台架构从0到1的演进历程(二)》  《微信团队原创分享:Android内存泄漏监控和优化技巧总结》  《全面总结iOS版微信升级iOS9遇到的各种“坑”》  《微信团队原创资源混淆工具:让你的APK立减1M》  《微信团队原创Android资源混淆工具:AndResGuard [有源码]》  《Android版微信安装包“减肥”实战记录》  《iOS版微信安装包“减肥”实战记录》  《移动端IM实践:iOS版微信界面卡顿监测方案》  《微信“红包照片”背后的技术难题》  《移动端IM实践:iOS版微信小视频功能技术方案实录》  《移动端IM实践:Android版微信如何大幅提升交互性能(一)》 《移动端IM实践:Android版微信如何大幅提升交互性能(二)》 《移动端IM实践:实现Android版微信的智能心跳机制》  《移动端IM实践:WhatsApp、Line、微信的心跳策略分析》  《移动端IM实践:谷歌消息推送服务(GCM)研究(来自微信)》 《移动端IM实践:iOS版微信的多设备字体适配方案探讨》  《信鸽团队原创:一起走过 iOS10 上消息推送(APNS)的坑》 《腾讯信鸽技术分享:百亿级实时消息推送的实战经验》 >> 更多同类文章 …… [2] 有关QQ、微信的技术故事:2017微信数据报告:日活跃用户达9亿、日发消息380亿条》 《腾讯开发微信花了多少钱?技术难度真这么大?难在哪?》 《技术往事:创业初期的腾讯——16年前的冬天,谁动了马化腾的代码》  《技术往事:史上最全QQ图标变迁过程,追寻IM巨人的演进历史》  《技术往事:“QQ群”和“微信红包”是怎么来的?》  《开发往事:深度讲述2010到2015,微信一路风雨的背后》  《开发往事:微信千年不变的那张闪屏图片的由来》  《开发往事:记录微信3.0版背后的故事(距微信1.0发布9个月时)》  《一个微信实习生自述:我眼中的微信开发团队》 《首次揭秘:QQ实时视频聊天背后的神秘组织》 >> 更多同类文章 ……

(本文同步发布于:http://www.52im.net/thread-1422-1-1.html

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏养码场

一位资深Java的阿里系公司实战面试经验,套路还是面试官的多

占小狼:一位奋斗在魔都的资深Java开发。去年6月在简书上发第一篇技术文章,已坚持发表76篇技术文章,粉丝数突破4000。

24970
来自专栏不二小段

【一起学Python】爬取前程无忧招聘信息并写入Excel

说在前面:这是依旧是来自Ricky的爬虫小实战。预告一下未来的几个主要内容吧,打算重写一下微信公众号后台,然后写几篇教程;之后是有关分词、词云、情感分析的一些内...

43760
来自专栏刘望舒

Flutter最佳入门方式:写一个计算器

工作以来,我在学习新知识的时候,常常与在应试教育阶段被传授的方法作斗争——把书本的内容100%复制到脑中才算学会吗?不会被使用或者不常用的知识,花费大量功夫去记...

16720
来自专栏企鹅号快讯

再谈前后端分离

前段时间我针对手头上的项目前端配置进行了反思以及总结并且写了两篇文章:webpack传统后端渲染的项目前端配置,webpack配置之前后端不分离, 很显然这些配...

26880
来自专栏IMWeb前端团队

JavaScript: 2015年度回顾(译文)

本文作者:IMWeb coolriver 原文出处:IMWeb社区 未经同意,禁止转载 本文为 《 2015 in review》 一文的中文译文,...

19650
来自专栏斑斓

业界时评 | Twitter如何使用Redis提高可伸缩性

最近,Twitter Cache团队的工程师Yu Yao在Youtube发表了一段演讲,介绍了Twitter如何使用Redis提高系统可伸缩性。High Sca...

40060
来自专栏机器人网

电气控制原理图讲解,一文让你完全掌握

一、继电器—接触器自动控制线路的构成 绘制原理图的基本规则 : 1)为了区别主电路与控制电路,在绘线路图时主电路用粗线表示,而控制电路用细线表示。通常习惯将主电...

32040
来自专栏程序员互动联盟

C语言和C++本质区别在哪?

疑惑一 做网站前端开发需要具备哪些基础知识? 做网站开发分为前端和后台,如果从事前端开发需要学习哪些基础知识呢?现在为大家总结一下。 html: ...

47130
来自专栏北京马哥教育

Python Web不知道怎么学?看这篇就够了!

Python的用处太多,前端、后端、数据、ML\AI、自动化等等等等。很多小白不知道学习方向导致学的东西太杂,技能范围很广但是没有高度,自己玩可以工作就完蛋。这...

443100
来自专栏ytkah

Unix时间戳转换怎样在Excel批量修改?

  最近在操作项目的时候碰到一个Unix时间戳转换的问题。"date_time":1393031347这个是什么,你知道吗?如果你对Unix时间戳了解的话一眼就...

415120

扫码关注云+社区

领取腾讯云代金券