您的位置:首页 > 其它

iPhone开发之深入浅出 (3) — ARC之前世今生

2013-06-05 20:31 381 查看
博主:易飞扬
原文链接 : 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 开头的函数,而且要返回对象的话,那么调用端只是生成对象,而不是其持有者。

1
2
3
4
5
6
7
8
9
10
11

-(id)allocObject {
/*
* 生成对象并拥有所有权
*/
id obj = [[NSObject alloc] init];

/*
* 自己一直是持有对象状态
*/
return obj;
}

如上面的例子,alloc 生成的对象,其所有权会传递给函数的调用端;即满足了 alloc 开头函数的命名规则。

再看下面的例子

1
2
3
4
5
6
7
8
9
10
1112
13
14
15

-(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 的时候也尽量建议局部使用,比如下面的循环。

1
2
3
4
5
6
7
8
9
10
1112
13
14
15
16
17
18
19
20

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


一般情况下,我们这样生成对象并使用

1
2
3

MyController* controller = [[MyController alloc] init];
// ......
[controller release];

如果在 [controller release] 之前函数return了怎么样,内存泄露了不是;为了防患于未然,一般像下面一样 生成对象时,使用autorelease。这样一来,该对象就被自动加入到最近的那个 pool 中。

1

MyController* controller = [[[MyController alloc] init] autorelease];


对象代入时,先autorelease后再retain


对象代入的时候,如果之前不将变量所持有的对象释放,那么很可能引起内存泄露。比如下面的代码

1
2
3
4
5
6
7
8
9
10
1112

{
_member = [[TempValue alloc] init];
}

- (void)setValue:(TempValue *)value {
_member = value;
// 这时,之前持有的对象因为没有 release 而引起内存泄露
// 当然,先 [_member release] 后再代入也是可以的,
// 但是当与「对象在函数中返回时」的问题一同考虑时,
// 如果没有 return [[object retain] autorelease] 的保证,这里即使 [_member release]也是百搭
// 详细的解释见下
}

鉴于以上原因,我们将原先的对象先autorelease后再将新对象retain代入。

1
2
34
5
6
7
8
9
10

{
_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]。

我们结合下面的例子来说明,并总结出该问题的几种解决方案

1
2
3
4
5
6
7
8
9
10
1112
13
14
15
16
17
18
19
20
2122
23
24
25
26
27
28
29
30
3132
33
34
35
36
37
38
39
40
4142
43
44
45
46
47
48
49
50
51

@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];

// 创建第一个对象,引用计数 = 1MyObject * firstObject = [[MyObject alloc] init];
// setObject中由于 [object retain] ,引用计数 = 2
[foo setObject:firstObject];
// 释放一次,引用计数 = 1;这之后对象有正确的所有权属性
[firstObject release];

// 通过非 alloc/new/copy/mutableCopy 开头函数得到对象
// anObject 指向第一个对象,但是并没有其所有权,对象引用计数 = 1MyObject * anObject = [foo object];
[anObject testMethod];

// 创建第二个对象
MyObject * secondObject = [[MyObject alloc] init];
// setObject中由于 [_object release]; 第一个对象引用计数 = 0,内存被释放
[foo setObject:secondObject];
[secondObject release];

// 程序在这里崩溃了,因为 anObject 指向了一个空地址
[anObject testMethod];
}

@end

从结论我们来看看该问题的几种可行的解决方案;各种方案中没有列出的代码与原先代码一致。

生成对象时,使用autorelease

1
2
3
4
5
6
7
8
9
10
1112
13
14
15
16
17
18
19

@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

1
2
3
4
5
6
7
8
9
10
11

- (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];

1
2
3
4
5
6
7
8
9
10
11

- (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 情况。

1
2
3
4
5
6
7
8
9
10
1112
13

{
/*
* 生成对象并拥有所有权
*/
id __strong obj = [[NSObject alloc] init];

/*
* 自己一直是持有对象状态
*/
}
/*
* 变量出生命周期时,失去全部所有者,对象内存空间被释放
*/

这种情况毫无悬念,缺省使用 alloc/new/copy/mutableCopy 开头的函数也是这样的结果。并且在这里,编译器帮我们自动的调用了对象的 release 函数,不需要手工维护。再看看下面的情况。

1
2
3
4
5
6
7
8
9
10
1112
13
14

{
/*
* 生成对象但是并没有其所有权
*/
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 中,并且用几次放几次。比如下面的简单例子。

1
2
34
5
6
7
8

{
id __weak o = obj;
NSLog(@"1 %@", o);
NSLog(@"2 %@", o);
NSLog(@"3 %@", o);
NSLog(@"4 %@", o);
NSLog(@"5 %@", o);
}

这里我们用了5次,那么pool中就被登录了5次;从效率上考虑这样当然不是很好,可以通过代入 __strong 修饰的强参照变量来避开这个问题。

1
2
34
5
6
7
8
9

{
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 关键字声明变量的时候:对象指针类型。比如下面的对应关系。

12

id *obj == id __autoreleasing *obj
NSObject **obj == NSObject * __autoreleasing *obj

所以,下面两个函数的是等价的。

1
2
3

-(BOOL)performOperationWithError:(NSError **)error;

-(BOOL)performOperationWithError:(NSError * __autoreleasing *)error;

像下面的函数调用,为什么是可行的呢?

12

NSError __strong *error = nil;
BOOL result = [obj performOperationWithError:&error];

其实,编译器是这样解释这段代码的。

1
2
34

NSError __strong *error = nil;
NSError __autoreleasing *tmp = error;
BOOL result = [obj performOperationWithError:&tmp];
error = tmp;

那么我们这样声明函数不就可以了吗?

1

-(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的。

1

-(id)initWithObject:(id)obj;

而下面的是NG的。

1

-(void)initWithObject;

不过声明为 -(void) initialize; 是没有问题的
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: