iOS 内存管理
1 原理
1.1 虚拟内存
虚拟内存是计算机系统内存管理的一种技术,它使得应用程序认为它拥有连续的可用的内存。
对于32位进程,可用的虚拟内存空间为4G,64位进程,可用的虚拟内存空间为16EB
各平台实际限制:
- X64 CPU 仅支持64位虚拟地址中的48位,理论上可用的虚拟内存空间为256TB
- Windows 做了进一步限制,只使用其中的44位,理论上可用的虚拟内存空间为16TB
- iOS 有效指针长度为36位,只使用了其中的33位,也就是虚拟内存空间为8G
- Android 有效指针40位,只使用了其中的39位,理论上可用的虚拟内存空间为1TB,用户空间为512G

参考:全民K歌内存篇2——虚拟内存浅析
1.2 物理内存
1.2.1 存储器的层次结构
存储器的层次结构从快到慢、从贵到便宜依次为:寄存器、高速缓存、主存、辅存。
1.2.2 iOS物理内存
内存分类:
- Free Memory:未使用的RAM容量
- Used Memory:
- Wired Memory:存放内核代码和数据结构
- Active Memory:活跃内存
- Purged Memory(也属于活跃内存):可释放内存
- Inactive Memory:不活跃内存
内存指标:
iOS 应用占用物理内存使用 phys_footprint 比 resident_size 更精准。
virtual memory = dirty memory + compressed memory + clean memory
resident memory = dirty memory + compressed memory + clean memory that loaded in physical memory
footprint memory = dirty memory + compressed memory
关系: virtual memory > resident memory > footprint memory


活动监视器说明:
- 内存:footprint 内存
- 实际内存:resident 内存
- 专用内存:通常是指进程所使用的堆和栈的大小,以及进程所加载的代码和数据的大小
1.2.3 macOS物理内存
resident memory = dirty memory + compressed memory + clean memory that loaded in physical memory
footprint memory = dirty memory + compressed memory + swap memory
在没有交换页的情况下,footprint 小于实际内存大小,在有交换页的情况下,footprint内存可能大于实际内存大小。
1.2.4 Windows物理内存
查看方式:任务管理器 -> 内存(活动的专用工作集)
1.2.5 Android物理内存
(待补充)
1.3 内存机制
1.3.1 iOS 内存机制
iOS没有内存交换机制,但有内存压缩机制。
虚拟页与物理页大小:
| 处理器 | 虚拟页 | 物理页 |
|---|---|---|
| A1~A6 | 4K | 4K |
| A7-A8 | 16K | 4K |
| A9~ | 16K | 16K |
1.3.2 Jetsam 机制
上报顺序: 先内存告警 -> 内存触顶 -> OOM线上监控
清理策略:
- 单个 App 物理内存占用超过上限
- 整个设备物理内存占用收到压力按照下面优先级完成清理:
- 后台应用 > 前台应用
- 内存占用高的应用 > 内存占用低的应用
- 用户应用 > 系统应用
OOM 监控方案:
使用 Facebook 排除法,OOM定位方案主要有:
– 线上 Allocation(OOMDetector hook 堆栈和VM 分配的方式)
– 线上 Memory Graph(抖音方案)
OOM 阈值(Android):
| 内存大小 | 代表机型 | OOM阈值(PSS+Swap)(64位) | OOM阈值(PSS+Swap)(32位) | 安全值推荐(32位) |
|---|---|---|---|---|
| 1G | vivo Y33 | — | 520~660 | 600 |
| 2G | OPPO A33 | — | 1200~1400 | 1000 |
| 3G | vivo Y66 | 1700~2050 | 1700~2050 | 1500 |
| 4G | OPPO R11 | 2400~2900 | 2400~2900 | 2000 |
| 6G | OPPO R15 | 4650~4900 | 3000+ | 2500 |
| 8G | 一加6 | 5250~5500 | — | 3000 |
OOM 阈值(iOS):
| 内存大小 | 代表机型 | OOM阈值(PSS+Swap) | 安全值推荐 |
|---|---|---|---|
| 1G | iPhone 5S | 645 | 500 |
| 2G | iPhone 6S | 1392 | 1000 |
| 2.5G | iPhone X | 1800(iOS12及以上) | 1600 |
| 3G | iPhone 8P | 2040 | 1500 |
| 4G | iPhone 11, iPhone 12 | 2050 | 1550 |
| 6G | iPhone 12 Pro | 2850 | 2000 |
Jetsam 原因:
Jetsam里面的”largestProcess”指出了最大使用内存的进程。
| 原因 | 解释 |
|---|---|
| per-process-limit | 超过每个进程的限制,不同进程的限制额度不一样,比如扩展 |
| vm-pageshortage | 虚拟内存页不足 |
| vnode-limit | 系统打开太多的文件 |
| highwater | 系统常驻进程超过内存限制 |
| fc-thrashing | 使用过多的系统文件缓存 |
| jettisoned | 其它原因 |
1.3.3 macOS 内存机制
macOS 有内存交换机制。
1.4 Zombie Objects
用于检测对象释放之后,又继续使用的问题。Address Sanitizer 相比 Zombie Objects 检测范围更广。
1.5 Address Sanitizer
1.5.1 检测如下内存使用错误
- 内存释放后又被使用
- 内存重复释放
- 释放未申请的内存
- 使用栈内存作为函数返回值
- 使用了超出作用域的栈内存
- 内存越界访问
1.5.2 使用限制
- Address Sanitizer 是运行时的能力,代码只有被运行到了才能检测出内存问题,而我们无法保证所有的代码分支和逻辑都能执行到,所以检测并不是全面的。Apple 推荐结合单元测试一起使用。
- Address Sanitizer 不能检测内存泄露、访问未初始化的内存或整形溢出
1.5.3 对性能的影响
将使代码执行效率降低2-5倍,内存使用增加2-3倍。
参考:iOS内存调试技巧
1.6 Abandoned Memory
基本原则: 重复操作回到原来状态,内存不应该有增长。
2 系统工具
参考资源:
– iOS堆内存碎片化及如何定位优化
– 深入解析iOS内存 WWDC2018 iOS Memory Deep Dive
常用命令行工具:
vmmap integrity_647.memgraph
vmmap --summary integrity_647.memgraph
leaks integrity_647.memgraph
leaks --tracetree integrity_647.memgraph
heap integrity_647.memgraph -sortBySize
malloc_history integrity_647.memgraph
2.1 内存调试技巧
针对不同问题的调试方法:
- SIGABRT:开启 Exception Breakpoint
- EXC_BAD_ACCESS:开启 Zombie Objects & Address Sanitizer
- Memory leak:使用 Debug Memory Graph
2.2 命令行工具
常用工具: leaks、heap、vmmap、malloc_history
命令行工具查找内存问题步骤:
前置条件: 生成前后对比的 memgraph
-
检测是否有内存泄漏
bash
leaks -quiet *.memgraph -
如果没有内存泄漏,通过 vmmap 查看 footprint
bash
vmmap *.memgraph
vmmap --summary *.memgraph -
查看 footprint 是否内存有变化,如果有明显增长,说明有内存泄漏
-
查看泄漏的对象
bash
heap -s -diffFrom=pre.memgraph post.memgraph -
查看对象的地址
bash
heap -address='泄漏对象' post.memgraph -
查看对象分配堆栈
bash
malloc_history -fullStacks post.memgraph 泄漏对象地址 -
查看对象的引用关系
bash
leaks -traceTree=泄漏对象地址 post.memgraph -
开启 malloc history,从命令行启动进程
bash
export MallocStackLogging=1
export MallocStackLoggingNoCompact=1
leaks -outputGraph 34.memgraph pid
open -a TencentMeeting.app
Android端命令行工具:
dumpsys meminfo 会议包名
2.3 Instruments 工具
Instruments 工具包含:Leaks、Allocations、VMTracker
备注: iOS重签名包,Leaks模板跑不起来,估计需要使用源码编译,比较麻烦。
内存泄漏检测命令:
xcrun xctrace record --template 'Leaks' --attach 6654 --device 6daac09469e94a24a2fa1f684bcd57963fd29c25 --output 700.trace --time-limit 8000ms
2.4 Xcode 工具
包括内存仪表盘、Debug Memory Graph。
2.4.1 Xcode 内存选项
| 选项 | 含义 |
|---|---|
| Malloc Scribble | libsystem_malloc.dylib 提供的功能,内存涂鸦,申请内存填0xAA,释放内存填0x55 |
| Malloc Guard Edges | 内存页保护,申请大片内存的之前或者之后都会在 page 上加保护 |
| Guard Malloc | 捕获内存的非法使用,类似 Address Sanitizer,使用libgmalloc替换libsystem_malloc.dylib,只能在模拟器使用 |
| Malloc Stack logging | 记录内存分配的堆栈 |
3 线上工具
3.1 FBAllocationTracker
原理: HOOK OC对象的分配和释放
功能: FBAllocationTracker 用于跟踪有哪些对象生成,配合 generation 可以实现切片范围对象的检测
技巧: 需要人工打点对比对象快照,可以精准的找出泄漏对象
3.2 FBRetainCycleDetector
原理: 有向图环检测,使用DFS深度优先遍历算法
作用: 检测所有OC对象的循环引用,但无法检测OC和C混用导致的循环引用
缺点: 检测所有对象,会很耗性能
难点: 如何筛选出需要检测的对象
技巧: 通过 MLeakFinder 和 FBAllocationTracker 先筛选出可能泄漏的对象,然后再使用该工具
FBMemoryProfiler: 基于 FBRetainCycleDetector 和 FBAllocationTracker 开发的循环检测工具
使用示例:
// step 1: 生成当前generation对应的切片
static NSInteger index = 0;
[[FBAllocationTrackerManager sharedManager] markGeneration];
++index;
// 通过FBAllocationTracker获取切片生成的对象
// 把获取的对象添加到FBRetainCycleDetector中,并检测是否存在循环引用的情况
FBRetainCycleDetector *detector = [FBRetainCycleDetector new];
NSArray<NSArray<FBAllocationTrackerSummary *> *> *summary =
[[FBAllocationTrackerManager sharedManager] currentSummaryForGenerations];
NSLog(@"total generation %d %d", summary.count, index);
if (index < summary.count) {
NSArray<FBAllocationTrackerSummary *> *summaries = summary[index];
for (FBAllocationTrackerSummary *obj in summaries) {
NSArray *instances = [[FBAllocationTrackerManager sharedManager]
instancesForClass:NSClassFromString(obj.className)
inGeneration:index];
NSLog(@"generation info %@ %llu", NSClassFromString(obj.className), obj.instanceSize);
for (id s in instances) {
[detector addCandidate:s];
}
}
}
NSSet *retainCycles = [detector findRetainCycles];
NSLog(@"find cycle %@", retainCycles);
3.3 MLeakFinder
原理: UIViewController 被pop或者dismiss后,很快会被释放
实现: UIViewController dismiss之后,延迟3秒之后检测弱引用指针是否还存在
作用: 可以检测ViewController和View泄漏,但对于ViewController其它成员出现的内存泄漏,则无法自动检测,需手工添加
3.4 Fishhook
原理:
- 找到
nl_symbol_ptr(got)/la_symbol_ptr数据段 - 数据段里面都是符号指针,通过符号指针找到对应的符号
- 流程:
dynamic symbol table -> symbol table -> string table
3.5 线程内存监控
原理: Hook内存分配函数,按线程统计内存的分配和释放
优化: 内存泄漏的时候开始监控
各平台分配函数:
| 平台 | 分配函数 |
|---|---|
| Android | malloc, calloc, realloc, memalign, free |
| Windows | NtAllocateVirtualMemory,NtFreeVirtualMemory |
| iOS | (待补充) |
| macOS | (待补充) |
| Linux | (待补充) |
4 内存优化
4.1 内存碎片化
优化建议:
- 尽量保证连续创建生命周期相似的对象
- 碎片化尽量降低到25%或者更少
- 使用 autorelease pool 是一种减少碎片的方式
- 长时间运行的进程尤其容易产生碎片化,多关注一下这些进程的碎片化
- 也可以使用 Instruments 的 Allocations 工具来诊断碎片化问题
4.2 XCTest 性能测试
命令示例:
# macOS 项目
xcodebuild test -project leak_mac.xcodeproj -scheme leak_mac -enablePerformanceTestsDiagnostics YES
# iOS 项目
xcodebuild test -workspace integrity.xcworkspace -scheme integrity -destination platform=iOS,name="mj-6s" -enablePerformanceTestsDiagnostics YES
5 循环引用
5.1 基本原则
Block 持有调用对象指针不一定会导致循环引用,要满足循环引用的另外一个条件是调用对象直接或者间接的持有 Block 对象。
5.2 常见案例
5.2.1 Masonry 不存在循环引用
原因: 调用对象并不持有 Block
5.2.2 GCD 不存在循环引用
原因: Queue 并不持有 Block,而是 Block 持有 Queue
在 dispatch_async 函数中追加 Block 到 Dispatch Queue 中后,该 Block 就通过 dispatch_retain 函数持有了 Dispatch Queue。

5.2.3 会议属性导致的循环引用

5.2.4 BulletWindowView.m 循环引用案例
问题: 循环引用会导致它的所有成员对象都没有释放,通过工具并不好直接找到源头
代码示例:
[self.viewModel bindProperty:kWRPropInMeetingBulletBooleanAnimatableBulletHidden
withHandler:^(Variant *newValue, Variant *oldValue) {
self.bulletComingView.hidden = YES;
}];

5.2.5 其他常见问题
- WKWebView 替代 UIWebView:解决大图片和大视图内存问题
- WMPicInPicImageView.m drawImageData:iOS渲染层异步队列堆积
- human_mask_mgr.cc OnVideoFrame:跨平台层异步队列堆积
