一、iOS内存机制介绍
二、OOM介绍
三、OOM常见原因
四、内存泄漏监控
五、内存异常增长捕捉
六、优化成果
七、展望
在iOS开发过程或者用户反馈中,可能会经常看到这样的情况,用着用着就崩溃了,而在后台查看崩溃栈的时候,找不到崩溃日志。其实这大多数的可能是系统产生了低内存崩溃,也就是OOM;所以内存问题一直是导致系统崩溃的重要原因,绝大部分的原因可能是因为开发者在开发过程中往往会忽视内存问题,我们经常专注于使用而忘了深究,在进行深入之前我们先了解一下iOS的内存机制。
一. iOS内存机制介绍
-
虚拟内存
iOS 和大多数桌面操作系统一样,使用了虚拟内存机制,保护了每个进程的地址空间、简化了内存管理。
-
单应用可用内存有限
对于移动设备来说,受限于客观条件,物理内存容量本身就小,而 iPhone 的 RAM 本身也是偏小的,例如 iPhone XS Max 也才有 4GB,横向对比小米 9 可达 8GB,华为 P30 也是 8GB。根据 List of iPhones 可以查看历代 iPhone 的内存大小。但是与其他手机不同的是,iOS 系统给每个进程分配的虚拟内存空间非常大。据官方文档的说法,iOS 为每个 32 位的进程都会提供高达 4GB 的可寻址空间,这已经算非常大的了。
-
没有内存交换机制
虚拟内存远大于物理内存,那如果物理内存不够用了该怎么办呢?例如其他桌面操作系统(比如 OS X)有内存交换机制,在需要时能将物理内存中的一部分内容交换到硬盘上去,利用硬盘空间拓展内存空间,这也是使用虚拟内存带来的优势之一。然而 iOS 并不支持内存交换机制,大多数移动设备都不支持内存交换机制。移动设备上的大容量存储器通常是闪存(Flash),它的读写速度远远小于电脑所使用的硬盘,这就导致了在移动设备就算使用内存交换机制也并不能提升性能。其次,移动设备的容量本身就经常短缺、闪存的读写寿命也是有限的,所以这种情况下还拿闪存来做内存交换,就有点太过奢侈了。
-
内存警告
当内存不够用时,iOS 的处理是会发出内存警告,告知进程去清理自己的内存。iOS 上一个进程就对应一个 app。代码中的 didReceiveMemoryWarning() 方法就是在内存警告发生时被触发,app 应该去清理一些不必要的内存,来释放一定的空间。
-
OOM 崩溃
如果 App 在发生了内存警告,并进行了清理之后,物理内存还是不够用了,那么就会发生 OOM 崩溃,也就是 Out of Memory Crash。在 stack overflow 上,有人对单个 app 能够使用的最大内存做了统计:iOS app max memory budget。以 iPhone XS Max 为例,总共的可用内存是 3735 MB(比硬件大小小一些,因为系统本身也会消耗一部分内存),而单个 app 可用内存达到 2039 MB,达到了 55%。当 app 使用的内存超过这个临界值,就会发生 OOM 崩溃。可以看出,单个 app 的可用物理内存实际上还是很大的,要发生 OOM 崩溃,绝大多数情况下都是程序本身出了问题,分析了iOS 内存机制的特点之后,我们能够意识到合理控制 app 使用的内存是非常重要的一件事。
二. OOM 介绍
造成OOM的直接原因是iOS的 Jetsam 机制造成的,在Apple的中解释了具体的运行情况:当内存不足时,系统向当前运行中的App发起applicationDidReceiveMemoryWarning(_ application: UIApplication) 调用和 UIApplication.didReceiveMemoryWarningNotification 通知,如果内存仍然不够用则会杀掉一些后台进程,如果仍然吃紧就会杀掉当前App。OOM 分为两大类,Foreground OOM / Background OOM,简写为 FOOM 以及 BOOM。而其中 FOOM 是指 app 在前台时由于消耗内存过大,而被系统杀死,直接表现为 crash。BOOM则是由于当前设备在后台中,比如用户正在使用拍照功能进行大量的拍照和图像特效时,此时内存使用量大幅度增加,为了保证正在进行的进程有足够的内存可供使用,系统会根据优先级以及内存占用会关闭一些进程。以下内容主要是围绕FOOM介绍。
三. OOM 常见原因
-
持久化对象
关于持久化对象这里主要指的是类似于App进入后在主界面永远不会释放的对象,以及某些单例对象。基本上不kill整个app是无法释放的,但是如果因为设计原因又在首页有大量这样的持久对象那么OOM的问题理论上更加难以解决,因为此时要修改整个App结构几乎是不可能的。
-
UIWebview 缺陷
无论是打开网页,还是执行一段简单的 js 代码, UIWebView 都会占用大量内存,同时旧版本的 css 动画也会导致大 量问题,所以最好使用 WKWebView 。
-
内存泄漏
内存泄漏造成内存被持久占用无法释放,对OOM的影响可大可小,多数情况下并非泄漏的类直接造成大内存占用,而是无法释放的类引用了比较大的资源造成连锁反应最终形成OOM。
-
内存异常增长
缩放、绘制分辨率高的大图片,播放 gif 图,以及渲染本身 size 过大的视图(例如超长的 TextView)等,都会占用大量内存,有时会不恰当的操作会造成内存的异常增长出现OOM,尽管这部分内存可能一会就被释放掉,并不会长久的占用内存, 可能在解析、渲染的过程中发生 OOM。
本章内容主要从以下两个层面监控iOS OOM问题
- 内存泄漏监控
- 内存异常增长捕捉
四. 内存泄漏监控
内存泄漏(memory leak):是指申请的内存空间使用完毕之后未回收, 一次内存泄漏危害可以忽略,但若一直泄漏,无论有多少内存,迟早都会被占用光,最终导致程序crash。(因此,开发中我们要尽量避免内存泄漏的出现)
项目现状
- 需要手动去统计泄漏信息,缺少自动整合
- 内存泄漏人工排查,难发现、发现晚
- 仅限于开发环境的排查,针对线上的泄漏问题难以监控和捕捉
检测内存泄漏的方法
-
工具检测,使用 Xcode 自带的工具进行检测
-
自动化检测,自动检测出发生内存泄漏的地方,并打印出对应的信息选择 Xcode -> Product -> Profile,选择 Leaks
-
选中 Leaks,在 Leaks 所在栏中选择 CallTree
-
Call Tree 会给我们大概的位置,这个时候需要缩小范围、筛选数据
-
点击下方的 CallTree ,发现有这几个筛选项:
- Separate by Thread :按线程分开做分析,这样更容易揪出那些吃资源的问题线程。
- Invert Call Tree :反向输出调用树。把调用层级最深的方法显示在最上面,更容易找到最耗时的操作。
- Hide System Libraries:隐藏系统库文件。过滤掉各种系统调用,只显示自己的代码调用。
- Flattern Recursion:拼合递归。将同一递归函数产生的多条堆栈(因为递归函数会调用自己)合并为一条。
工具检测的不足:
- 虽然 Xcode 的 Instrucment 提供了 Leaks 和 Allocations 工具能精准地定位内存泄漏问题,但是这种方式相对比较繁琐,需要开发人员频繁地去操作应用界面,以触发泄漏场景,所以 Leaks 和 Allocations 更加适合定期组织的大排查,作为监测手段,则显得笨重
- 只能统计开发模式下的泄漏问题
方案选型
对于内存泄漏的监测,很显然Xcode自带工具在发现泄漏问题的过程中还是需要人工不断去干预,不能达到一个自动化监测的状态,所以通过结合项目本身的需求以及后期的可扩展,我们选用业内已经有了两款成熟的开源工具为 MLeaksFinder + FBRetainCycleDetector组合,并在组合的基础上做更适合当前需求的二次开发,更高效的帮助我们发现并解决项目中出现的泄漏问题。
MLeaksFinder
MLeaksFinder 是 WeRead团队开源的一款检测 iOS 内存泄漏的框架,对代码没有侵入性,而且其使用非常简单,只需要引入项目中,如果有内存泄漏,2秒后自动弹出 alert 来显示捕捉的信息。它默认只检测应用里 UIViewController 和 UIView 对象的泄漏情况。因为一般应用里内存泄漏影响最严重的就是这两种内存占用比较高的对象,它也可以在代码里设置扩展以检测其他类型的对象泄漏情况。一般情况下,当一个 UIViewController 被 pop 或者 dismiss 掉后,它的 view 和 view 的subview等也会很快地被释放掉,除非我们把它设置为单例或者还有强引用指向它。MLeaksFinder 的做法就是根据这种基本情况,在一个 UIViewController 被 pop 或者 dismiss 掉2秒后,看看它的 view 和 view 的 subview 等是否还存在,如果还存在,就意味着有可能有内存泄漏发生。具体的方法是,为基类 NSObject 添加一个方法 -willDealloc 方法,该方法的作用是,先用一个弱指针指向 self,并在一小段时间(2秒)后,通过这个弱指针调用 -assertNotDealloc,而 -assertNotDealloc 主要作用是直接弹框提醒该对象可能存在内存泄漏。
MLeaksFinder 虽然帮我们找到了内存泄漏的对象,但是我们具体不知道引起循环引用的链条,还要自己去看代码进行排查,这是很浪费时间的。因为内存泄漏一般都是循环引用导致的。
FBRetainCycleDetector
FBRetainCycleDetector是FaceBook开源的用于检测强引用循环的工具。默认是在DEBUG环境中启用,当然你也可以通过设置RETAIN_CYCLE_DETECTOR_ENABLED以始终开启。使用这个工具可以传入应用内存里的任意一个 Objective-C 对象,FBRetainCycleDetector 会查找以该对象为根节点的强引用树中有没有循环引用。
检测核心代码
这两个工具一起搭配使用很容易排查出内存的问题,先用 MLeaksFinder 找出泄漏的对象,然后再用 FBRetainCycleDetector 检测该对象有没有循环引用,如果有,根据找出来的循环引用链条去查看修改代码。
方案设计
因为当前所支持的功能并不完全能解决项目的现状问题,所以需要进行拓展
信息收集
- 泄漏信息自动记录存储
- 每一条泄漏数据是转换成唯一的key值,所有的操作都针对于在当前key值下进行存储、更新
- 在发生泄漏问题的时候进行数据存储和转换
- 本地维护文件格式如下,记录了当前类、泄漏周期以及发生次数,最终通过该文件数据进行格式转换后导出和上传
问题归类
- 本地plist统计文件的可视化导出,直观发现问题并归类
实时监控
- 支持线上统计文件的上传功能,分析线上泄漏问题,进行分类并解决
总结
内存泄露这种问题,最好在应用初期就开始着手监测和解决,否则当应用功能代码逐渐增多后,回过头来处理这种问题费时费力,还是比较麻烦的。基于 MLeaksFinder 和 FBRetainCycleDetector监测工具的基础上,结合团队业务情况,进行了一些的改造,添加了适用于当前项目的一些功能,解决了当前项目的不足,做到了自动化的监控和收集,优化了部分有问题的代码,在一定程度上提升了工具的可用性。
五. 内存异常增长捕捉
日常开发中方法使用不当或者异常大图的渲染都会产生内存异常增长的问题,而带来的影响轻则造成卡顿,重则直接发生崩溃
- FOOM 因为用户的感知更明显,所以对用户的体验的伤害更大,导致用户流失的话对业务损失更大,所以更有必要建立线上的监控手段。
- 内存占用过高即使没导致 FOOM 也可能会导致其他应用 BOOM 的概率变大,导致应用重启,对用户来说,体验是非常糟糕的。
项目现状
- bugly信息有限,并且当我们在调试阶段遇到这种崩溃的时候,从设备设置->隐私->分析与改进中是找不到普通类型的崩溃日志,所以难以定位产生问题的原因
- 没有线上监控OOM崩溃以及内存异常增长的的有效方案
- 开发过程中没有提前预防的功能和措施
为了解决当前现状的问题,所以我们需要借助一些第三方的工具来辅助我们发现和收集内存异常增长的问题,形成一套完整的线上化监控方案;所以我们调研了业界比较流行的以下几个工具。
Allocation
苹果官方提供的Allocation内存分析工具,在开发调试阶段,可以用Allocation详细分析App各模块内存占用。Allocation对App的内存监控比较全面,能监控到所有堆内存以及部分VM内存分配。
FBAllocationTracker
FBAllocationTracker是Facebook开源的内存分析工具,它的原理是用 Method Swizzling替换原本的alloc方法,这样可以在App运行时记录所有OC实例的分配信息,帮助App在运行阶段发现一些OC对象的异常增长问题。相比Allocation,FBAllocationTracker对App性能影响较低,可以在App中独立运行。
OOMDetector
OOMDetector是腾讯研发的一个内存监控组件。通过Hook系统底层的内存分配方法,能够记录到进程所有内存分配的堆栈信息,同时组件能够在对性能流畅度影响不大的情况下保证在App中独立运行,可以方便分析和监控线上用户的内存问题。
- 监控OOM,Dump引起爆内存的堆栈
- 大内存分配监控 监控单次大块内存分配,提供分配堆栈信息
|
| Allocation | FBAllocationTracker | OOMDetector |
| --- | --- | --- | --- |
| 使用场景 | 连接Mac用 | App中独立运行 | App中独立运行 |
| 监控范围 | 所有内存对象 | 只监控OC对象 | 所有内存对象 |
| 性能影响 | 性能影响高 | 性能影响低 | 性能影响低 |
方案选型
通过结合项目本身的需求以及工具的优缺点,选用方案为OOMDetector工具,除了对比的三个维度的优势外,该工具对堆栈的回溯进行了优化,耗时低于1us。
方案设计
信息收集
-
记录每个页面的浏览轨迹,以及页面的内存消耗情况做自动统计,记录数据到本地文件,当发生OOM问题产生崩溃后,获取当前调用堆栈,进行数据重组,下次启动APP时进行上传操作。
目前针对于每个页面内存消耗情况,只是根据页面内存差值进行计算,后续会考虑多种因素(比如上一个页面的未完成操作导致的内存增长)再具体优化。
实时监控
- 线上通过设置阈值,当超出阈值后,获取当前调用堆栈,存储到文件,下一次启动APP时进行上传操作。
线下预防
-
debug模式下实时检测内存增长情况,如果当大于设定阈值的时候,需要进行异常提醒,能够在开发阶段就能避免一些内存异常增长的问题。
实时内存的更新
通过对以上两个工具的拓展开发,为了后期维护和接入,我们把两个工具整合成一个独立的模块,模块结构如下:
六. 优化成果
- 解决了代码中内存泄漏的几类问题以及超大图片加载内存暴增的问题
- 输出了统一代码规范,跟版进行常态化的跟进
- 对每个版本内存泄漏出现的概率减少60%
- 单独抽离成SDK,其它App能够快速接入
七. 展望
会对更多产生异常崩溃的场景进行监控,例如卡死崩溃(watchdog),这类因为进程外的指令强制退出导致的异常,原有的监控原理是覆盖不到的,导致此类问题长时间被忽略,所以下一个优化方向是针对卡死崩溃的发现以及治理。
网友评论