objc内存管理(一)


8种机械键盘轴体对比
本人程序员,要买一个写代码的键盘,请问红轴和茶轴怎么选?

自动引用计数(ARC,Automatic Reference Counting),是指内存中对引用采取自动计数的技术。在LLVM编译器中设置ARC为有效状态,就无需再次键入retain或release代码。

本文为《Objective-C高级编程》第一章读书笔记

内存管理的思考方式

  • 主要思路:
    • 自己生成的对象,自己持有
    • 非自己生成的对象,自己也能持有
    • 不再需要自己持有的对象被释放
    • 非自己持有的对象无法释放
  • OC中对象操作的方法有:
对象操作OC方法
生成并持有对象alloc/new/mutableCopy等
持有对象retain
释放对象release
废弃对象dealloc

alloc/retain/release/dealloc实现

由于Foundation框架并没有开源,所以实现部分用的是GNUstep框架源码。两者的行为和方式是一样的,所以理解了GNUstep源代码就相当于理解了苹果的cocoa实现。

alloc

  • 下面列出执行alloc方法所调用的方法和函数:
1
2
3
4
5
+ alloc
+ allocWithZone
class_createInstance

calloc

retainCount/retain/release

1
2
3
4
5
6
7
8
9
10
11
12
13
// 上面方法名,下面底层调用
-retainCount
__CFDoExternRefOperation
CFBasicHashGetCountOfKey

-retain
__CFDoExternRefOperation
CFBasicHashAddValue

-release
__CFDoExternRefOperation
// 返回0时,-release调用dealloc
CFBasicHashRemoveValue

可以看到上面的代码都调用了同一个函数__CFDoExternRefOperation,该函数源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int __CFDoExternRefOperation(uintptr_t op, id obj) {
CFBasicHashRef table = 取得对象对应的哈希表;
int count;

switch(op) {
case OPERATION_retainCount:
count = CFBasicHashGetCountOfKey(table, obj);
return count;

case OPERATION_retain:
CFBasicHashAddValue(table, obj);
return obj

case OPERATION_release:
count = CFBasicHashRemoveValue(table, obj);
return 0 == count;
}
}

从以上的代码可以看出,苹果的实现大概是采用哈希表(引用计数表)来管理引用计数,这样做的好处在于引用计数表各记录中存有内存块地址,可以从各个记录中追溯到各个对象的内存块。它的意义在于调试的时候,即使出现故障导致对象占用的内存块损坏,但只要引用计数表还在,就能确认各内存块的位置,在利用工具检测内存泄漏时,引用计数表的各记录也有助于检测各对象持有者是否存在

autorelease

autorelease类似于C语言中自动变量的特性,C语言中程序执行时,若某自动变量超出其作用域,该自动变量将被自动废弃。

autorelease的具体使用方法如下:

  • 生成并持有NSAutoreleasePool对象
  • 调用分配对象的autorelease实例方法
  • 废弃NSAutoreleasePool对象

autorelease实现

关键类:AutoreleasePoolPage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class AutoreleasePoolPage
{
static inline void *push()
{
// 相当于生成或持有NSAutoreleasePool对象
}

static inline void *pop(void *token)
{
// 相当于废弃NSAutoreleasePool类对象
releaseAll();
}

static inline id autorelease(id obj)
{
// 相当于NSAutoreleasePool类的addObject类方法
AutoreleasePoolPage *autoreleasePoolPage = 取得正在使用的AutoreleasePoolPage实例;
autoreleasePoolPage->add(obj);
}

id *add(id obj)
{
// 将对象追加到内部数组中
}

void releaseAll()
{
// 调用内部数组中对象的release实例方法
}
}

所有权修饰符

ARC有效时,类型必须附加所有权修饰符,所有权修饰符一共有4种:

  • __strong
  • __weak
  • __unsafe_unretained
  • __autoreleasing

__strong修饰符

附有__strong修饰符的变量在超出其作用域时,即在该变量被废弃时,会释放其被赋予的对象

__weak修饰符

仅通过__strong修饰符不能解决某些重大问题—-引用计数式内存管理中的循环引用问题

如以下源码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
@interface : NSObject
{
id __strong obj_;
}
- (void)setObject:(id __strong)obj;
@end

@implementation
- (id)init
{
self = [super init];
return self;
}

- (void)setObject:(id __strong)obj
{
obj_ = obj;
}
@end

// 以下代码发生循环引用
{
id test0 = [[Test alloc] init]; /* 对象A */

/*
* test0持有对象A
*/

id test1 = [[Test alloc] init]; /* 对象B */

/*
* test1持有对象B
*/

[test0 setObject:test1];

/*
* 对象A的成员变量obj_持有对象B的强引用
*
* 此时,持有对象B的强引用的变量为对象A的obj_和test1
*/

[test1 setObject:test0];

/*
* 对象B的成员变量obj_持有对象A的强引用
*
* 此时,持有对象A的强引用的变量为对象B的obj_和test0
*/

}

/*
* 超出作用域,强引用失效,自动释放对象A和对象B
*
* 此时持有对象A的强引用变量为对象B的obj_
*
* 持有对象B的强引用变量为对象A的obj_
*
* 发生内存泄漏!
*/

所谓内存泄漏就是应当废弃的对象在超出其生存周期后继续存在。对象持有自身时也会发生循环引用

避免循环可以使用__weak修饰符,在超出其变量作用域时,对象即被释放。__weak修饰符还有一个优点,就是若该变量被废弃,则此弱引用将自动失效并且处于nil被赋值的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
id __weak obj1 = nil;
{
// 自己生成并持有对象
id __strong obj0 = [[NSObject alloc] init];

/*
* 因为obj0变量为强引用
* 所以自己持有对象
*/

obj1 = obj0;

/*
* obj1变量持有对象的弱引用
*/

NSLog(@"A: %@", obj1);

/*
* 输出obj1变量持有的弱引用对象
*/

}

/*
* obj0超出强引用范围,强引用失效
* 所以自动释放自己持有的对象
* 对象无持有者,所以废弃对象
*
* 废弃对象同时,
* 持有该对象弱引用的obj1变量的弱引用失效,nil赋值给obj1
*/

NSLog(@"B: %@", obj1);

// 执行结果:输出nil

__unsafe_unretained修饰符

__unsafe_unretained修饰符是不安全的所有权修饰符。附有该修饰符的变量不属于编译器内存管理的对象

附有__unsafe_unretained修饰符的变量同附有__weak修饰符的变量相同的是,自己生成并持有的对象不能继续为自己所有,所以生成的对象会立即被释放。不同之处看下面的代码和注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
id __unsafe_unretained obj1 = nil

{
id __strong obj0 = [[NSObject alloc] init];
obj1 = obj0;

/*
* 虽然obj0变量赋值给obj1
* 但obj1变量不持有对象的强引用或弱引用
*/

NSLog(@"A: %@", obj1);

/*
* 输出obj1变量表示的对象
*/

}
/*
* 超出作用域,强引用失效
* 所以自动释放自己持有的对象
* 对象无持有者,废弃该对象
*/

NSLog(@"B: %@", obj1);
/*
* 输出obj1变量表示的对象
* 已经被废弃(悬空指针)
* BAD_ACESS!
*/

最后一行的NSLog只是碰巧正常运行而已,虽然访问已经废弃的对象但应用程序在个别运行情况下才会崩溃。在使用__unsafe_unretained时,赋值给__strong修饰符的变量时有必要确保被赋值对象确实存在。这个修饰符的存在意义在于iOS4以前的应用必须使用__unsafe_unretained替代__weak

__autoreleasing修饰符

ARC有效时,不能使用autorelease方法,也不能用NSAutoreleasePool类。虽然autorelease无法直接使用,但实际上,ARC有效时autorelease功能是起作用的。

1
2
3
4
5
6
7
8
9
10
11
// ARC无效时
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain];

// ARC有效时
@autoreleasepool {
id __autoreleasing
obj = [[NSObject alloc] init];
}

ARC有效时,要通过将对象赋值给附加了__autoreleasing修饰符的变量来替代调用autorelease方法。总的来说,就是ARC有效时,用@autoreleasepool块替代NSAutoreleasePool类,用附有__autoreleasing修饰符替代autorelease方法。

但为什么很少见到显式地调用上面所述的标识符呢?因为编译器会检查方法名是否以alloc/new/copy/mutableCopy开始,如果不是,则将返回值的对象注册到autoreleasepool

实际上,访问附有__weak修饰符的变量必须访问注册到autoreleasepool的对象。因为访问弱引用持有的对象时,该对象有可能被废弃掉,所以把它注册到autoreleasepool中,在自动释放池结束之前都能确保对象存在

属性

属性声明的属性所有权修饰符
assign__unsafe_unretained修饰符
copy__strong修饰符(赋值的是被复制的对象)
retain__strong修饰符
strong__strong修饰符
weak__weak修饰符
__unsafe_unretained__unsafe_unretained修饰符