先来看下苹果文档:
- Memory Management Programming Guide for Core Foundation
- Advanced Memory Management Programming Guide
- Memory Usage Performance Guidelines
Objective-C提供两种方式的内存管理方式:
- 手动管理(“manual retain-release” or MRR)
- 自动引用计数方式(Automatic Reference Counting, or ARC)
内存管理一般出现的问题:
Freeing or overwriting data that is still in use
This causes memory corruption, and typically results in your application crashing, or worse, corrupted user data.Not freeing data that is no longer in use causes memory leaks
A memory leak is where allocated memory is not freed, even though it is never used again. Leaks cause your application to use ever-increasing amounts of memory, which in turn may result in poor system performance or your application being terminated
内存管理的基本法则:
- You own any object you create(自己生成的对象,自己持有)
- You can take ownership of an object using retain(非自己生成的对象,自己也能持有)
- When you no longer need it, you must relinquish ownership of an object you own(不再需要自己持有的对象时释放)
- You must not relinquish ownership of an object you do not own(无法释放非自己持有的对象)
去年参加了一个技术分享,讲的是iOS内存管理及优化
引子就很吸引人:
- 桌面系统中很少有应用因为使用内存过多而被Kill掉,为啥iOS会呢?
- 虚拟内存为何物?为啥有时它能超过物理总内存?虚拟内存占用过高会引来内存警告吗?
- Allocations中的Dirty Size和Resident Size分别指的是什么?All Heap & Anonymous VM是什么?
- iOS内存管理机制是什么样的?它基于什么原则来Kill掉进程的?
- 内存有分类吗?什么类型的内存可以回收?
- 我们了解自己的程序吗?什么地方占用内存多,什么地方可以优化?如何避免内存峰值过高?
程序员对内存的关注点:
正确使用(1.非法访问 2.内存泄露)
高效使用(1.降低内存峰值 2.处理内存警告 3.Cache)
-
内存管理的历史
-
逻辑地址 VS 物理地址
程序访问的都是逻辑地址,逻辑地址需要经过转换之后才能访问物理地址。CPU访问先通过界限寄存器的对比,如果越界就报越界错误,否则加上基址寄存器的值,然后构成物理地址.
- Swap
当物理内存不够用时,可以将不用的进程放到磁盘去,腾出内存空间给新的进程.相当于通过通过辅存(磁盘)来扩充实际的物理内存.
- Swap
-
-
虚拟地址
虚拟地址相当于逻辑地址.虚拟地址到物理地址是通过CPU内部的内存管理单元(MMU)处理.32位系统,虚拟地址为4GB,64位系统为16GGB.
-
段式虚拟内存
以前的内存分配空间为整个进程空间,现在可以将其分为小单位的段以提高利用率,以前的连续分配改为离散分配,以前的无权限分区改为按逻辑分配权限.
段式虚拟内存的转换过程分为两部分:段号和段内偏移.系统有一个全局的段表.先通过段号去段表里查基值和界限,然后加载到基址寄存器和界限寄存器上.然后在经过转换访问物理地址.
- 页式虚拟内存
段式虚拟内存分配的最小单位是段,但相对来讲还是比较大(几兆).段与段之间可能会产生外部碎片.页式虚拟内存用来解决外部碎片,将虚拟地址和物理地址划分成等大小的页框(4KB或8KB,iOS中为4KB).通过等大小的页框来做映射关系.可以理解为段式虚拟内存的特例,所有段都等大小。有个特点就是页错误,当访问物理地址中的一个没有做映射的地址时会触发一个中断,操作系统会接管这个中断,将这个页在辅存中的内容读取到物理页,然后在建立映射关系,然后再恢复现场,程序无感知.
-
程序内存分布
可执行文件里面有个头,里面记录着所有段的大小,进程加载器会根据头将各个段加载到物理内存去,比如代码段和数据段,有些段在可执行文件里面只是个占位符,实际加载到虚拟内存上才会分配内存.数据段是初始过的全局变量和静态变量.未初始化的全局变量和静态变量放在bss段.堆从地地址到高地地址,一般用malloc分配.栈从高地址到低地址,用来存储局部变量或者函数调用时候用到. -
iOS中的内存段
- _PAGEZERO 固定分配在零地址,一个页大小.没有访问权限,用于零地址出发exception.
- _TEXT 代码段
- _DATA 数据段
- __MALLOC_TINY 堆地址,和以下两个只是大小区别,小于一个页大小,分配到TINY段里面
- __MALLOC_SMALL 大于一个页小于一兆
- __MALLOC_LARGE 大于一兆
关于iOS的内存分类可以阅读 Finding iOS memory
-
iOS内存管理
iOS使用全功能的内存管理模式,有端式和页式. 桌面系统中很少有应用因为使用内存过多而被Kill掉,为啥iOS会呢?
因为iOS上没有Swap机制.(1.移动设配的闪存容量有限2.闪存的写次数有限,频繁写会降低寿命)思考:代码是要加载到内存执行的,如果没有Swap机制那代码很大的程序岂不是很占内存?
-
低内存处理机制Jetsam
- 基于优先队列,从上往下优先级越高.
当系统内存过低时就会广播消息,大家尽量去释放内存.过一段时间后,内存还不够用时,就是从上往下Kill进程. - UIKit提供三种通知方式:
- [UIApplicationDelegate applicationDidReceiveMemoryWarning:]
- [UIViewController didReceiveMemoryWarning:]
- UIApplicationDidReceiveMemoryWarningNotification
- 内存警告消息来自主线程,应避免主线程这时候卡顿后者分配过大内存或者快速分配(腾讯Buddly可以检测主线程卡顿)
- 如果App因为内存警告被Kill掉,会生成LowMemory***.log
- 基于优先队列,从上往下优先级越高.
-
内存的分类
- Clean Memory 在闪存中有备份,能再次读取重建
- Code,framework,memory-mapped files
- Dirty Memory 所有非Clear Memory,系统无法回收
- Heap allocations,decompressed images,caches
例子:
- Heap allocations,decompressed images,caches
- Clean Memory 在闪存中有备份,能再次读取重建
NSString *str1 = [NSString stringWithString:@"Welcome!"]; //堆分配的内存 Dirty Memory
NSString *str2 = @"Welcome!"; //常量字符串,存放在一个只读数据段里面,这段内存释放后,还可以在读取重建 Clear Memory
char *buf = malloc(100 * 1024 *1024); // Clear Memory分配100M虚拟内存,当没有用时没有建立映射关系
for (int i = 0; i < 3 * 1024 * 1024; ++i) {
buf[i] = rand();
}
关于Clear Memory和Dirty Memory的介绍 [What is resident and dirty memory of iOS?](http://stackoverflow.com/questions/13437365/what-is-resident-and-dirty-memory-of-ios)
* Dirty & Resident & Virtual Memory
* 虚拟内存层面
* Virtual Memory = Clear Memory + Dirty Memory
* 物理内存层面
* Resident Memory = Clean Memory(Loaded in Physical Memory) + Dirty Memory
* 物理页面的生命周期
根据状态来划分
* Free(空闲) 物理页没被任何虚拟内存使用
* Active(活跃) 物理页正用于一个虚拟内存页,并且最近被引用过,这种页面一般不会被交换出去
* Inactive(非活跃)物理页正用于一个虚拟内存页,但最近没有被引用过,这种页面有可能被交换出去
* Speculative(投机) 针对可能的内存需要做了一个猜测的分配,对页面进行投机映射,因为很可能很快被访问到
* Wired(联动) 物理页正用于一个虚拟内存页,但不能被交换出去,一般用于内核代码数据
![物理页面的生命周期](http://upload-images.jianshu.io/upload_images/1736329-5490b505dd10a15b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
* 内存的分析工具Allocations
![Allocations](http://upload-images.jianshu.io/upload_images/1736329-32f9220e8c5f9856.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
All Heap & Anonymous VM中All Heap为堆上分配的对象,Anonymous VM比如创建UIView时CALayer底层所占空间.Diry Size为Dirty Memory所占内存,Resident Size为实际所占的物理内存.
* 内存最佳实践
* Weak Strong Dance(解决block循环引用的技巧)
AFNetworking中的实践:
__weak __typeof(self)weakSelf = self;
AFNetworkReachabilityStatusBlock callback = ^(AFNetworkReachabilityStatus status) {
__strong __typeof(weakSelf)strongSelf = weakSelf;
strongSelf.networkReachabilityStatus = status;
if (strongSelf.networkReachabilityStatusBlock) {
strongSelf.networkReachabilityStatusBlock(status);
}
};
}];
可以查看[对Weak String Dance的思考](http://www.jianshu.com/p/4ec18161d790)
* Dealloc Block Executor(释放内存的小技巧)
![Dealloc Block Executor](http://upload-images.jianshu.io/upload_images/1736329-9fa7d457e2a0bd5f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
AssociateObject的父对象释放的时候,子对象也会被释放.通过block来释放对象,不用使用dealloc.
![UIView的释放时序图](http://upload-images.jianshu.io/upload_images/1736329-f39d8cf05106499b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
* 降低内存峰值
* Lazy Allocation
MyBuffer *GetGlobalBuffer()
{
static MyBuffer *sMyBuffer = NULL;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sMyBuffer = [MyBuffer new];
});
return sMyBuffer;
}
直到使用的时候才分配,且为线程安全,可以用于分配对象或者资源文件读取等,方便Patch,比如JSPatch.
* alloca VS malloc
栈内存分配alloca(size_t)
* 栈分配仅仅修改栈指针寄存器,比malloc遍历并修改空闲列表要快得多
* 栈内存一般都已经在物理内存中,不用担心页错误
* 函数返回的时候栈分配的空间都会自动释放
* 但仅适合小空间的分配,并且函数嵌套不宜过深
* calloc VS malloc + memset
calloc(size_t num,size_t size)分配内存时是虚拟内存,只有在访问的时候才会发生物理页的映射关系,malloc+memset就会产生Dirty Memory.
* 分配内存并初始化
* 立即分配虚拟空间并设置清0标记位,但不分配物理内存
* 只有相应的虚拟地址空间被读写操作的时候才需要分配相应的物理内存页并初始化
* AutoreleasePool
* 基于引用计数,Pool执行drain方法会release所有该Pool中的autorelease对象
* 可以嵌套多个AutoReleasePool
* 每个线程并没有设置默认的AutoReleasePool,需要手动创建,避免内存泄露
* 在一段内存分配频繁的代码中嵌套AutoReleasePool有利于降低整体内存峰值
* imageNamed VS imageWithContentOfFile
* imageNamed使用系统缓存,适用于频繁使用的小图片
* imageWithContentOfFile不带缓存机制,适用于大图片,使用完就释放
* NSData with fileMapping
NSData & 内存映射文件,NSData有两种读取方式:
* [NSData dataWithContentsOfFile:path];
* [NSData dataWithContentsOfFile:path options:NSDataReadingMappedIfSafe error:&error];映射文件到虚拟内存,只有读取操作的时候才会读取相应页的内容到物理内存页中,常用语大文件中.
* NSCache & NSPurgableData
* NSCache
* 2种界限条件:totalCostLimt & countLimit 超过这两种界限时都会去释放一些旧的资源.
* 类NSMutableDictionary,setObject:forKey:cost
* evictsObjectWithDiscardContent &
* 最好监听内存警告消息并移除所有Cache
* NSPurgableData
* 当系统处于低内存的时候自动移除
* 适用于大数据
* 内存警告的处理
* 尽可能释放多资源,尤其图片等占内存多的资源,等需要用的时候再重建
* 单例对象不要创建之后就一直持有数据,在内存紧张的时候释放掉
* iOS6之后系统内存紧张会自动释放CALayer的CABackingStore对象,需要使用的时候在调用drewRect来构建,所以没必要将self.view = nil,但有时候对于隐藏的ViewController直接设置self.view = nil能简化代码逻辑
示例代码:
@interface ViewController ()
@property (strong,readonly)NSString testData;
@end
@implementation ViewController
@synthesize testData=_testData;
// Override the default getter for testData
-(NSString)testData
{
if(nil==_testData)
_testData=[self createSomeData];
return _testData;
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
_testData=nil;
}
* 业内趋势
* 内存压缩
* 地址空间布局随机化ASLR