自动引用计数,又称ARC(Automatic Reference Counting)是苹果在iOS5中引入的重要特性,它减少了我们在内存管理时的麻烦,让我们可以把更多的精力放在其它更重要的事情上。
虽然ARC给我们带来了很多方便,但如果开发者不了解基本的内存管理知识,还是会在开发工作中遇到很多问题。所以,我总结了ARC环境下应该知道的内存管理知识,供诸位参考。
基于引用计数的内存管理
要了解ARC,必须先了解Objective-C中对象的内存管理机制以及手动管理引用计数(MRR,Manual Retain-Release)。
Objective-C内存管理机制
Objective-C中的对象都是基于引用计数来管理生命周期的。简单来说就是,我们在需要持有一个对象时,调用retain让它的引用计数+1。不需要这个对象的时候,调用release让它的引用计数-1。当一个对象引用计数为0的时候,这个对象就会被自动销毁。
MRR
我们在手动管理引用计数的时候,要明确地控制对象的生命周期,显式的调用每一个retain和release。我们必须清楚的了解每个接口对引用计数的处理(如把一个对象放到数组里引用计数会被+1,用alloc创建的对象的引用计数一开始就是1,用哪些接口创建的对象是已经被调用过autorelease的等等)。在处理引用计数时稍有疏忽,就可能导致程序崩溃或内存泄漏。
ARC
ARC是编译器通过对代码的静态分析,确定对象的生命周期,并在合适的位置自动加上retain和release的机制。把内存管理交给编译器以后,我们不需要再调用任何的retain和release了。ARC减少了MRR带来的思考负担,减少了内存问题出现的可能性,也大幅减少了代码量。
下图是苹果的文档中对ARC效果的描述,略显夸张的展示出了ARC的好处。
ARC的内存管理
ARC简化了引用计数的概念,它把变量对对象的引用分为强引用和弱引用两种。所有ARC下的内存管理原则都将基于这两个概念。强引用表示变量拥有对象的所有权。多个变量有可能同时持有同一个对象的强引用。当一个对象没有被任何变量强引用,它就会被释放。弱引用表示引用但不拥有对象,它可以避免两个对象相互强引用导致的内存泄漏问题。
所有权修饰符
ARC下的变量都会被加上下面几种所有权修饰符,它们指定了变量对其指向对象的所有权形式。整个ARC的规则也正是围绕着这几个修饰符运作:__strong:表示对对象的强引用。它是变量修饰符的默认值,也就是说只要没有显式的给变量加上所有权修饰符,它就是__strong的。__strong变量在离开作用域范围后会被废弃(其实就是编译器插入了一个release),对对象的强引用也随之消失。__weak:表示弱引用。当其指向的对象被释放时,这个变量会被置成nil。__autoreleasing:表示将修饰的对象加入autoreleasepool中,在autoreleasepool被销毁时自动释放对对象的强引用。在@autoreleasepool的代码块中的变量都会被加上这个修饰符,在超出代码块范围后释放。详见下面autorelease的部分。__unsafe_unretained:表示既不持有对象的强引用,也不持有弱引用(对象析构时它不会被置为nil)。正如它的名字描述,它是不安全的。它只是在iOS5之前用来代替__weak。
属性修饰符
在使用MRR的时候,我们可以给property添加retain、assign、copy这几种修饰符,来设置想要的内存管理模式,用下面这段代码来说明这三种修饰符的作用:
// MRR环境
@property (nonatomic, retain) NSObject* retainedObject;
@property (nonatomic, assign) NSObject* assignedObject;
@property (nonatomic, copy) NSMutableString* copiedObject;
- (void)testProperties {
NSObject* objectA = [[NSObject alloc] init]; // objectA引用计数为1。
self.retainedObject = objectA; // self.retainedObject和objectA指向同一个对象,且该对象引用计数为2。
NSObject* objectB = [[NSObject alloc] init]; // objectB引用计数为1。
self.assignedObject = objectB; // self.assignedObject和objectB指向同一个对象,且该对象引用计数为1。
NSMutableString* objectC = [[NSMutableString alloc] initWithFormat:@"test"]; // objectC引用计数为1
self.copiedObject = objectC; // self.copiedObject和objectC指向两个不同的对象,两个对象引用计数都为1。
// 这里可能会有疑问,为什么copy的例子用的是NSMutableString而不是NSObject了或者NSString?
// 因为NSObject没有实现NSCopying协议,没法复制。
// 而使用NSString会导致self.copiedObject和objectC因为编译器优化而指向相同的对象。
// 因为是MRR环境,我们要释放我们自己分配的内存,否则会产生内存泄漏。
[objectA release];
[objectC release];
// 当然,给分配内存的代码加上autorelease也行:
// NSObject* objectA = [[[NSObject alloc] init] autorelease];
// NSMutableString* objectC = [[[NSMutableString alloc] init] autorelease];
}
- (void)dealloc {
// MRR要在析构时手动清理内存。
[_objectA release];
[_objectB release];
[_objectC release];
[super dealloc];
}
在ARC环境下,增加了两种新的修饰符:strong和weak,分别对应强引用和弱引用:
// ARC环境
@property (nonatomic, strong) NSObject* strongObject;
@property (nonatomic, weak) NSObject* weakObject;
- (void)testProperties {
NSObject* objectD = [[NSObject alloc] init]; // objectD持有对象D的强引用。
self.strongObject = objectD; // self.strongObject和objectD都持有对象D的强引用
NSObject* objectE = [[NSObject alloc] init]; // objectE持有对象E的强引用。
self.weakObject = objectE; // self.weakObject持有对象E的弱引用
// 在ARC环境下,这个方法执行完后,objectD和objectE对对象D和E的强引用会消失。
// 这时候self.strongObject仍然持有对对象D的强引用。
// self.weakObject之前对对象E持有的是弱引用,对象E析构。self.weakObject的指针被置为nil。
}
// 与MRR不同,ARC环境下,self对象析构时,self.strongObject对对象E的强引用自动消失,对象E自动析构。
Retain Cycle(保留环)也叫循环引用
ARC确实帮助我们避免了许多内存管理的问题。但在ARC环境下,有一类问题还是需要被妥善的处理,这类问题叫做Retain Cycle(保留环)。
ARC环境下的对象在没有被强引用时就会被释放,当两个对象互相对对方持有强引用时,这两个对象就永远不会被释放了。这就导致了上面说的保留环问题。
要解决保留环的思路也简单,就是理清这两个对象之间的所有权关系,再让其中一个对象对另一个对象持有弱引用(使用weak指针)即可。
有一种保留环的问题相对隐蔽,出现在使用block的时候。block是一个可以被独立运行的代码块,为了保证它随时可以被运行,它会持有对它包含的所有变量的强引用。我们来看有问题的代码:
@property (nonatomic, strong) ExampleBlock aBlock; // self持有aBlock对象的强引用
- (void)exampleFunction {
self.aBlock = ^{
[self doSomething]; // aBlock持有self的强引用
};
}
上面的代码中,self和aBlock两个对象互相持有对方的强引用,导致了两个对象都无法被释放。
我们在遇到上述情况时,要让其中一个引用变为弱引用,修改后的代码如下:
@property (nonatomic, strong) ExampleBlock aBlock;
- (void)exampleFunction {
// 让block捕获self的弱引用
__weak __typeof(self)weakSelf = self;
self.aBlock = ^{
// 把弱引用转化为强引用,防止在block处理过程中self被析构。
__strong __typeof(weakSelf)strongSelf = weakSelf;
[strongSelf doSomething];
}];
}
autorelease
在iOS中,autorelease的意义是稍后释放。我们先看一段MRR的代码:
// MRR
- (void)doSomething {
NSArray* arrayA = [[NSArray alloc] init];
NSArray* arrayB = [self getEmptyArray];
// 按照MRR的原则,创建的对象的地方必须负责对象的释放。这样才能保持引用计数的平衡。
// 所以这里必须调用[arrayA release];来与上面的alloc保持平衡。
[arrayA release];
// 然而arrayB是从getEmptyArray方法中得来,
// 我们并不知道getEmptyArray是创建了一个新的对象还是返回了某个类的成员变量,
// 这里释放getEmptyArray返回的对象是不合适的。
}
- (void)getEmptyArray {
// 我们必须在getEmptyArray的实现中来保证引用计数的平衡。
// 如果写return [[[NSArray alloc] init] release];会返回一个nil。
// 所以,用autorelease让新创建的对象进行稍后释放。
return [[[NSArray alloc] init] autorelease];
}
稍后到底是什么时候?这个涉及到autoreleasepool的概念。
当一个对象被autorelease的时候,它其实是被注册到最里层(autoreleasepool是类似栈的结构)的autoreleasepool里,在autoreleasepool被销毁的时候,里面所有的对象都会被销毁。
系统会在每次消息循环开始的时候,建立一个autoreleasepool,在这一次消息循环结束后销毁这个autoreleasepool。一般情况下,这个就是最里层的autoreleasepool了。
我们可能会在一次消息循环周期内创建大量的autorelease对象。为了防止内存占用过多,我们可以手动使用@autoreleasepool代码块来创建自己的autoreleasepool,让我们创建的这些对象提前释放:
- (void)autoreleasePoolExample {
@autoreleasepool {
// 在这个块范围内被autorelease的对象会加到这个新的autoreleasepool里,
// 在这个代码块结束时就被释放。
}
}
在ARC下我们不能显式调用autorelease方法,那autorelease到底会在什么时候用到呢?其实编译器已经帮我们加上了autorelease:
- (id)getEmptyArray {
// 在ARC下我们不能自己加上autorelease。编译器为保证平衡和返回值的有效性,
// 会给这个方法的返回值隐式的加上autorelease。
// 实际上,除了alloc/new/copy等开头的方法外,
// 其它方法的返回值都会按照这个规则,被自动加上autorelease。
return [[NSArray alloc] init];
}
以上是对ARC环境下iOS内存管理总结。