阅读更多

0顶
0踩

移动开发

转载新闻 iOS微信内存监控

2018-03-06 10:41 by 副主编 jihong10102006 评论(0) 有9005人浏览
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的发展和前景。

  • 大教堂与集市

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

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

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

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

    大教堂与集市读后感

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

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

  • 大教堂和市集

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

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

  • 大教堂与市集

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

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

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

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

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

  • 第二章 大教堂与市集

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

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

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

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

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

  • 读 Eric S. Raymond 大教堂与市集

    “你常常在第一次实现一个解决方案之后才能理解问题所在,第二次你也许才足够清楚怎样做好它,因此如果你想做好,准备好推翻重来至少一次。”我有一句相似的话:第一次购买总会失败,所以不要指望第一次购买一种东西时买得让自己满意。 “如果你有正确的态度,有趣的问题会找上你的”我有一个发生在自己身上的联想:当我状态好时(当我有正确的态度),我会对所有事情感兴趣;相反,当我状态不好时,我对所有事情的兴趣消

Global site tag (gtag.js) - Google Analytics