阅读更多

0顶
0踩

移动开发

转载新闻 iOS微信内存监控

2018-03-06 10:41 by 副主编 jihong10102006 评论(0) 有9014人浏览
ios
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原因。微信急需一个有效的内存监控工具来发现问题。

一、实现原理

微信内存监控最初版本是使用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为参考,着重四个方面优化,分别是数据收集、存储、上报及展现。

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具体含义可以在头文件<mach/vm_statistics.h>找到;对于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.数据存储

存活对象管理

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

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

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

堆栈存储

据统计,微信运行期间,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%。

性能数据

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

3.数据上报

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

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

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有关的特征聚合。效果如下:
一级分类

二级分类

5.运营策略

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

二、降低误判

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

1.App没有升级
2.App没有调用exit()或abort()退出
3.App没有出现crash
4.用户没有强退App
5.系统没有升级/重启
6.App当时没有后台运行
7.App出现FOOM

1、2、4、5比较容易判断,3依赖于自身CrashReport组件的crash回调,6、7依赖于ApplicationState和前后台切换通知。微信自上线FOOM数据上报以来,出现不少误判,主要情况有:

ApplicationState不准

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

群控类外挂

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

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

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

前台卡死引起系统watchdog强杀

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

三、成果

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


四、常见问题

UIGraphicsEndImageContext

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

UIWebView

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

autoreleasepool

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

互相引用

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

大图片处理

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

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

大视图

大视图是指View的size过大,自身包含要渲染的内容。超长文本是微信里常见的炸群消息,通常几千甚至几万行。如果把它绘制到同一个View里,那将会消耗大量内存,同时造成严重卡顿。最好做法是把文本划分成多个View绘制,利用TableView的复用机制,减少不必要的渲染和内存占用。
  • 大小: 71.8 KB
  • 大小: 211.6 KB
  • 大小: 28.8 KB
  • 大小: 17.8 KB
  • 大小: 70.2 KB
  • 大小: 328.1 KB
  • 大小: 61.3 KB
  • 大小: 60.4 KB
  • 大小: 52.6 KB
  • 大小: 228.1 KB
  • 大小: 182 KB
  • 大小: 143.2 KB
  • 大小: 144.1 KB
  • 大小: 401.2 KB
  • 大小: 167.1 KB
  • 大小: 109.1 KB
  • 大小: 51.9 KB
  • 大小: 127.8 KB
0
0
评论 共 0 条 请登录后发表评论

发表评论

您还没有登录,请您登录后再发表评论

相关推荐

  • 大教堂与市集(中英)

    大教堂与市集(中英)--Eric Steven Raymond。讲的是有关Linux的发展和前景。

  • 《大教堂与市集》(TheCathedral and the Bazaar)全文中译版

    原文地址:http://article.yeeyan.org/view/Angelo/2005 《大教堂与市集》(TheCathedral and the Bazaar)全文中译版 埃里克·斯蒂芬·雷蒙(Eric Steven Raymond)【著】 刘安辙(Angelo Liu)【译】     译者按:本书还有一章“注释”,我为了方便阅读已经拆分加入了对应章节。此外,解

  • 大教堂与集市

    当你真的在影响历史时,那种感觉真的很不一样…1998 年 1 月 22 日,大约是我第一次发布这篇文章之后七个月,网景公司公开声明他们计划要发布通讯家族(Netscape Communicator)的原始代码,之前我对这件事情的发生一点头绪也没有。网景的执行副总裁兼首席技术官 Eric Hahn 寄给我一封电子邮件,内容简要如下︰「我代表网景公司的每一个人,向你致谢,因为你帮我们成为第一家做到开源的公司,你的思想和著作是我们做出这个决定的原因。

  • 《大教堂与集市》读后感

    大教堂与集市读后感

  • 大教堂与市集——Eric Raymond

    大教堂与市集 说明: 本文的作者Eric Raymond是Open Source Software领域的领袖,这方面许多 新的思想正是从他那儿产生的,同时他也是UNIX上最流行的Email软件Fetchmail 的作者。 下面这篇文章有点儿长,然而它是值得你去耐心把它读完的。文章以Fetchmail 为例,讨论了Internet上集市风格的开发方式。 我们应该感谢HansB,是他把这篇长文章翻译

  • 《大教堂与集市》读书笔记

    《大教堂与集市》本书讨论两种不同的自由软件开发模式: 大教堂模式(The Cathedral model)︰原始码在本模式是公开的,但在软件的每个版本开发过程是由一个专属的团队所控管的。作者以GNU Emacs及GCC这两软件为例。 市集模式(The Bazaar model)︰原始码在本模式也是公开的,不过却是放在因特网上供人检视及开发。作者以Linux核心的创始者林纳斯·托瓦兹带领Linux核...

  • 读书笔记-大教堂与集市

    内容和作者简介《大教堂与市集》(The Cathedral and the Bazaar)是埃里克·斯蒂芬·雷蒙(Eric Steven Raymond)所撰写的软件工程方法论。《大教堂与集市》是开源运动的《圣经》,颠覆了传统的软件开发思路,影响了整个软件开发领域。作者Eric S. Raymond是开源运动的旗手、***文化第一理论家,他讲述了开源运动中惊心动魄的故事,提出了大量充满智慧的观念和...

  • 《大教堂和集市》学习笔记

    开放式的文化会最终胜利,这或许不是因为"开放"在道德上正确,或者"封闭"在道德上错误,而只是因为开放式合作可以在一个问题上投入多几个数量级的技术工时,封闭的世界无法赢得这样的竞争。我们的问题是,有一个项目,方案A是精心准备后再投入使用,方案B是将半成品先公开,然后再逐步完善。而是在我看来,未来的成功者只是从自己的远见和才华开始工作,然后通过有效的社区合作,将其不断地放大。一个开放式的项目,如果加以良好的管理和运作,能取得比同等的封闭式项目大得多的成功。换句话说,结构是第一位的,功能是第二位的。

  • 卫剑钒:《大教堂与集市》被过誉了吗?

    1997 年 5 月 27 日,开源运动的领导者之一 Eric S·Raymond 发表文章,阐述了两种不同的自由软件开发模式,并将其比喻为「大教堂模式」与「集市模式」。文章一经发表便引起轰动,随后在 1999 年出版成书,这就是被称为「开源圣经」的《大教堂与集市》。作为开源运动的独立宣言,《大教堂与集市》是当代技术领域最重要的著作之一。但此书多年来与国内读者无缘,201...

  • 关于团队模式——大教堂与市集

    1. 大教堂和集市的软件发展模式:   大教堂的模型:在大教堂模式中,每一个版本的源代码都是可以被用到的,但是在不同版本间的已经开发好的代码被限制在一个专有的软件开发团队中。   集市的模式:在集市模式中,代码的开发是通过互联网以大众的视角来开发的。 2. 关于我们团队SuperBrothers的软件开发模式:   我们的团队没有像Linx核心创始者那样讲代码放在互联网上让大众来检查,虽然...

  • 大教堂和市集

    大教堂和市集  原著:Eric Raymond 翻译:HansB 一. 大教堂和市集  Linux的影响是非常巨大的。甚至在5年以前,有谁能够想象一个世界级的操作系统能够仅仅用细细的Internet连接起来的散布在全球的几千个开发人员有以业余时间来创造呢?  我当然不会这么想。在1993年早期我开始注意Linux时,我已经参与Unix和自由软件开发达十年之久了。我是八十年代中期GN

  • 大教堂与市集

    描述Linux开源世界的开发方式,和集中式开发方式的对比

  • 大教堂与市集(中文版)

    From Wikisource 中文> GNU 大教堂和市集 -- Eric Raymond, HansB翻译 目录一. 大教堂和市集 二. 邮件必须得通过 2.1 1.每个好的软件工作都开始于搔到了开发者本人的痒处。 2.2 2.好程序员知道该写什么,伟大的程序员知道该重写(和重用)什么。 2.3 3.“计划好抛弃,无论如何,你会的”(Fred Brooks,《神秘的人月》第11章) 2.4 4

  • 第二章 大教堂与市集

    Linux是颠覆性的。就是五年以前(1991),谁能想得到散布在全球各地的几千名开发者,仅靠细细的互联网连接,能够在业余时间魔术般地铸成一个世界级的操作系统呢? 反正我没想到。在1993年初Linux引起我的注意的时候,我已经在Unix和开放源代码开发领域做了十年了。我是80年代中期最早的GNU开发者之一,已经在网上发布了相当一部分软件,正在开发或协助开发好几个直到今天都在广泛使用的软件(ne

  • 开源领袖:Eric Raymond

                                             开源领袖:Eric Raymond      埃里克·斯蒂芬·雷蒙(Eric Steven Raymond)是《大教堂与市集》的作者、《新黑客词典》的维护人、著名黑客。作为《新黑客词典》的主要编撰人以及维护者,Eric Raymond很早就被认为是黑客文化的历史学家以及人类学家。但是在1997年以后,Eric Ray

  • 牛年说牛人牛事之Eric Raymond篇

    <br />转自:http://www.linuxeden.com/html/news/20090227/64298.html<br /> <br />       牛年一定要找几个牛人说说牛人牛事,今天就说说Eric Steven Raymond(埃里克·斯蒂芬·雷蒙)。 <br /><br />        埃里克·斯蒂芬·雷蒙(Eric Steven Raymond)是《大教堂与市集》的作者、《新黑客词典》的维护人、著名黑客。作为《新黑客词典》的主要编撰人以及维护者,Eric Raymond很早就被

  • Eric Raymond对于几大开发语言的评价

    【译者注】Eric Raymond是开源运动的领袖人物,对于UNIX开发有很深的造诣,主持开发了fetchmail。他的《大教堂与集市》被奉为开源运动的经典之作。下面对几大开发语言的评价非常中肯,是我近年来看到的比较出色的评论。特别是他评价中抱有的那种“简单就是好”的思想,很值得我们深思。我特别选译出一些段落,供大家阅读思考。原文参见:http://www.catb.org/~esr/writin

Global site tag (gtag.js) - Google Analytics