iOS微信内存监控

iOS微信内存监控

云加社区 2018-03-02 10:45:31

更多腾讯海量技术文章,请关注云+社区:https://cloud.tencent.com/developer

作者:杨津,腾讯移动客户端开发 高级工程师

由 WeTest质量开放平台团队 发布在云+社区

商业转载请联系腾讯WeTest获得授权,非商业转载请注明出处。

原文链接:http://wetest.qq.com/lab/view/367.html

WeTest 导读

目前iOS主流的内存监控工具是Instruments的Allocations,但只能用于开发阶段。本文介绍如何实现离线化的内存监控工具,用于App上线后发现内存问题。

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源码,无意中发现这几个接口:

iOS微信内存监控

1.png

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

iOS微信内存监控

2.png

另外为了更好的归类数据,每个内存对象应该有它所属的分类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来获取:

iOS微信内存监控

3.png

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

iOS微信内存监控

4.png

通过上面方式,我们的监控数据来源基本跟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。所以用伸展树管理最适合不过了。

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

iOS微信内存监控

5.png

堆栈存储

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

iOS微信内存监控

6.png

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

iOS微信内存监控

7.png

  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之。

iOS微信内存监控

8.png

3.数据上报

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

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

iOS微信内存监控

9.png

4.页面展现

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

iOS微信内存监控

10.png

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

● UIViewController数量是否异常

● UIView数量是否异常

● UIImage数量是否异常

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

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

iOS微信内存监控

11.png

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

一级分类

iOS微信内存监控

12.png

二级分类

iOS微信内存监控

13.png

5.运营策略

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

二、降低误判

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

iOS微信内存监控

14.jpg

  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%,效果特别明显。

iOS微信内存监控

15.png

iOS微信内存监控

16.png

四、常见问题

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来规避这类问题。

大图片处理

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

iOS微信内存监控

17.png

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

iOS微信内存监控

18.png

大视图

大视图是指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

腾讯WeTest iOS预审工具

为了提高IEG苹果审核通过率,腾讯专门成立了苹果审核测试团队,打造出iOS预审工具这款产品。经过1年半的内部运营,腾讯内部应用的iOS审核通过率从平均35%提升到90%+。

现将腾讯内部产品的过审经验,以线上工具的形式共享给各位。在WeTest腾讯质量开放平台上可以在线使用。点击 http://wetest.qq.com/product/ios 即可立即体验!

如果使用当中有任何疑问,欢迎联系腾讯WeTest企业QQ:800024531

iOS预审服务

【扫描工具】上传IPA包、图片、视频、应用描述即可进行测试; 多维度自动扫描提审材料的被拒风险;1小时内反馈全面的扫描报告。

【专家预审】腾讯专家为您遍历App所有功能模块;全面暴露App内容被拒风险;跟进问题直至上线(需提供官方拒绝邮件)。

【专家咨询】资深预审专家一对一服务; 咨询时间灵活可选,按需购买;有的放矢解 决审核问题。

【ASO优化】专业团队多维度深度剖析App的ASO现状;围绕App目标用户群筛选高 度关联的关键词;帮助提升App在苹果应用商店中的曝光率。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,522评论 25 707
  • “你,还好吗”,五大三粗的大胡子遇见多年不见的柳柳,终于憋出了一句话。 “还ok啊,你呢”,柳柳故作镇定地回道,她...
    芬兰子阅读 199评论 0 0
  • >要在人的必死性方面谋求共识,并以生命尊严和保持有意义生活作为生存追求,医患双方都面临着学习的任务。——阿图•葛文...
    二耳来了阅读 864评论 0 5
  • 伏明霞!做运动员成功,后来的生活更成功!与香港财经司长梁锦松结婚!
    悟空_e34a阅读 180评论 0 2
  • 其实我想说的是,期待经历更多的苦痛,因为现在你拼命战胜的苦痛,积攒着以后无尽的幸福。 痛苦与幸福,就如同失与得。失...
    freecourage阅读 1,897评论 0 0