赞
踩
同一个app, 没有任何改动, 使用xcode查看内存占用是118M, 而使用Instuments Allocations 查看是153M, 为什么会不一样呢?
虚拟内存机制,主要包括内存管理单元MMU、内存映射、分段、分页。在iOS中,一页通常有16KB的内存空间。
分配内存的时候,先分配虚拟内存,然后使用的时候再映射到实际的物理内存。
一个VM Region指的是一段连续的虚拟内存页,这些页的属性都相同。
- /* localized structure - cannot be safely passed between tasks of differing sizes */
- /* Don't use this, use MACH_TASK_BASIC_INFO instead */
- struct task_basic_info {
- integer_t suspend_count; /* suspend count for task */
- vm_size_t virtual_size; /* virtual memory size (bytes) */
- vm_size_t resident_size; /* resident memory size (bytes) */
- time_value_t user_time; /* total user run time for
- * terminated threads */
- time_value_t system_time; /* total system run time for
- * terminated threads */
- policy_t policy; /* default policy for new threads */
- };
-
- struct mach_task_basic_info {
- mach_vm_size_t virtual_size; /* virtual memory size (bytes) */
- mach_vm_size_t resident_size; /* resident memory size (bytes) */
- mach_vm_size_t resident_size_max; /* maximum resident memory size (bytes) */
- time_value_t user_time; /* total user run time for
- * terminated threads */
- time_value_t system_time; /* total system run time for
- * terminated threads */
- policy_t policy; /* default policy for new threads */
- integer_t suspend_count; /* suspend count for task */
- };

VM分为Clean Memory和Dirty Memory。即:
虚拟内存 Virtual Memory = Dirty Memory + Clean Memory + Compressed Memory。
使用malloc函数,申请一段堆内存,则该内存为Clean的。一旦写入数据,通常这块内存会变成Dirty。
获取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;
- }
mach_task_self()表示获取当前的Mach task。
可以简单理解为能够被写入数据的干净内存。对开发者而言是read-only,而iOS系统可以写入或移除。
注意:如果通过文件内存映射机制memory mapped file载入内存的,可以先清除这部分内存占用,需要的时候再从文件载入到内存。所以是Clean Memory。
主要强调不可被重复使用的内存。对开发者而言,已经写入数据。
iOS中的内存警告,只会释放clean memory。因为iOS认为dirty memory有数据,不能清理。所以,应尽量避免dirty memory过大。
Tis: Frameworks you link actually use clean memory and dirty memory.
Frameworks框架占用clean memory 也占用dirty memory.
要清楚地知道Allocations和Dirty Size分别是因为什么?
下方有测量实验,如+50dirty的操作,在release环境不生效,因iOS系统自动做了优化。
iOS设备没有swapped memory,而在iOS7之后采用Compressed Memory机制,一般情况下能将目标内存压缩至原有的一半以下。对于缓存数据或可重建数据,尽量使用NSCache或NSPurableData,收到内存警告时,系统自动处理内存释放操作。并且是线程安全的。
首先,针对那些有一段时间没有被访问的dirty pages(多个page),内存压缩器会对其进行压缩。但是,在这块内存再次被访问时,内存压缩器会对它解压以正确的访问。举个例子,某个Dictionary使用了3个page的内存,如果一段时间没有被访问同时内存吃紧,则系统会尝试对它进行压缩从3个page压缩为1个page从而释放出2个page的内存。但是如果之后需要对它进行访问,则它占用的page又会变为3个。
这里要注意,压缩内存机制,使得内存警告与释放内存变得稍微复杂一些。即,对于已经被压缩过的内存,如果尝试释放其中一部分,则会先将它解压。而解压过程带来的内存增大,可能得到我们并不期待的结果。如果选用NSDictionary之类的,内存比较紧张时,尝试将NSDictionary的部分内存释放掉。但若NSDictionary之前是压缩状态,释放需要先解压,解压过程可能导致内存增大而适得其反。
所以,我们平常开发所关心的内存占用其实是 Dirty Size和Compressed Size两部分,也应尽量优化这两部分。而Clean Memory一般不用太多关注。
已经被映射到虚拟内存中的物理内存。而phys_footprint才是真正消耗的物理内存。
Resident Memory = Dirty Memory + Clean Memory that loaded in pysical memory。
获取App消耗的Resident Memory:
- - (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;
- }
- /*
- * 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消耗的实际物理内存,包括:
获取App的Footprint:
- - (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。phys_footprint是我们关注的重点.
获取设备的所有物理内存大小,可以使用
[NSProcessInfo processInfo].physicalMemory
iPhone 7, iOS 13.3。
初始状态
类型 | 内存值(MB) | 分析 |
---|---|---|
resident | 59 | App消耗的内存 |
footprint | 13 | 实际物理内存 |
VM | 4770 | App分配的虚拟内存 |
Xcode Navigator | 14.3 | footprint + 调试需要 |
加50MB的clean memory
代码为:
__unused char *buf = malloc(50 * 1024 * 1024);
类型 | 内存值(MB) | 增量 | 分析 |
---|---|---|---|
resident | 60 | +1 | App消耗的内存 |
footprint | 14 | +1 | 实际物理内存 |
VM | 4817 | +47 | App分配的虚拟内存 |
Xcode Navigator | 14.3 | +0 | footprint + 调试需要 |
实际,仅增加50MB的VM,而这里额外会有1~2MB的footprint增加,猜测是用于内存映射所需的。
到达虚拟内存上限会报错: error: can't allocate region,但不会导致崩溃***。
同时,申请的过程不会耗时。
再加50MB的clean memory
类型 | 内存值(MB) | 增量 | 分析 |
---|---|---|---|
resident | 60 | +0 | App消耗的内存 |
footprint | 14 | +0 | 实际物理内存 |
VM | 4868 | +51 | App分配的虚拟内存 |
Xcode Navigator | 14.3 | +0 | footprint + 调试需要 |
Resident、footprint、VM都增加。是实实在在的内存消耗,各个工具都会统计。
初始状态
类型 | 内存值(MB) | 分析 |
---|---|---|
resident | 59 | App消耗的内存 |
footprint | 13 | 实际物理内存 |
VM | 4769 | App分配的虚拟内存 |
Xcode Navigator | 14.3 | footprint + 调试需要 |
加50MB的dirty memory
代码为:
- // 仅此一句,依然是仅申请虚拟内存,物理内存不会变
- char *buf = malloc(50 * 1024 * 1024 * sizeof(char));
- // 内存使用了,所以是实际的物理内存被使用了。即内存有数据了,变成dirty memory。
- for (int i = 0; i < 50 * 1024 * 1024; i++) {
- buf[i] = (char)rand();
- }
类型 | 内存值(MB) | 增量 | 分析 |
---|---|---|---|
resident | 110 | +51 | App消耗的内存 |
footprint | 64 | +51 | 实际物理内存 |
VM | 4817 | +48 | App分配的虚拟内存 |
Xcode Navigator | 64.4 | +50.1 | footprint + 调试需要 |
实际增加了50MB的物理内存,Resident Memory也会变化,同时额外多了1~2MB。
申请过程比较耗时,超出上限会导致崩溃。
但该操作仅在debug下生效,release环境不生效,应该是iOS系统自行的优化。
再加50MB的dirty memory
类型 | 内存值(MB) | 增量 | 分析 |
---|---|---|---|
resident | 160 | +50 | App消耗的内存 |
footprint | 114 | +50 | 实际物理内存 |
VM | 4868 | +51 | App分配的虚拟内存 |
Xcode Navigator | 114.4 | +50 | footprint + 调试需要 |
初始状态
类型 | 内存值(MB) | 分析 |
---|---|---|
resident | 59 | App消耗的内存 |
footprint | 13 | 实际物理内存 |
VM | 4770 | App分配的虚拟内存 |
Xcode Navigator | 14.3 | footprint + 调试需要 |
加50MB的clean memory,使用其中10MB
代码为:
- // 申请50MB的虚拟内存
- char *buf = malloc(50 * 1024 * 1024 * sizeof(char));
- // 实际只用了10MB,所以10MB的dirty memory
- for (int i = 0; i < 10 * 1024 * 1024; i++) {
- buf[i] = (char)rand();
- }
类型 | 内存值(MB) | 增量 | 分析 |
---|---|---|---|
resident | 70 | +11 | App消耗的内存 |
footprint | 24 | +11 | 实际物理内存 |
VM | 4817 | +47 | App分配的虚拟内存 |
Xcode Navigator | 24.3 | +10 | footprint + 调试需要 |
申请了50MB,但实际仅使用了10MB,因此只有这10MB为Dirty Memory。
再加50MB的clean memory,使用其中10MB
类型 | 内存值(MB) | 增量 | 分析 |
---|---|---|---|
resident | 80 | +10 | App消耗的内存 |
footprint | 34 | +10 | 实际物理内存 |
VM | 4868 | +51 | App分配的虚拟内存 |
Xcode Navigator | 34.3 | +10 | footprint + 调试需要 |
初始状态
类型 | 内存值(MB) | 分析 |
---|---|---|
resident | 59 | App消耗的内存 |
footprint | 13 | 实际物理内存 |
VM | 4770 | App分配的虚拟内存 |
Xcode Navigator | 14.3 | footprint + 调试需要 |
加100MB的VM
代码为:
- vm_address_t address;
- vm_size_t size = 100*1024*1024;
- // VM Tracker中显示Memory Tag 200
- vm_allocate((vm_map_t)mach_task_self(), &address, size, VM_MAKE_TAG(200) | VM_FLAGS_ANYWHERE);
- // VM Tracker中显示VM_MEMORY_MALLOC_HUGE
- // vm_allocate((vm_map_t)mach_task_self(), &address, size, VM_MAKE_TAG(VM_MEMORY_MALLOC_HUGE) | VM_FLAGS_ANYWHERE);
类型 | 内存值(MB) | 增量 | 分析 |
---|---|---|---|
resident | 60 | +1 | App消耗的内存 |
footprint | 14 | +1 | 实际物理内存 |
VM | 4867 | +97 | App分配的虚拟内存 |
Xcode Navigator | 14.3 | +0 | footprint + 调试需要 |
这里,mach_task_self()表示在自己的进程空间内申请,size的单位是byte。使用参数VM_MAKE_TAG(200)给申请的内存提供一个Tag标记,该数字在VM Tracker中会有标记。
再加100MB的VM
类型 | 内存值(MB) | 增量 | 分析 |
---|---|---|---|
resident | 60 | +0 | App消耗的内存 |
footprint | 14 | +0 | 实际物理内存 |
VM | 4967 | +100 | App分配的虚拟内存 |
Xcode Navigator | 14.3 | +0 | footprint + 调试需要 |
图片大小:map.jpg: 9054*5945
初始状态
类型 | 内存值(MB) | 分析 |
---|---|---|
resident | 60 | App消耗的内存 |
footprint | 14 | 实际物理内存 |
VM | 4768 | App分配的虚拟内存 |
Xcode Navigator | 14.3 | footprint + 调试需要 |
self.image = [UIImage imageNamed:@"map.jpg"]
类型 | 内存值(MB) | 增量 | 分析 |
---|---|---|---|
resident | 61 | +2 | App消耗的内存 |
footprint | 14 | +0 | 实际物理内存 |
VM | 4768 | +0 | App分配的虚拟内存 |
Xcode Navigator | 14.4 | +0.1 | footprint + 调试需要 |
构建UIImage对象所需要的图片数据消耗其实不大。这里的数据指的是压缩的格式化数据。
self.imageView.image = self.image;
类型 | 内存值(MB) | 增量 | 分析 |
---|---|---|---|
resident | 61 | +0 | App消耗的内存 |
footprint | 92 | +78 | 实际物理内存 |
VM | 4845 | +77 | App分配的虚拟内存 |
Xcode Navigator | 92 | +77.6 | footprint + 调试需要 |
这个阶段,需要将图片数据解码成像素数据bitmap,并渲染到屏幕上。解码过程非常消耗内存和CPU资源,且默认在主线程中执行会阻塞主线程。
关于这里的一些详细信息及优化(如异步解码图片数据,主线程渲染),请看后文。
通过以上的比较,可以对各个内存类型有一个初步直观的认识。
初略展示了真实的物理内存消耗。颜色表明了内存占用是否合理。Xcode Navigator = footprint + 调试需要。不跟踪VM。往往初略观察App的内存占用情况,不能作为精确的参考。
这里显示的内存,其实只是整个App占用内存的一部分,即开发者自行分配的内存,如各种类实例等。简单而言,就是开发者自行malloc申请的。
Allocations中主要包含的是所有MALLOC_XXX方法申请的内存, VM Region和部分App进程创建的VM Region。
注意: 非动态的内存,以及部分其他动态库创建的VM Region并不在Allocations的统计范围内。比如主程序或者动态库的_DATA数据段,这些数据内存区域并非通过malloc分配,也就没有统计在All Heap Allocations中,所以你会发现All Heap Allocations往往会比较小。除非你自行使用malloc系列方法创建大内存块,否则很难看到All Heap Allocations有一个大的数值。
我们在实际的App中,大的内存占用一般都是类似于WebKit,ImageIO,CoreAnimation等虚拟内存区域(VM Region),这些VM Region一般由系统代码生成和管理,我们编写的代码如果间接引用了这些内存而没有释放,也就会造成大面积的内存泄漏。
总结一下Allocations统计的内容:
All Heap Allocations
Malloc
开发者手动分配的堆区域内存块,比如一些人脸检测模型等,还有一些C/C++代码中的。
All Anonymous VM
无法由开发者直接控制,一般由系统接口调用申请的。例如图片之类的大内存,属于All Anonymous VM -> VM: ImageIO_IOSurface_Data,其他的还有IOAccelerator与IOSurface等跟GPU关系比较密切的.
以下内容参考自:iOS 内存管理研究,总结得非常到位了。
- (CGImage是一个可以惰性初始化(持有原始压缩格式DataBuffer),并且通过类似引用计数管理真正的Image Bitmap Buffer的设计,
- 只有渲染时通过RetainBytePtr拿到Bitmap Buffer塞给VRAM(IOSurface),不渲染时ReleaseBytePtr释放Bitmap Buffer,DataBuffer占用本身就小)。
- 通常我们使用UIImageView,系统会自动处理解码过程,在主线程上解码和渲染,会占用CPU,容易引起卡顿。
- 推荐使用ImageIO在后台线程执行图片的解码操作(可参考SDWebImageCoder)。但是ImageIO不支持webp。
-
- ASDK的原理:拿空间换时间,换取流畅,牺牲内存,但内存开销比UIKit高。
- 正常用一个全屏的UIImageView,直接用image = UIImage(named:xxx)来设置图片,要在主线程解码,但消耗内存反而较小,只有4MB(正常需要10MB)。
- 应该是IOSurface对图片数据做了一些优化。但如果是非常大的图片就会阻塞,不建议直接渲染。
- CGImage是一个可以惰性初始化(持有原始压缩格式DataBuffer),并且通过类似ARC管理真正的Image Bitmap Buffer的设计。
- 只有渲染时候通过RatainBytePtr拿到Bitmap Buffer塞给VRAM(IOSurface),不渲染时ReleaseBytePtr释放Bitmap Buffer,DataBuffer本身占用很小。
- 复制代码
VM: Stack
调用堆栈,一般不需要做啥。每个线程都需要500KB左右的栈空间,主线程1MB。
VM: CG raster data
SDWebImage的图片解码数据的缓存,为了避免渲染时在主线程解码导致阻塞。如果对于这一点比较介意,可以做相应设置即可, 但是不推荐这样设置, 频繁降低性能
常见堆栈:
- mmap
- CGDataProvicerCreateWithCopyOfData
- CGBitmapContextCreateImage
- [SDWebImageWebPCoder decodedImageWithData:]
- [SDWebImageCodersManager decodedImageWithData:]
- [SDImageCache diskImageForKey:data:options:]
- [SDImageCache queryCacheOperationForKey:options:done:]_block_invoke
interesting VM regions such as graphics- and Core Data-related. Hides mapped files, dylibs, and some large reserved VM regions.
比较大块的内存占用,如WebKit、ImageIO、CoreAnimation等VM Region,一般由系统生成和管理。
其他比如MALLOC_LARGE,MALLOC_NANO等都是申请VM的时候设置的tag。
分析一个VM Tracker的截图
例如:
Type All 那一行说明:
VM Tracker中的内存Type
VM_Tracker如何识别出每个内存块的Type?答案即为vm_allocate函数调用时的最后一个参数flags。如MALLOC_TINY, MALLOC_SMALL, MALLOC_LARGE, ImageIO等。 vm_allocate((vm_map_t)mach_task_self(), &address, size, VM_MAKE_TAG(200) | VM_FLAGS_ANYWHERE); VM_FLAGS_ANYWHERE是flags中控制内存分配方式的flag,表示可以接受任意位置。
- #define VM_FLAGS_FIXED 0x0000
- #define VM_FLAGS_ANYWHERE 0x0001
- #define VM_FLAGS_PURGABLE 0x0002
- #define VM_FLAGS_4GB_CHUNK 0x0004
- #define VM_FLAGS_RANDOM_ADDR 0x0008
- #define VM_FLAGS_NO_CACHE 0x0010
- #define VM_FLAGS_RESILIENT_CODESIGN 0x0020
- #define VM_FLAGS_RESILIENT_MEDIA 0x0040
- #define VM_FLAGS_OVERWRITE 0x4000 /* delete any existing mappings first */
- 复制代码
- 即 2个字节就可存储该flag,而int4个字节的剩下两个就可用于存储标记内存类型的Type了。
- VM_MAKE_TAG可快速设置Type。
- #define VM_MAKE_TAG(tag) ((tag) << 24)
- 将值左移24个bit,即3个字节,则一个字节表示内存类型。
-
- 苹果内置的Type有:
- #define VM_MEMORY_MALLOC 1
- #define VM_MEMORY_MALLOC_SMALL 2
- #define VM_MEMORY_MALLOC_LARGE 3
- #define VM_MEMORY_MALLOC_HUGE 4
- #define VM_MEMORY_SBRK 5// uninteresting -- no one should call
- #define VM_MEMORY_REALLOC 6
- #define VM_MEMORY_MALLOC_TINY 7
- #define VM_MEMORY_MALLOC_LARGE_REUSABLE 8
- #define VM_MEMORY_MALLOC_LARGE_REUSED 9
- 所以,这个地方的Type即为VM Tracker中显示的Type。
- 而设置自己的数字也是为了快速定位到自己的虚拟内存。
- 复制代码

该工具可以非常方便地查看所有对象的内存使用情况、依赖关系,以及循环引用等。如果将其导出为memgraph文件,也可以使用一些命令来进行分析:
不过在开始之前我们首先需要开启Scheme -> Run -> Diagnostics -> Malloc Stack选项, 这样才能在右侧看到具体的malloc 堆栈信息。
运行起来后, 选择Debug Memory Graph
右侧堆栈信息只有在打开 上面的Malloc Stack选项才能看到
想要把本次的数据导出, 选择file -> Export Memory Graph
vmmap
- vmmap memory-info.memgraph
- # 查看摘要
- vmmap --summary memory-info.memgraph
结合shell中的grep、awk等命令,可以获得任何想要的内存数据。
- # 查看所有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'
leaks
- # 查看是否有内存泄漏
- leaks memory-info.memgraph
- # 查看内存地址处的泄漏情况
- leaks --traceTree [内存地址] memory-info.memgraph
malloc_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]
通常情况下,
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。