iOS 内存管理

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_footprintresident_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

iOS物理内存示意图

活动监视器

活动监视器说明:

  • 内存: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 物理内存占用超过上限
  • 整个设备物理内存占用收到压力按照下面优先级完成清理:
  • 后台应用 > 前台应用
  • 内存占用高的应用 > 内存占用低的应用
  • 用户应用 > 系统应用

参考:iOS Jetsam 机制详解

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 命令行工具

常用工具: leaksheapvmmapmalloc_history

命令行工具查找内存问题步骤:

前置条件: 生成前后对比的 memgraph

  1. 检测是否有内存泄漏
    bash
    leaks -quiet *.memgraph

  2. 如果没有内存泄漏,通过 vmmap 查看 footprint
    bash
    vmmap *.memgraph
    vmmap --summary *.memgraph

  3. 查看 footprint 是否内存有变化,如果有明显增长,说明有内存泄漏

  4. 查看泄漏的对象
    bash
    heap -s -diffFrom=pre.memgraph post.memgraph

  5. 查看对象的地址
    bash
    heap -address='泄漏对象' post.memgraph

  6. 查看对象分配堆栈
    bash
    malloc_history -fullStacks post.memgraph 泄漏对象地址

  7. 查看对象的引用关系
    bash
    leaks -traceTree=泄漏对象地址 post.memgraph

  8. 开启 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 工具包含:LeaksAllocationsVMTracker

备注: 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。

GCD内存关系图

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

会议属性循环引用

5.2.4 BulletWindowView.m 循环引用案例

问题: 循环引用会导致它的所有成员对象都没有释放,通过工具并不好直接找到源头

代码示例:

[self.viewModel bindProperty:kWRPropInMeetingBulletBooleanAnimatableBulletHidden 
    withHandler:^(Variant *newValue, Variant *oldValue) {
        self.bulletComingView.hidden = YES;
    }];

BulletWindowView循环引用

5.2.5 其他常见问题

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

留下评论

您的电子邮箱地址不会被公开。 必填项已用 * 标注

Index