内存管理不是iOS特有,几乎所有程序员都会遇到内存管理的问题。内存分栈区和堆区,栈区靠操作系统申请释放无需程序员关心,需要关心的是堆区内存的申请和释放。
原理说起来很容易就是申请和释放要配对出现,常见有三种释放内存的方式:
- 显式释放内存:C的free,C++的delete
- 基于引用计数释放内存:C++的Smart Pointer,Objective-C
- 垃圾回收:Java,C#
iOS一直不支持垃圾回收(OS X曾短暂支持过现已废弃),只支持引用计数方式管理内存,从早期的MRC演进到了现在的ARC。
MRC
MRC(Manual Retain Count)手动引用计数,顾名思义就是程序员需要自己确保对象的retain与release的成对出现。对象创建好之后引用计数为1,哪天引用计数为0了,对象就会被销毁内存将被回收。
用alloc,allocWithZone,copy,copyWithZone,mutableCopy,mutableCopyWithZone方法创建的对象,会retain持有该对象。
也可以用retain方法持有别人创建的对象。
当不需要这些对象时,可以用release,autorelease来释放内存。
引用计数也会出现两个对象直接或间接地互相持有对方,甚至自己持有自己的引用,导致Retain Cycle。解决方式有两种:
对于类属性property:声明时设为assign,要释放时手动置为nil
@property (nonatomic, assign) NSObject *parent;
对于block:用__block修饰符来修饰使用到的变量
AutoRelease
AutoRelease这个名字貌似有点歧义,感觉是将手动释放内存升级到自动释放内存。其实AutoRelease是为了解决延迟销毁对象的问题。
例如下面代码中,既不能在return前也不能在return后释放对象。
-(NSObject*)object { NSObject *o = [[NSObject alloc] init]; // [o release]; // 这行代码究竟应该在return前执行还是return后执行呢? return o; }
AutoRelease就是为了解决上述问题,可以延迟释放对象。
程序员可以手动调用autorelease方法,也可以用工厂方法的返回值(工厂方法默认使用了autorelease)。
// 和普通用[alloc init]方法创建的对象不同,工厂方法返回的对象默认使用了autorelease + (id)typeRemainderOfMethodName + (id)dataWithContentsOfURL:(NSURL *)url;
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // 需要在AutoreleasePool里使用autorelease方法 ... NSObject *obj = [[NSObject alloc] init] autorelease]; // 方式一:手动调用autorelease方法 ... NSString *string; char *cString = "Hello World"; string = [NSString stringWithUTF8String:cstring]; // 方式二:工厂方法的返回值会自动调用autorelease方法 ... [pool release];
其实程序员很少自己去手动创建AutoreleasePool,因为每个线程(包括主线程)都会拥有一个专属的NSRunLoop对象,并且会在有需要的时候自动创建。NSRunLoop对象的每个eventloop开始前,系统会自动创建一个AutoreleasePool,并在eventloop结束时drain。每一个线程都会维护自己的AutoreleasePool堆栈,即AutoreleasePool是与线程紧密相关的,每一个AutoreleasePool只对应一个线程。
善用AutoRelease可以解决一些内存峰值问题,例如下面循环结束前是不会自动触发autorelease的,导致循环次数很多时,内存占用率高:
// 会有内存峰值问题 for(int i=0; i<100; i++) { NSError*error; NSString*fileContents = [NSString stringWithContentsOfURL:urlArray[i] encoding:NSUTF8StringEncoding error:&error]; } // 解决方式: for(int i=0; i<100; i++) { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // 每次循环都开启AutoreleasePool NSError*error; NSString*fileContents = [NSString stringWithContentsOfURL:urlArray[i] encoding:NSUTF8StringEncoding error:&error]; [poolrelease]; // 每次循环结束就关闭AutoreleasePool }
ARC
MRC需要程序员手动调用retain,release,autorelease,导致内存管理一直是开发的噩梦。
iOS 4.3版引入了ARC,它是Objective-C编译器的特性,而不是运行时特性或者垃圾回收机制。
ARC的内存管理机制与MRC手动机制差不多,只是不再需要手动调用retain,release,autorelease了,编译器会在在适当位置插入retain,release,autorelease。
但ARC仍旧有野指针问题,所以iOS 5版支持了week关键字(用week修饰的对象,在没有引用时会自动置为nil)。
所以现在的内存管理是:编译时ARC+运行时week搭配使用。
ARC里得到对象的方式:alloc,new,copy,mutableCopy,init
ARC里对象修饰符有四种:__strong,__weak,__unsafe_unretained,__autoreleasing
__strong:ARC里用strong强引来取代retain,持有对象,将对象引用计数+1,同时编译器会自动在不需要它的时候插入release代码。而且__strong是默认修饰符,所以写代码时不用显式地加上__strong修饰符:
// 声明变量时,ARC和MRC代码一样都是(ARC里 __strong 是默认修饰符,不用显式写) id obj = [[NSObject alloc] init]; // ARC编译器会默认加上 __strong 的修饰符: id __strong obj = [[NSObject alloc] init]; // 作用域里的ARC和MRC的代码不一样,ARC的代码: { id __strong obj = [[NSObject alloc] init]; // [obj release]; // ARC里不需要这行了,编译器会在作用域结束时自动插入这行代码 } // 作用域里的MRC的代码 { idobj = [[NSObject alloc] init]; [obj release]; }
__weak:不持有对象,在没有引用时会自动置为nil。所以下面的写法是错误的:
// 会报错,不能将可以retain的对象赋值给weak修饰的变量 id __weak obj = [[xxx alloc] init];
__unsafe_unretained:不持有对象,在没有引用时不会自动置为nil。平时不常用,和__weak相似,__weak出现前是代替__weak的,即在iOS 4.3~5之间用于代替__weak去修饰对象的属性的。但有一种情况,在在C结构体里使用到Objective-C对象时,只能用它来修饰。
__autoreleasing:表明传引用的参数(id *)在返回时是autorelease的,效果同MRC下调用autorelease方法,即被修饰的对象会被加入autorelease pool。
@autoreleasepool { NSError *error; NSString *fileContents = [NSString stringWithContentsOfURL:url encoding:NSUTF8StringEncoding error:&error]; // 参数是autorelease的 ... }
ARC下同样会有Retain Cycle的问题,解决方式有:对property用weak修饰。对block用__weak, @weakify & @strongify来修饰
用weak修饰property来解决Retain Cycle的问题:
// 错误代码,会有Retain Cycle的问题 @property (nonatomic, copy) void(^myBlock)(void); - (void)test { self.myBlock = ^{ [self doSomething]; // self持有block的指针,block里又使用了self,形成了Retain Cycle }; } // 解决方式1 @property (nonatomic, copy) void(^myBlock)(void); - (void)test { @weakify(self); self.myBlock = ^{ [@strongify(self); [self doSomething]; }; } // 解决方式2 @property (nonatomic, copy) void(^myBlock)(void); - (void)test { __weak typeof(self) weakSelf = self; self.myBlock = ^{ [weakSelf doSomething]; }; }
QA
nonatomic修饰的属性在多线程设置属性时非常容易产生crash,因为nonatomic的属性被赋值后会将oldValue释放,如果两个线程同时设置nonatomic的属性后,会释放两次oldValue导致crash。所以多线程时属性要用atomic修饰。