物理内存
一个设备的 RAM 大小。Mac的RAM大小不固定,用户可以随便扩展。而iphone是固定不变的,以下是维基百科上的资料:
简单来说,iPhone 8(不包括 plus) 和 iPhone 7(不包括 plus)及之前都是 2G 内存,iPhone 6 和 6 plus 及之前都是 1G 内存。
虚拟内存
虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换 (iOS并无数据交换)。
从系统角度来说,每个进程都有一个自己私有的相同大小的虚拟内存空间。虚拟内存空间大小跟内存地址表示位数有关(也就是指针的位数),32位系统下为32位, 64位系统下为64位。所以32位系统的虚拟内存是4GB,而64位是 18EB(1EB = 1000PB, 1PB = 1000TB)。
操作系统的位数 >= CPU的位数,比如64位CPU上可以运行32位系统,只是没有充分利用CPU的运算能力。 而ios系统一般跟CPU保持位数一致,而iphone 5s(A7)及以后的CPU都是64位的。
系统将虚拟内存和物理内存分割成统一大小的单元,叫做页(page)。在 OS X 和早期的iOS里,页大小均为4K;之后基于A7和A8的iOS里,采用虚拟内存每页16K,物理内存每页4K;基于A9或更新CPU的iOS里,页大小均为16K.
CPU有个内存管理单元(MMU), 它维护了一张页表(page table), 可以将虚拟地址映射到物理地址。用户访问虚拟地址时,会自动被MMU转换成物理地址。当CPU访问的虚拟地址并未映射到物理地址时,CPU会触发页错误(page fault)中断, 并暂停当前执行的程序代码,然后分配一块干净的物理内存,从磁盘中加载所需的一页数据到该物理内存,同时更新页表,然后继续执行程序代码。
当进程向系统申请内存时,系统也并不会直接返回物理内存的地址,而是返回一个虚拟内存地址。仅当CPU需要访问该虚拟内存地址时,系统才会分配并映射到物理内存。
VM Object
进程的虚拟地址空间包含了多个区域(VM Region), 每个VM Region管理一段连续的虚拟内存页,这些页拥有相同的属性(如读写权限、是否是 wired,也就是是否能被 page out)。举几个例子:
- mapped file,即映射到磁盘的一个文件
- __TEXT,r-x,多数为二进制
- __DATA,rw-,为可读写数据
- MALLOC_(SIZE),顾名思义是 malloc 申请的内存
每个 VM Region 对应一个内核数据结构,名为 VM Object。Object 会记录这个 Region 内存的属性:
- Resident pages - 已经被映射到物理内存的虚拟内存页列表
- Size - 所有内存页所占区域的大小
- Pager - 用来处理内存页在硬盘和物理内存中交换问题
- Attributes - 这块内存区域的属性,比如读写的权限控制
- Shadow - 用作(copy-on-write)写时拷贝的优化
- Copy - 用作(copy-on-write)写时拷贝的优化
VM Object的Pager一般有三种类型:
- default pager 用于磁盘交换内存;
- vnode pager 用于将磁盘文件映射到内存
- device pager 将地址映射到非主存的硬件上,比如PCI内存、帧缓冲内存等,很多I/O Kit的调用背后用了这个。
除了以上Pager, VM Object还可以关联另外一个VM Object, 通过这种引用方式实现写时复用。 这种机制可以允许不同进程或同一进程里的不同代码段共享同一内存页,直到有一方试图写入数据时,才会真正的拷贝内存页。
内核常驻内存(Wired Memory)
内核常驻内存存储了内核代码和数据结构,不允许换出到磁盘上。应用(Application)、开发框架(Framework)和其他用户级别的软件无法分配内核常驻内存。然而,他们可以影响内核常驻内存的大小。举个例子,一个而应用创建了线程和端口时,会隐式的分配常驻内存来存放对应的内核资源。
- Process: 16k
- Thread: blocked in a continuation—5 k; blocked—21 k
- Mach port: 116b
- Mapping: 32 b
- Library: 2k plus 200b for each task that uses it
- Memory region: 160b
除了应用运行产生的内核常驻内存,内核自己本身也会创建一些常驻内存来存储以下实体:
- VM Objects
- the virtual memory buffer cache
- I/O buffer caches
- drivers
内核页表
内核维护了三种状态的页表:
- 活跃的内存页(active page):内存页已经被映射到物理内存中,而且近期被访问过,处于活跃状态。
- 非活跃的内存页(inactive page):内存页已经被映射到物理内存中,但是近期没有被访问过。
- 可用的内存页(free page):没有关联到虚拟内存页的物理内存页集合。
如果可用内存页(free page)数量低于一个阈值时(根据物理内存大小定义的),系统会去平衡一下这个队列。 具体就是从非活跃内存里获取。在OS X里是通过将非活跃页的数据交换到磁盘后,再将非活跃页清理并放置到可用内存页中。
Page Out 过程
在OSX上系统会将不活跃的内存块写入硬盘,一般称之为Swap out
或Page out
。
由于虚拟内存的空间远远大于物理内存,在任意一个时间点,虚拟内存中的一个页并不一定总是在物理内存中,而是可能被暂时存到了磁盘上,这样物理内存便可以暂时释放这部分空间,供优先级更高的任务使用,因此磁盘可以作为 backing store
以扩展物理内存(MacOS 中有,iOS 没有)。
具体实现是,内核遍历活跃页表和非活跃页表,并执行以下操作:
- 如果活跃页表的某页最近没有被访问过,则将它移入非活跃页表。
- 如果非活跃页表的某页最近被访问过,则内核会找出它所在的VM Object.
- 如果VM Object没有关联过pager,那就给他创建一个default pager
- VM Object的default pager尝试将该页写入磁盘交互区(backing store)
- 如果pager完成写入,内核会释放该页占用的物理内存,并将其移入可用页表里。
Page In 过程
当程序代码试图访问某个还未映射到物理内存的虚拟地址时,会触发两种内存访问错误(fault):
-
soft fault
: 要访问的页数据在物理内存里,但并未映射到当前进程的虚拟地址空间。 -
hard fault
: 要访问的页数据并未在物理内存里,可能是之前被交换到磁盘了,也可能是文件的一部分还未映射到内存。 这也是最经典的页错误(page fault)
任何一种错误发生时,内核会定位到当前region关联的VM Object,并且遍历VM Object的常驻内存(resident pages)页表,如果目标页在该表中,那么就触发soft fault
,否则触发hard fault
。
针对soft fault
,内核会将对应的物理页映射到当前进程的虚拟地址空间里,并且将该页标记为活跃。针对hard fault
,VM Object的pager将从磁盘backing store
或文件中查找对应的页数据,并将其复制到物理内存里,然后加入到活跃页表里。
Swap In/Out & Page In/Out
磁盘内部有一个区域叫做交换空间(Swap Space),MMU(内存管理单元) 会将暂时不用的内存块内容写在该交互空间上,这就是Swap Out;当需要时候再从Swap Space中读取到内存中,这就是Swap In;Swap in和swap out的操作都是比较耗时的, 频繁的Swap in和Swap out操作非常影响系统性能;
Page In/Out和 Swap In/Out 概念类似,只不过Page In/Out是将某些页的数据写到内存/从内存写回磁盘交互区;而Swap In/Out是将整个地址空间的数据写到内存/从内存写回磁盘交互区;本质都是交互机制。
-
macOS支持这类交换机制,但是iOS不支持;主要有两方面考虑:
- 移动设备的闪存读写次数有限,频繁写会降低寿命;
- 相比PC机,移动设备闪存空间有限(15年6s最小存储空间16GB、最大128GB;19年XS Max最小64GB,最大521GB)
Memory Compression
由于闪存容量和读写寿命的限制,iOS 上没有Disk swap机制,取而代之使用Memory Compression
。等压缩后内存也不够用后,iOS上则会通知App,让App清理内存,也就是我们熟知的Memory Warning
。
内存压缩技术是从 OS X Mavericks (10.9) 开始引入的(iOS 则是 iOS 7 开始),可以参考官方文档:OS X Mavericks Core Technology Overview。该技术在内存紧张时能够将最近使用过的内存占用压缩至原有大小的一半以下,并且能够在需要时解压复用。它在节省内存的同时提高了系统的响应速度,其特点可以归结为:
- 减少了不活跃内存占用
- 改善电源效率,通过压缩减少磁盘IO带来的损耗
- 压缩/解压十分迅速,能够尽可能减少 CPU 的时间开销
- 支持多核并行操作
这里要注意,对于已经被压缩过的内存,如果尝试释放其中一部分,则会先将它解压。而解压过程带来的内存增大,可能适得其反, 典型的场景就是内存告警时清理NSDictionary缓存数据。对于缓存数据或可重建数据,应尽量使用NSCache或NSPurableData,收到内存警告时,系统自动处理内存释放操作,并且是线程安全的。
内存类型
Virtual Memory
OS X虚拟内存分配大小
Virtual Memory = Dirty Memory + Clean Memory + Swapped Memory
iOS 虚拟内存分配大小
Virtual Memory = Dirty Memory + Clean Memory + Compressed Memory
获取App申请到的所有虚拟内存:
- (int64_t)memoryVirtualSize {
struct task_basic_info info;
mach_msg_type_number_t size = (sizeof(task_basic_info_data_t) / sizeof(natural_t));
kern_return_t ret = task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t)&info, &size);
if (ret != KERN_SUCCESS) {
return 0;
}
return info.virtual_size;
}
Clean Memory
能够被系统清理出内存且在需要时能重新加载数据的Page:
- 应用或系统库的二进制可执行文件
- Memory mapped files:image.jpg,.data,.model 文件等,这些文件通常是只读的,映射的内存页可以被系统直接释放掉,需需要的时候再从文件载入到内存。
- Framework:__DATA_CONST字段,但是有一点需要注意:这个字段在创建的时候是 Clean Page 类型的,但是当程序运行起来的时候,如果我们对系统方法进行了 Swizzling 就会把这个内存页变成 Dirty Page。
- malloc分配后但还未写过的内存(其实仅分配了虚拟内存,真正写时才会分配物理内存)。
Dirty Memory
主要强调不可被重复使用的内存,准确讲是如果不交换到硬盘保存状态就不能复用的内存(IOS系统根本没有swap机制)。
- 所有堆上的对象(array,uiview,string等等)。
- 图片解析缓冲(CGRasterData,imageIO)。
- Framework 的__DATA 和 __DATA_DIRTY部分
iOS中的内存警告,只会释放clean memory。因为iOS认为dirty memory有数据,不能清理。所以应尽量避免dirty memory过大
Clean和Dirty示例
int *array = malloc(20000 * sizeof(int)); // 第1步
array[0] = 32 // 第2步
array[19999] = 64 // 第3步
- 第一步,申请一块长度为80000 字节的内存空间,按照一页 16KB 来计算,就需要 6 页内存来存储。当这些内存页开辟出来的时候,它们都是 Clean 的;
- 第二步,向处于第一页的内存写入数据时,第一页内存会变成 Dirty;
- 第三步,当向处于最后一页的内存写入数据时,这一页也会变成 Dirty;
Compressed Memory
注意,用vvmap等工具测量的Compressed Memory都是压缩之前的大小
Resident Memory
已经被映射到虚拟内存中的物理内存。存在一些“非代码执行开销”,如系统和应用二进制加载的内存。
这里存在两种Resident Memory,系统的和我们APP的:
All Resident = 系统Resident + APP Resident
APP Resident = Dirty Memory + Clean Memory that loaded in pysical memory(_TEXT + _OBJC_RO + Other)
其中系统Resident
主要就是dyld_shared_cache
(动态库共享缓存),所有APP的虚拟内存对动态库的物理内存映射都是映射到这块的,我们运行不同的app查看dyld_shared_cache
的地址都是一样的:
获取App消耗的Resident Memory:
#import <mach/mach.h>
- (int64_t)memoryResidentSize {
struct task_basic_info info;
mach_msg_type_number_t size = sizeof(task_basic_info_data_t) / sizeof(natural_t);
kern_return_t ret = task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t)&info, &size);
if (ret != KERN_SUCCESS) {
return 0;
}
return info.resident_size;
}
Memory Footprint
App消耗的实际物理内存,具体定义在官方文档 Minimizing your app’s Memory Footprint 里有说明:
Refers to the total current amount of system memory that is allocated to your app.
经过测试,Xcode Debug Navigator、系统活动监控器、footprint 命令工具和代码中phys_footprint 得到的数据是一致的,都表示当前App消耗的实际物理内存。
内核代码里的注释:
/*
* phys_footprint
* Physical footprint: This is the sum of:
* + (internal - alternate_accounting)
* + (internal_compressed - alternate_accounting_compressed)
* + iokit_mapped
* + purgeable_nonvolatile
* + purgeable_nonvolatile_compressed
* + page_table
*
* internal
* The task's anonymous memory, which on iOS is always resident.
*
* internal_compressed
* Amount of this task's internal memory which is held by the compressor.
* Such memory is no longer actually resident for the task [i.e., resident in its pmap],
* and could be either decompressed back into memory, or paged out to storage, depending
* on our implementation.
*
* iokit_mapped
* IOKit mappings: The total size of all IOKit mappings in this task, regardless of
clean/dirty or internal/external state].
*
* alternate_accounting
* The number of internal dirty pages which are part of IOKit mappings. By definition, these pages
* are counted in both internal *and* iokit_mapped, so we must subtract them from the total to avoid
* double counting.
*/
App消耗的实际物理内存,包括:
- Dirty Memory
- Compressed Memory
- Page Table
- IOKit used
- NSCache, Purgeable等
通过footprint命令的输出可以知道,Footprint主要是Dirty部分(可以粗略理解二者等价),也就是我们可以控制优化的部分。注意: Resident Memory 包含了 Memory Footprint
。
获取App的Footprint:
#import <mach/mach.h>
- (int64_t)memoryPhysFootprint {
task_vm_info_data_t vmInfo;
mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
kern_return_t ret = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t)&vmInfo, &count);
if (ret != KERN_SUCCESS) {
return 0;
}
return vmInfo.phys_footprint;
}
XNU中Jetsam
判断内存过大,使用的也是phys_footprint,而非resident size。
Jetsam: 由于iOS设备不存在交换区导致的内存受限,所以iOS内核不得不把一些优先级不高或者占用内存过大的杀掉。
内存监控工具
除了系统提供的活动监视器
来观察内存,还有很多更强大的工具来定位内存问题。
vm_stat
在 macOS 上我们在终端运行 vm_stat
可以看到以下系统整体内存信息:
➜ darwin-xnu git:(master) vm_stat
Mach Virtual Memory Statistics: (page size of 4096 bytes)
Pages free: 349761.
Pages active: 1152796.
Pages inactive: 1090213.
Pages speculative: 22734.
Pages throttled: 0.
Pages wired down: 979685.
Pages purgeable: 519551.
"Translation faults": 300522536.
Pages copy-on-write: 16414066.
Pages zero filled: 94760760.
Pages reactivated: 4424880.
Pages purged: 4220936.
File-backed pages: 480042.
Anonymous pages: 1785701.
Pages stored in compressor: 2062437.
Pages occupied by compressor: 598535.
Decompressions: 4489891.
Compressions: 11890969.
Pageins: 6923471.
Pageouts: 38335.
Swapins: 87588.
Swapouts: 432061.
这个系统命令就是通过 host_statistics64()
获取的,代码参考。
这才是最终的系统内存占用情况,以 byte 为单位:
(active_count + wired_count + speculative_count + compressor_page_count) * page_size
footprint
footprint -p 14022 --wired --swapped
Xcode Navigator
初略展示了真实的物理内存消耗。颜色表明了内存占用是否合理。Xcode Navigator = footprint + 调试需要。不跟踪VM。往往初略观察App的内存占用情况,不能作为精确的参考。
Instuments Allocations
这里显示的内存,其实只是整个App占用内存的一部分,即开发者自行分配的内存,如各种类实例等。
- 主要是MALLOC_XXX, VM Region, 以及部分App进程创建的VM Region。
- 非动态的内存,及部分其他动态库创建的VM Region并不在Allocations的统计范围内。
- 主程序或动态库的_DATA数据段、Stack函数栈,并非通过malloc分配,因此不在Allocations统计内。
All Heap Allocations:
- malloc
- CFData
All Anonymous VM
无法由开发者直接控制,一般由系统接口调用申请的。例如图片之类的大内存,属于All Anonymous VM -> VM: ImageIO_IOSurface_Data,其他的还有IOAccelerator与IOSurface等跟GPU关系比较密切的.
Instruments VM Tracker
Instruments里打开Allocations
就可以看到有VM Tracker
了,记得要开启自动捕获快照后才能展示结果
上面是一个空的iOS App的VM Tracker示意图。一共有9列,下面我来一一解释它们的含义。
-
% of Res
, 当前Type的VM Regions总Resident Size占比。 -
Type
,VM Regions的Type,All和Dirty算是统计性质的Type,__TEXT表示代码段的内存映射,__DATA表示数据段的内存映射。 -
# Regs
,当前Type的VM Region总数。 -
Path
,VM Region是从哪个文件映射过来,因为有些类似于__DATA和mapped file的内存块是从文件直接映射过来的。 -
Resident Size
,使用的物理内存量。 -
Dirty Size
,使用中的物理内存块如果不交换到硬盘保存状态就不能复用,那么就是Dirty的内存块。 -
Swapped Size
, 在OSX中,不活跃的内存页可以被交换到硬盘,这是被交换的大小。在iOS中,只有非Dirty的内存页可以被交换,或者说是被卸载。 -
Virtual Size
,VM Regions所占虚拟内存的大小 -
Res. %
,Resident Size在Virtual Size中的占比
Xcode Memory Debugger
该工具可以非常方便地查看所有对象的内存使用情况、依赖关系,以及循环引用等。如果将其导出为memgraph文件,也可以使用一些命令来进行分析:
vmmap
vmmap memory-info.memgraph
-
查看摘要
vmmap --summary memory-info.memgraph
结合shell中的grep、awk等命令,可以获得任何想要的内存数据。
摘要里的字段含义跟VM Tracker里一致:
查看所有dylib的Dirty Pages的总和
vmmap -pages memory-info.memgraph | grep '.dylib' | awk '{sum += $6} END { print "Total Dirty Pages:"sum}'
查看CG image相关的内存数据
vmmap memory-info.memgraph | grep 'CG image'
heap
查看堆内存
查看Heap上的所有对象
heap memory-info.memgraph
按照内存大小来排序
heap memory-info.memgraph -sortBySize
查看某个类的所有实例对象的内存地址
heap memory-info.memgraph -addresses all | 'MyDataObject'
代码中通过malloc_size函数获取某对象占用的内存大小
malloc_size((__bridge const void *)(object))
leaks
- 查看是否有内存泄漏
leaks memory-info.memgraph
- 查看内存地址处的泄漏情况
leaks --traceTree [内存地址] memory-info.memgraph
lmalloc_history
需要开启Run->Diagnostics
中的Malloc Stack
功能,建议使用Live Allocations Only
。则lldb会记录debug过程中的对象创建的堆栈,配合malloc_history
,即可定位对象的创建过程。
malloc_history memory-info.memgraph [address]
malloc_history memory-info.memgraph --fullStacks [address]
内存分配实例
测试环境:
iPhone11模拟器+iOS14
macOS Catalina 10.15.7
Xcode 12.1
- malloc 10M内存
void *memBlock = malloc(10 * 1024 * 1024);
内存类型 | 初始大小 | 当前大小 | 增量 |
---|---|---|---|
Virtual | 5364.49M | 5374.50M | +10.01M |
Resident | 119.25M | 119.27M | +0.02M |
Footprint | 18.64M | 18.65M | +0.01M |
仅VM增大10M,即只分配了虚拟地址,并映射到物理地址
- memset 10M内存
memset(memBlock, 0, 10 * 1024 * 1024);
内存类型 | 初始大小 | 当前大小 | 增量 |
---|---|---|---|
Virtual | 5374.50M | 5374.50M | 0 |
Resident | 119.27M | 129.27M | +10M |
Footprint | 18.65M | 28.65M | +10M |
访问虚拟地址,会触发分配物理内存页。Resident和Footprint都增加。
- 分配10M虚拟内存
vm_address_t address;
vm_size_t size = 100*1024*1024;
vm_allocate((vm_map_t)mach_task_self(), &address, size, VM_MAKE_TAG(200) | VM_FLAGS_ANYWHERE);
内存类型 | 初始大小 | 当前大小 | 增量 |
---|---|---|---|
Virtual | 5610.42M | 5620.42M | +10M |
Resident | 119.16M | 119.16M | 0 |
Footprint | 18.66M | 18.66M | 0 |
- imageWithFile 加载7.2M图片
图片分辨率3502 × 1518,含透明通道,解码后的Bitmap大小应该为20.279M
NSURL* imageUrl = [[NSBundle mainBundle] URLForResource:@"big_pic" withExtension:@"png"];
UIImage* image = [UIImage imageWithContentsOfFile:imageUrl.path];
内存类型 | 初始大小 | 当前大小 | 增量 |
---|---|---|---|
Virtual | 5661.55M | 5661.55M | 0 |
Resident | 121.67M | 122.96M | +1.29M |
Footprint | 17.68M | 17.76M | +0.08M |
Resident增加了1M左右,而不是7.2M,因为文件映射内存是Clean Memory,没必要完全加载到物理内存。
- imageWithFile渲染上屏
self.imageView.image = image;
//延迟1s后统计内存,保证渲染结束
内存类型 | 初始大小 | 当前大小 | 增量 |
---|---|---|---|
Virtual | 5661.55M | 5830.41M | + 40.22M |
Resident | 122.96M | 164.43M | +41.47M |
Footprint | 17.76M | 58.87M | +41.11M |
Footprint增加的大小约为图片解码后的大小的两倍,后面会分析。
- imageWithFile的Footprint分析
分析:
图片渲染时,主要增加了两个跟解码大小接近的Dirty内存:
20 MB 0 B 1408 KB 26 CoreAnimation
20 MB 0 B 0 B 2 MALLOC_LARGE
所以Resident和Footprint(footprint命令统计)会增加40M。
退出界面后,CoreAnimation占用内存被释放, 而MALLOC_LARGE
还存在。
而Instruments的Allocations只捕获CoreAnimation的分配,说明MALLOC_LARGE是内核分配的,不在Allocations统计范围
但换了个测试环境,结果却不一样了:MALLOC_LARGE
不再增加20M,而是MALLOC_LARGE_REUSABLE
增加了20M的Clean Memory。所以猜想MALLOC_LARGE
也是系统为了复用而预分配的内存,只是由Clean Memory换成了Dirty Memory,具体意义未知。
iPhone11模拟器+iOS13.3
macOS Catalina 10.15.3
Xcode 11.3
Dirty Clean Reclaimable Regions Category
656 KB 0 B 0 B 9 MALLOC_LARGE
0 B 0 B 20 MB 1 MALLOC_LARGE_REUSABLE
- imageNamed加载7.2M图片(相同图片,不同名字)
UIImage* image = [UIImage imageNamed:@"big_pic2"];
内存类型 | 初始大小 | 当前大小 | 增量 |
---|---|---|---|
Virtual | 5679.04M | 5679.04M | 0 |
Resident | 122.01M | 123.36M | +0.04M |
Footprint | 17.91M | 18.01M | +0.01M |
- imageNamed渲染上屏
self.imageView2.image = image;
//延迟1s后统计内存,保证渲染结束
内存类型 | 初始大小 | 当前大小 | 增量 |
---|---|---|---|
Virtual | 5679.04M | 5868.07M | + 40.59M |
Resident | 123.36M | 184.90M | +40.7M |
Footprint | 18.01M | 58.91M | +20.35M |
- imageNamed的Footprint分析
分析:
图片渲染时,与ImageWithFile的消耗类似,增加了40M Dirty内存,但还多了ImageIO产生的20M可回收内存(Reclaimable,不计算到Footprint里),多的这块其实是ImageName自动解码产生的:
Dirty Clean Reclaimable Regions Category
--- --- --- --- ---
20 MB 0 B 1408 KB 26 CoreAnimation
20 MB 0 B 0 B 2 MALLOC_LARGE
0 B 0 B 20 MB 1 ImageIO
退出界面时,Footprint不降反升,CoreAnimation多消耗了2M。但再次进入同一个测试页面,CoreAnimation的消耗回到了20M。可见imageNamed
会将解码的数据缓存在CoreAnimation里,如果图片较大还是建议使用imageWithFile
参考文章
About the Virtual Memory System
iOS Memory Deep Dive
macOS 内核之内存占用信息
关于iOS内存的深入排查和优化
iOS内存分配:虚拟内存
iOS 内存管理研究
Tips for Allocating Memory
Image and Graphics Best Practices