本文由易飞扬的博客出品,泰然授权转载,如需转载请联系原版权方。
原文链接:http://www.yifeiyang.net/development-of-the-iphone-simply-3/
前两节我们对 ARC(Automatic Reference Counting) 有了一个基本的理解,但是 ARC 是怎么产生的,为什么苹果要在其最新的 iOS/Mac OS X 上导入该框架? 如果不理解其背后的基本原理,只是死记硬背那些规则/方法,是毫无意义的。就像我们从小接受的填鸭式教育,基本上到后来都还给老师了。
本节,我们先来看看 ARC 产生之前的 Objective-C 内存管理世界,然后再来看看导入 ARC 后,新的 LLVM 编译器在背后为我们做了什么。
Objective-C 内存管理
和许多面向对象语言一样,Objective-C 中内存管理的方式其实就是指 引用计数 (Reference Counting)的使用准则。如下图所示,对象生成的时候必定被某个持有者拿着,如果有多个持有者的话,其引用计数就会递增;相反失去一个持有者那么引用计数即会递减,直到失去所有的持有者,才真正地从内测中释放自己。
基本原则
- 内存管理的依循下面的基本原则
- 自己生成的对象,那么既是其持有者
- 不是自己生成的对象,也可成为其持有者(一个对象可以被多个人持有)
- 如果不想持有对象的时候,必须释放其所有权
- 不能释放已不再持有所有权的对象
结合 Objective-C 语言中的方法,我们来看看基本的内存管理。
方法 | 动作 |
---|---|
alloc/new/copy/mutableCopy | 生成对象并拥有所有权 |
retain | 拥有对象所有权 |
release | 释放对象所有权 |
dealloc | 释放对象资源 |
实际上这些函数并不能说是 Objective-C 语言所特有的,而是 OS X / iOS 系统库中包含的基类函数;具体说就是 Cocoa Framework::Foundation::NSObject 基类的成员函数。
Objective-C 语言内部严格遵守上面表格中的定义;首先是 alloc/new/copy/mutableCopy 这几个函数,并且是alloc/new/copy/mutableCopy 开头的函数,比如:allpcMyObject/newTheObject/copyThis/mutableCopyTheObject 等都必须遵循这个原则。
反而言之,如果不是 alloc/new/copy/mutableCopy 开头的函数,而且要返回对象的话,那么调用端只是生成对象,而不是其持有者。
-(id)allocObject { /* * 生成对象并拥有所有权 */ id obj = [[NSObject alloc] init]; /* * 自己一直是持有对象状态 */ return obj; }
如上面的例子,alloc 生成的对象,其所有权会传递给函数的调用端;即满足了 alloc 开头函数的命名规则。
- 再看下面的例子
-(id)object { id obj = [[NSObject alloc] init]; /* * 自己一直是持有对象状态 */ [obj autorelease]; /* * 对象还存在,只是并不持有它的所有权 */ return obj; }
这里我们用到了 autorelease 函数。它的作用既是将对象放入 NSAutoreleasePool 中,由其来维护其生命周期。换句话说对象的持有者是 NSAutoreleasePool;上面的例子中,object 返回后,调用者将不持有其所有权。(除非再调用 retain。)
用 autorelease 的一个理由既是让程序员来控制对象的存活周期,而不像 C/C++ 等语言中,出栈后,栈中数据都被自动废弃,或者用 { } 框住的自动变量,当出了范围就看不到了。在 Objective-C 中,只有当 [pool drain] 被调用的时候,才清空 pool 中所有登录的对象实体,在这之前,你可以像往常一样正常使用对象。
当然可以想象得到的,如果一个程序只有一个 NSAutoreleasePool,并在 main 中声明,程序结束时才 [pool drain]/[pool release] 的话,那么所有 autorelease 的对象都将塞满这个 pool,会耗掉系统大部分内存。所以,使用 NSAutoreleasePool 的时候也尽量建议局部使用,比如下面的循环。
for (i=0; i < 100; i++) { NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; // 下面的函数由于不属于 alloc/new/copy/mutableCopy 范畴的函数,所以都使用了 autorelease NSMutableArray* array = [NSMutableArray array]; NSString *str = [NSString stringWithFormat:@"TestCode"]; /* * 其他使用autorelease定义的对象 */ Test *test = [[[Test alloc] init] autorelease]; // 通过下面的函数,可以随时监控pool中的对象 // iOS以外的运行库的情况下,也可以使用 _objc_autoreleasePoolPrint() 私有函数,只是需要下面的声明 // extern void _objc_autoreleasePoolPrint(); [NSAutoreleasePool showPools]; // 这里把所有pool中的对象都释放掉 [pool release]; }
当然 NSAutoreleasePool 也可以嵌套,基本上都依存大括号规则。
编程准则
基于以上原则,在 ARC 诞生之前,我们往往用下面准则来写代码。
生成对象时,使用autorelease
一般情况下,我们这样生成对象并使用
MyController* controller = [[MyController alloc] init]; // ...... [controller release];
如果在 [controller release] 之前函数return了怎么样,内存泄露了不是;为了防患于未然,一般像下面一样 生成对象时,使用autorelease。这样一来,该对象就被自动加入到最近的那个 pool 中。
MyController* controller = [[[MyController alloc] init] autorelease];
对象代入时,先autorelease后再retain
对象代入的时候,如果之前不将变量所持有的对象释放,那么很可能引起内存泄露。比如下面的代码
{ _member = [[TempValue alloc] init]; } - (void)setValue:(TempValue *)value { _member = value; // 这时,之前持有的对象因为没有 release 而引起内存泄露 // 当然,先 [_member release] 后再代入也是可以的, // 但是当与「对象在函数中返回时」的问题一同考虑时, // 如果没有 return [[object retain] autorelease] 的保证,这里即使 [_member release]也是百搭 // 详细的解释见下 }
鉴于以上原因,我们将原先的对象先autorelease后再将新对象retain代入。
{ _member = [[TempValue alloc] init]; // 这里,即使使用【生成对象时,使用autorelease】的准则,也没有关系 // 使用autorelease一次就将制定对象放入pool中,放几次[pool drain]的时候就释放几次 } - (void)setValue:(TempValue *)value { [_member autorelease]; _member = [value retain]; }
该原则遵循 Failed Self 的原则,虽然从性能上看有所损耗但是保证了代码质量。
对象在函数中返回时,使用return [[object retain] autorelease]
严格地说,是除 alloc/new/copy/mutableCopy 开头函数以外的函数中,有对象放回时,使用return [[object retain] autorelease]。
- 我们结合下面的例子来说明,并总结出该问题的几种解决方案
@implementation FooClass - (void)setObject:(MyObject *)object; { // 这里故意没有使用 autorelease,以便说明问题 [_object release]; _object = [object retain]; } - (id)object; { return _object; } - (void)dealloc; { [_object release]; [super dealloc]; } @end @implementation BarClass - (void)doStuff; { FooClass * foo = [[FooClass alloc] init]; // 创建第一个对象,引用计数 = 1 MyObject * firstObject = [[MyObject alloc] init]; // setObject中由于 [object retain] ,引用计数 = 2 [foo setObject:firstObject]; // 释放一次,引用计数 = 1;这之后对象有正确的所有权属性 [firstObject release]; // 通过非 alloc/new/copy/mutableCopy 开头函数得到对象 // anObject 指向第一个对象,但是并没有其所有权,对象引用计数 = 1 MyObject * anObject = [foo object]; [anObject testMethod]; // 创建第二个对象 MyObject * secondObject = [[MyObject alloc] init]; // setObject中由于 [_object release]; 第一个对象引用计数 = 0,内存被释放 [foo setObject:secondObject]; [secondObject release]; // 程序在这里崩溃了,因为 anObject 指向了一个空地址 [anObject testMethod]; } @end
从结论我们来看看该问题的几种可行的解决方案;各种方案中没有列出的代码与原先代码一致。
- 生成对象时,使用autorelease
@implementation BarClass - (void)doStuff; { FooClass * foo = [[FooClass alloc] init]; MyObject * firstObject = [[[MyObject alloc] init] autorelease]; [foo setObject:firstObject]; MyObject * anObject = [foo object]; [anObject testMethod]; MyObject * secondObject = [[[MyObject alloc] init] autorelease]; [foo setObject:secondObject]; [anObject testMethod]; } @end
对象生成时,即被放入最近的 pool 中,不需要人为特殊的维护,对象的生命周期将被延续,出 {} 范围之时即对象释放之际。
- 对象代入时,先autorelease后再retain
- (void)setObject:(MyObject *)object; { [_object autorelease]; _object = [object retain]; } - (id)object; { // 遵循非 alloc/new/copy/mutableCopy 开头的函数,不赐予所有权原则 return _object; }
同样的,对象被放入最近的 pool 中,第二次 setObject 后对象引用计数仍为1, pool 清空时才执行最后一次对象release,从而保证了代码的正确性。
- 对象在函数中返回时,使用return [[object retain] autorelease];
- (void)setObject:(MyObject *)object; { [_object release]; _object = [object retain]; } - (id)object; { // 遵循非 alloc/new/copy/mutableCopy 开头的函数,不赐予所有权原则 return [[_object retain] autorelease]; }
好不容易回到了本小节要说明的方法;可以看到这是从另一个角度解决了该问题:[foo object] 的时候保证引用计数是2,并将对象放入pool中维护。
总结上面上面3中方法,虽说是从不同角度入手解决了这个问题,但是基本原则不变,利用 NSAutoreleasePool 机制帮程序员维护代码,管理内存。
如果你觉得3种编码原则怎么搭配使用,在什么样的场合下选择比较麻烦,不要紧,都用就得了。我们牺牲的只是 NSAutoreleasePool 中的一些内存,一小许性能损失罢了,这总比我们的程序崩溃了强。
ARC 诞生
ARC 是什么我不需要再解释,若有不明白,可以看iPhone开发之深入浅出 (1) — ARC是什么。
- ARC 严格遵守 Objective-C 内存管理的基本原则
- 自己生成的对象,那么既是其持有者
- 不是自己生成的对象,也可成为其持有者(一个对象可以被多个人持有)
- 如果不想持有对象的时候,必须释放其所有权
- 不能释放已不再持有所有权的对象
并从编译器角度维护了该原则,比如如果不是 alloc/new/copy/mutableCopy 开头的函数,编译器会将生成的对象自动放入 autoReleasePool 中。如果是 __strong 修饰的变量,编译器会自动给其加上所有权。等等,详细,我们根据不同的关键字来看看编译器为我们具体做了什么。并从中总结出 ARC 的使用规则。
__strong
我们先来看看用 __strong 修饰的变量,以及缺省隐藏的 __strong 情况。
{ /* * 生成对象并拥有所有权 */ id __strong obj = [[NSObject alloc] init]; /* * 自己一直是持有对象状态 */ } /* * 变量出生命周期时,失去全部所有者,对象内存空间被释放 */
这种情况毫无悬念,缺省使用 alloc/new/copy/mutableCopy 开头的函数也是这样的结果。并且在这里,编译器帮我们自动的调用了对象的 release 函数,不需要手工维护。再看看下面的情况。
{ /* * 生成对象但是并没有其所有权 */ id __strong obj = [NSMutableArray array]; /* * 由于变量声明是强引用,自己一直是持有对象状态 * 编译器根据函数名,再将该对象放入 autoreleasepool 中 */ } /* * 变量出生命周期时,失去全部所有者,对象内存空间被释放 */
由上,虽然不是用 alloc/new/copy/mutableCopy 开头的函数得到的对象,由于是强参照,我们任然成为对象的持有者。而这,正是编译器帮我们做到的。
具体做的是什么呢?其实就是【对象在函数中返回时,使用return [[object retain] autorelease]】所描述的;如果你反汇编一下ARC生成的代码,可以看到这时会自动调用名为 objc_retainAutoreleaseReturnValue 的函数,而其作用和 [[object retain] autorelease] 一致。编译器通过函数名分析,如果不是 alloc/new/copy/mutableCopy 开头的函数,自动加入了这段代码。
另外,缺省 __strong 修饰的变量,对象代入的时候也正确地保证对象所有者规则;代入新对象时,自动释放旧对象的参照,代入nil的时候,表示释放当前对象的强参照。
__weak
虽然大部分场合,大部分问题使用 __strong 来编码就足够了;但是为了解决循环参照的问题 __weak 关键字修饰【弱参照】变量就发挥了左右。关于循环参照的问题,准备在以后的博文中介绍;今天,主要看看编译器在背后怎么处理 __weak 变量的。
__weak 声明的变量其实是被放入一个weak表中,该表和引用计数的表格类似,是一个Hash表,都是以对象的内存地址做key,同时,针对一个对象地址的key,可以同时对应多个变量的地址。
- 当一个 __weak 所指对象被释放时,系统按下面步骤来处理
- 从weak表中,通过对象地址(key)找到entry
- 将entry中所有指向该对象的变量设为nil
- 从weak表中删除该entry
- 从对象引用计数表中删除对象entry(通过通过对象地址找到)
另外,当使用 __weak 修饰的变量的时候,变量将放入 autoreleasepool 中,并且用几次放几次。比如下面的简单例子。
{ id __weak o = obj; NSLog(@"1 %@", o); NSLog(@"2 %@", o); NSLog(@"3 %@", o); NSLog(@"4 %@", o); NSLog(@"5 %@", o); }
这里我们用了5次,那么pool中就被登录了5次;从效率上考虑这样当然不是很好,可以通过代入 __strong 修饰的强参照变量来避开这个问题。
{ id __weak o = obj; id temp = o; NSLog(@"1 %@", temp); NSLog(@"2 %@", temp); NSLog(@"3 %@", temp); NSLog(@"4 %@", temp); NSLog(@"5 %@", temp); }
另外,还有通过重载 allowsWeakReference/retainWeakReference 函数来限制 __weak 声明变量使用回数的方法,毕竟不在本次讨论范畴之内,就此省略。
话说回来,为什么使用弱参照变量的时候,要将其放入 autoreleasepool 中呢?想想弱参照的定义就应该明白了 —- 如果在访问弱参照对象时,该对象被释放了怎么办,程序不就崩溃了嘛;所以为了解决该问题,又再一次用到了 pool。
__autoreleasing
虽然上面还没有降到该关键字,但是编译器在很多时候已经用到了 autoreleasepool。比如非 alloc/new/copy/mutableCopy 开头的函数返回一个对象的时候,又比如使用一个 __weak 声明的变量的时候。
实际上,写ARC代码的时候,明示 __autoreleasing 声明变量和明示 __strong 声明变量一样基本上没有,因为编译器已经为我们做了很多,很智能了(前提是我们要按ARC的规则写代码)。
还有一种编译器缺省使用 __autoreleasing 关键字声明变量的时候:对象指针类型。比如下面的对应关系。
id *obj == id __autoreleasing *obj NSObject **obj == NSObject * __autoreleasing *obj
所以,下面两个函数的是等价的。
-(BOOL)performOperationWithError:(NSError **)error; -(BOOL)performOperationWithError:(NSError * __autoreleasing *)error;
像下面的函数调用,为什么是可行的呢?
NSError __strong *error = nil; BOOL result = [obj performOperationWithError:&error];
其实,编译器是这样解释这段代码的。
NSError __strong *error = nil; NSError __autoreleasing *tmp = error; BOOL result = [obj performOperationWithError:&tmp]; error = tmp;
那么我们这样声明函数不就可以了吗?
-(BOOL)performOperationWithError:(NSError * __strong *)error;
答案是肯定的,你可以这样做,编译是可以通过,但你违反了非 alloc/new/copy/mutableCopy 开头的函数,不返回对象持有权的原则。这里是没有问题了,但也许影响到其他地方NG。
ARC 规则
- 结合上面的讲解,我想你也应该能够总结出来使用ARC时的规则
(这里只列出本讲中涉及的内容,其他的内容以后总结)
- 代码中不能使用retain, release, retain, autorelease
- 不能使用NSAllocateObject, NSDeallocateObject
- 不能使用NSAutoReleasePool、而需要@autoreleasepool块
- 严守内存管理相关函数命名规则
关于函数命名,伴随ARC的导入,还有一系列函数的定义也被严格定义了,那就是以 init 开头的函数。init 函数作为alloc生成对象的初期化函数,需要按原样直接传递对象给调用段,所以下面的声明是OK的。
-(id)initWithObject:(id)obj;
而下面的是NG的。
-(void)initWithObject;
不过声明为 -(void) initialize; 是没有问题的。