iOS OOM问题分析与优化实践
一、iOS内存机制介绍
二、 OOM介绍三、 OOM常见原因
四、 内存泄漏监控五、内存异常增长捕捉
七、展望
一. 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 常见原因
-
持久化对象
关于持久化对象这里主要指的是类似于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
FBRetainCycleDetector
检测核心代码
方案设计
因为当前所支持的功能并不完全能解决项目的现状问题,所以需要进行拓展
信息收集-
泄漏信息自动记录存储
-
每一条泄漏数据是转换成唯一的key值,所有的操作都针对于在当前key值下进行存储、更新
-
在发生泄漏问题的时候进行数据存储和转换
-
本地维护文件格式如下,记录了当前类、泄漏周期以及发生次数,最终通过该文件数据进行格式转换后导出和上传
-
本地plist统计文件的可视化导出,直观发现问题并归类
-
支持线上统计文件的上传功能,分析线上泄漏问题,进行分类并解决
五. 内存异常增长捕捉
-
FOOM 因为用户的感知更明显,所以对用户的体验的伤害更大,导致用户流失的话对业务损失更大,所以更有必要建立线上的监控手段。 -
内存占用过高即使没导致 FOOM 也可能会导致其他应用 BOOM 的概率变大,导致应用重启,对用户来说,体验是非常糟糕的。
-
bugly信息有限,并且当我们在调试阶段遇到这种崩溃的时候,从设备设置->隐私->分析与改进中是找不到普通类型的崩溃日志,所以难以定位产生问题的原因 -
没有线上监控OOM崩溃以及内存异常增长的的有效方案 -
开发过程中没有提前预防的功能和措施
Allocation
FBAllocationTracker
OOMDetector
-
监控OOM,Dump引起爆内存的堆栈 -
大内存分配监控 监控单次大块内存分配,提供分配堆栈信息
| Allocation | FBAllocationTracker | OOMDetector | |
|---|---|---|---|
| 使用场景 | 连接Mac用 | App中独立运行 | App中独立运行 |
| 监控范围 | 所有内存对象 | 只监控OC对象 | 所有内存对象 |
| 性能影响 | 性能影响高 | 性能影响低 | 性能影响低 |
方案选型
方案设计
信息收集-
记录每个页面的浏览轨迹,以及页面的内存消耗情况做自动统计,记录数据到本地文件,当发生OOM问题产生崩溃后,获取当前调用堆栈,进行数据重组,下次启动APP时进行上传操作。
目前针对于每个页面内存消耗情况,只是根据页面内存差值进行计算,后续会考虑多种因素(比如上一个页面的未完成操作导致的内存增长)再具体优化。
-
线上通过设置阈值,当超出阈值后,获取当前调用堆栈,存储到文件,下一次启动APP时进行上传操作。
-
debug模式下实时检测内存增长情况,如果当大于设定阈值的时候,需要进行异常提醒,能够在开发阶段就能避免一些内存异常增长的问题。
实时内存的更新
六. 优化成果
-
解决了代码中内存泄漏的几类问题以及超大图片加载内存暴增的问题 -
输出了统一代码规范,跟版进行常态化的跟进 -
对每个版本内存泄漏出现的概率减少60% -
单独抽离成SDK,其它App能够快速接入