0%

ARC 是如何进行内存管理的

基础知识提要

对于 Objective-C 对象来讲,手动内存管理主要是通过以下方法来进行的:

  • retain :    使得对象的引用计数+1
  • release:   使得对象的引用计数-1
  • autorelease: 使得对象的引用计数在 autorelease pool 释放的时候再-1
  • dealloc:   当对象的引用计数为0的时候自动调用,表明对象被释放

因为只有 OC 对象是分配在堆上的(其他如 C 语言对象是分配在栈上的),因此也只有 OC 对象在未开启 ARC 的时候需要我们手动管理内存。

对象的计数器,用来表示当前有多少个事物想令此对象继续存活下去。

对象的内存管理

简单内存管理示例

  简单的手动内存管理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ARC 无效
id obj = [[NSObject alloc] init];
//obj变量持有NSObject对象,该对象的引用计数=1
[obj retain];
//NSObject对象的引用计数+1 = 2
[obj release];
//NSObject对象的引用计数-1 = 1
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
//创建自动释放池
[obj autorelease];
//NSObject对象加入自动释放池,引用计数+1 = 2
[pool drain];
//自动释放池释放,对池中的所有对象发送 release 消息,因此NSObject对象引用计数-1 = 1
[obj release];
//NSObject对象的引用计数-1 = 0
//自动调用 dealloc 方法,废弃对象

  对应的 ARC 自动管理:

  因为开启 ARC 后,编译器会自动对 OC 对象进行内存管理,所以,ARC 有效时,不能调用 retain /release /autorelease /dealloc /retainCount 方法,其中,dealloc 方法可以覆写,但依然不能显示调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
//启用 ARC
{
id obj = [[NSObject alloc] init];
//obj变量持有NSObject对象,该对象的引用计数=1
@autoreleasepool {
id __autoreleasing obj2 = obj;
//obj变量将NSObject对象赋给带有__autoreleasing关键字的obj2变量,相当于[obj autorelease];
//NSObject对象加入自动释放池,引用计数+1 = 2
} //自动释放池释放,对池中的所有对象发送 release 消息,因此NSObject对象引用计数-1 = 1
}
//NSObject对象的持有者obj变量超出其作用域,引用失效
//因此,NSObject对象的引用计数-1 = 0
//自动调用 dealloc 方法,废弃对象

ARC 对象所有权修饰符

  • __strong: 默认修饰符,表示对对象的“强引用”,该修饰符修饰的变量在超出其作用域时被废弃,随着强引用的失效,自动 release 自己所持有的对象;
  • __weak: 弱引用。不持有对象,若该对象被废弃,则弱引用变量将自动赋值为 nil;
  • __unsafe_unretained: 同 __weak 一样不持有对象,但对象废弃时,不会自动为 nil,容易出现悬挂指针;
  • __autoreleasing: 相当于调用 autorelease 方法,即对象被注册到 autorelease pool 中。

什么叫做持有对象?

  我们知道 OC 对象的变量类型其实是指针变量,这些指针存储在栈上,指针指向的对象存储在堆中。

  指针 X1 指向对象 A,并使得对象 A 的引用计数+1,则我们说指针变量 X1 持有对象 A,或者 X1 持有该对象的强引用。

  指针 X2 虽然指向对象 A,但是对对象 A 的引用计数没有任何影响,即 X2 不指向对象 A,对象 A 的引用计数也不会减1;X2 指向对象 A,对象 A 的引用计数也不会加1,则我们说指针变量 X2 不持有对象 A。

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
//启用 ARC
id __weak obj_weak = nil;
id __unsafe_unretained obj_unsafe;
{
id obj0 = [[NSObject alloc] init];
//obj变量默认加了__strong修饰符,所以是强引用,持有NSObject对象,该对象的引用计数+1 =1
obj_weak = obj0;
//obj1持有NSObject对象的弱引用,对引用计数无影响
obj_unsafe = obj0;
//obj_unsafe不持有NSObject对象,对引用计数无影响
id obj_strong = obj0;
//obj3变量默认加了__strong修饰符,是强引用,NSObject对象的引用计数+1 = 2
@autoreleasepool {
id __autoreleasing obj_auto = obj0;
//obj变量将NSObject对象赋给带有__autoreleasing关键字的obj_auto变量,相当于[obj autorelease];
//obj_auto暂时持有NSObject对象,稍后释放;
//NSObject对象被暂时持有,加入自动释放池,引用计数+1 = 3
} //自动释放池释放,obj_auto变量超出其作用域,持有对象失效,
//也就是自动释放池取消obj_auto变量对对象的暂时持有权,
//相当于对池中的NSObject对象发送 release 消息,因此对象引用计数-1 = 2
}
//NSObject对象的持有者obj0变量超出其作用域,强引用失效,释放自己所持有的对象,NSObject对象的引用计数-1 = 1;
//持有者obj_storng变量超出作用域,强引用失效,释放自己所持有的对象,对象的引用计数-1 = 0;
//NSObject对象无持有者(即引用计数为0),自动调用 dealloc 方法,废弃对象;
//该对象的弱引用变量obj_weak失效,自动赋值为nil;
//obj_unsafe变量表示的对象已被废弃,变为悬挂指针。

修饰符番外
  id 的指针或对象的指针会默认加上 __autoreleasing 修饰符,如NSError **error,实际上为NSError *__autoreleasing* error

  对象被废弃时,含有 __weak 修饰符的变量将会有以下动作:
1) 从 weak 表中获取以废弃对象的地址为键值的记录;
2) 将包含在记录中的所有 __weak 修饰符变量的地址,赋值为 nil;
3) 从 weak 表中删除记录;
4) 从引用计数表中删除以被废弃对象的地址为键值的记录。

  也就是说含有 __weak 修饰符的变量所指的对象被废弃时,会比其他修饰符多执行前3步,如果大量使用 weak 修饰符,则会消耗相应的 CPU 资源,因此最好是在需要避免循环引用的时候才使用 __weak 修饰符。

在访问有 __weak 修饰符的变量时,其实会访问注册到 autorelease pool 的对象。

1
2
3
4
5
6
7
8
{
id __weak obj1 = obj;
}

//实际上为:
id obj1;
objc_initWeak(&obj1, obj);
objc_destroyWeak(&obj1);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
id __weak obj2 = obj;
NSLog(@"%@", obj2);
}


//实际上为:
id obj2;
objc_initWeak(&obj2, obj);

id tmp = objc_loadWeakRetained(&obj2);
objc_autorelease(tmp);

NSLog(@"%@", tmp);

objc_destroyWeak(&obj2);

  在 @autoreleasepool 块结束之前,使用多少次 weak 变量,就会注册到 autorelease pool 中多少次,所以在使用 __weak 修饰符的变量时,最好赋值给 __strong 修饰符的变量后使用。

可通过 _objc_autoreleasePoolPrint() 函数打印出注册到 autorelease pool 中的对象。

方法的内存管理

方法命名规则:

  • alloc/new/copy/mutableCopy 使用这些名称开头的方法,意味着生成的对象自己持有;
  • 以上名称之外的其他方法取得的对象,自己不持有。

注:以 init 开始的方法必须是实例方法,且必须要返回对象,该返回对象不注册到 autorelease pool 上,基本上只是对 alloc 方法返回值的对象进行初始化处理并返回该对象。

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
// ARC 无效
- (id)allocObject
{
id obj = [[NSObject alloc] init];
return obj;
}
- (id)object
{
id obj = [[NSObject alloc] init];
[obj autorelease];
return obj;
}
{
id obj1 = [obj0 allocObject];
//alloc开头的方法返回自己生成并持有的对象,
//即obj变量持有NSObject对象,该对象的引用计数至少=1
id obj2 = [obj0 object];
//取得对象存在,但obj2变量不持有NSObject对象,
//该对象的引用计数无变化
[obj2 retain];
//使得obj2持有对象,对象的引用计数+1
}
//启用ARC
- (id)object
{
id obj = [[NSObject alloc] init];
return obj;
//因为return使得变量obj超出作用域,所以强引用失效,自己持有的对象会被释放,
//但是因为该对象是作为方法的返回值,所以ARC会自动将其注册到autorelease pool
}

  也就是说对于非自己持有的方法,比如 [NSMutableArray array] 方法,在其方法内部,自动为返回值添加了 autorelease,我们可以使用这个返回值,但并不持有返回值所指的对象。在其对应的 autorelease pool 释放时(在主线程中,就是 RunLoop 循环一次之后),返回值所指的对象即被释放,如果没有对返回值执行 retain 操作,则对象没有持有者,自动调用 dealloc 方法,被废弃。

  我们常说,ARC 有效时,编译器会自动插入 retain/release/autorelease 方法。但实际上,ARC 在调用这些方法时,并不是通过普通的 OC 消息派发机制,而是直接调用底层 C 语言版本,比如 ARC 会调用与 retain 等价的底层函数 objc_retain。这样做更能优化性能,也是不能覆写 retain、release、autorelease 方法的原因,因为这些方法不会被直接调用。

  ARC 的优化还体现在很多方面,如使用非自己持有的方法,我们可以看到,在方法内部的返回对象调用 autorelease,与方法返回后,在调用方对返回对象 retain,两个操作实际上是可以抵消的,ARC 会自动做这方面的优化。以 [NSMutableArray array] 为例:

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
// ARC 代码

+ (id)array
{
return [[NSMutableArray alloc] init];
}

//编译器的模拟代码

+ (id)array
{
id obj = objc_msgSend(NSMutableArray, @selector(alloc));
objc_msgSen(obj, @selector(init));
return objc_autoreleaseReturnValue(obj);
}

// ARC 代码
{
id obj = [NSMutableArray array];
//obj默认为 __strong 修饰符变量,相当于[返回对象 retain]
}

//编译器的模拟代码
id obj = objc_msgSend(NSMutableArray, @selector(array));

objc_retainAutoreleasedReturnValue(obj);

objc_release(obj);

  objc_autoreleaseReturnValue 函数会检查使用该函数的方法,或函数调用方的执行命令列表,如果方法的调用方在调用了该方法后,紧接着调用了 objc_retainAutoreleasedReturnValue() 函数,那么就不将返回的对象注册到 autorelease pool 中,而是直接传递到方法的调用方。

Block 的内存管理

Block 的内存管理主要涉及到循环引用的问题。

Block 的创建一般是在栈上,但以下情况会被复制到堆上:
1) 调用 Block 的 copy 方法时;
2) Block 作为函数返回值时;
3) 将 Block 赋值给 __strong 修饰符的 id 类型或 Block 类型的成员变量时;
4) 在方法名中含有 usingBlock 的 Cocoa 框架方法或 GCD 的 API 中传递 Block 时。

我们知道 Block 会在声明时截获在 Block 内部将会用到的变量,如:

1
2
3
4
5
6
7
8
int main()
{
int val = 10;
void (^blk) (void) = ^{printf(val);};
val = 20;
blk();
//输出结果:10
}

__strong 变量

  对于 OC 对象而言,当 Block 从栈上被复制到堆上时,会对将要用到的带有 __strong 修饰符的变量执行 retain 操作,也就是 Block 会持有这个变量所指向的对象。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//开启 ARC
typedef void (^blk_t)(id);
blk_t blk;
{
id array = [[NSMutableArray alloc] init];
blk = ^(id obj) {
[array addObject:obj];
NSLog(@"count = %ld", [array count]);
}
}//array超出作用域,变量被废弃,
//但blk持有array所指向的对象,所以对象不会被废弃
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
//输出: count = 1
// count = 2
}
  • 开启 ARC 时,对于修饰符为 __strong 且捕获了外部变量(无论是否是 OC 对象)的 Block,会自动进行 copy 操作,将 Block 从栈上复制到堆上,由 NSStackBlock 转换为 NSMallocBlock。
  • 修饰符为 __strong 但未捕获外部变量的 Block,或者通过声明全局变量来声明 Block,都会自动创建为 NSGlobalBlock 类型。
  • 无法手动创建堆上的 Block,即 NSMallocBlock。

  上述代码中 array 变量超出了作用域因此被废弃,但是 blk 调用的时候仍可以使用 array 是为什么呢?
  是因为 Block 在进行 copy 操作的时候,会在自身结构体中添加一个同类型的 __strong 修饰符的 array 变量,所以访问的并不是之前我们所定义的 id array,而是 block->array

__block 变量

  __block 修饰符的变量可以在 Block 中更改变量,Block 在捕获变量时,会对有该修饰符的变量生成 __Block_byref_val 结构体。

1
2
3
4
5
6
7
8
9
10
11

__block val =10;

//在 Block 中存储为结构体
__Block_byref_val_0 val = {
void *__isa;
__Block_byref_val_0 *__forwarding; //= &val
int __flags;
int __size; //=sizeof(__Block_byref_val_0)
int val; //=10
}

  __Block_byref_val 结构体的成员变量 __forwarding 是指向该结构体实例自身的指针。Block 在访问__block 修饰的变量时,是通过这个指针来的:
  block \->val(block结构体中的成员变量)->__forwarding->val
当 Block 从栈复制到堆上是,该变量也会复制到堆上,栈上的原来指向自身的 __forwarding 指针会改变为指向堆上的 __block 变量。

  因此 __block 修饰的变量在 ARC 和非 ARC 中是有差别的。

  ARC 有效时,__block 变量除了可以在 Block 内部修改之外,无其他用处,是否 retain 取决于变量的 ARC 修饰符(__strong 持有、__weak 不持有等)。那么由于可修改,因此可以在 Block 内部对造成循环引用的变量赋值为 nil,释放掉自身的对象持有权,从而打破循环。

  ARC 无效时,在 [block copy] 之后,没有__block 修饰符的变量对象会被自动后台 retain,从而被 Block 持有;而有__block 修饰符的变量反而不会被 retain,不会被 Block 持有。因此对变量添加 __block 修饰符可以在非 ARC 情况下打破循环引用。

循环引用

既然 Block 也会持有对象,那么就很容易出现不易发现的循环引用问题了。如下:

1
2
3
4
5
6
7
8
9
10
11
//开启 ARC
- (void)loadView
{
[super loadView];
_observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"testKey"
object:nil
queue:nil
usingBlock:^(NSNotification *note) {
[self dismissModalViewControllerAnimated:YES];
}];
}

正如之前所提到的 Blcok 被复制到堆上的情况,在使用方法名中含有 usingBlock 的 Cocoa 框架方法时会被自动 copy 到堆上,从而对捕获到的 __strong 变量 执行 retain 操作,Block 持有该变量。

  在本例中,self 的成员变量 _observercopy 一份 Block,从而持有 Block,而 Block 中用到了默认为 __strong 修饰符的 self变量 ,从而持有 self,self 类本身又持有 成员变量 _observer,从而导致循环引用,使得谁都无法被最终释放,导致内存泄漏。

所以,要打破这种循环引用,需要使得 [block copy] 时不会 retain 捕获到的 self 变量。

注意:函数的闭包和 block 如果没有引用任何实例或类变量,其本身也不会造成循环引用,另外在 GCD 中,一般不会造成循环引用。这个例子之所以会造成循环引用,是因为 _observer 是 self 的成员变量。

方法一: 在 ARC 中使用不持有对象的 __weak__unsafe_unretain 修饰符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//开启 ARC
- (void)loadView
{
[super loadView];
__weak TestViewController *wself = self;
_observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"testKey"
object:nil
queue:nil
usingBlock:^(NSNotification *note) {
__strong TestViewController *sself = wself;
[sself dismissModalViewControllerAnimated:YES];
}];
}

  对于拥有 __weak 修饰符的 wself 变量,Block 复制时,不会对该变量指向的对象进行 retain,从而不持有该对象,对该对象的引用计数无影响。在 Block 内部又通过 __strong 修饰符的 sself 变量来持有对象,是为了避免在 Block 执行过程中,该对象被其他地方释放,从而造成访问错误。这实际上是一种延迟 self 的 retain 操作,使得它不在 Block 被 copy 的时候 retain,而是在执行的时候 retain。

  因为如果在最初 copy 的时候 retain,那么只有等 Block 被废弃时,该变量才会被废弃,从而释放对对象 X 的持有权。但是由于循环引用,该变量始终直接或间接的持有 block 对象,所以 Block 永远不会被废弃,进而也不会释放对象 X 的持有权,从而造成这两块内存永远不会被回收,即内存泄漏。

  而在执行的时候 retain,ARC会对 Block 的执行作用域的变量自动进行内存管理,执行完毕后即释放,不会等到 Block 被废弃时才能被释放,因此打破了循环引用。

方法二:在 ARC 中使用 __block 修饰符,并在 Block 中为其赋值为 nil

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//开启 ARC
- (void)loadView
{
[super loadView];
__blok TestViewController *wself = self;
_observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"testKey"
object:nil
queue:nil
usingBlock:^(NSNotification *note) {
[wself dismissModalViewControllerAnimated:YES];
}];
wself = nil;
}

由于在 Block 执行时释放了对 self 所指向的对象的持有权,因此 Block 执行后即打破循环引用,同样不会等到 Block 被废弃时才能释放对象的持有权,因此没有内存泄漏。

  这种方法的缺点是,一定要确保 Block 会执行。如果有多种分支,而某一条分支中的 Block 不会执行,那么这条分支同样会造成内存泄漏。

方法三:在非 ARC 中使用 __block 修饰符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ARC 无效
- (void)loadView
{
[super loadView];
__block TestViewController *wself = self;
_observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"testKey"
object:nil
queue:nil
usingBlock:^(NSNotification *note) {
[wself dismissModalViewControllerAnimated:YES];
}];
}

由于在非 ARC 中,Block 不持有 __block 修饰符修饰的对象,因此也不会造成循环引用。

注1:在使用委托 delegate 时,属性要用 weak 关键字也是为了避免循环引用。
注2:在异常 NSException 处理过程中,也容易遗忘对象释放,从而造成内存泄漏,一般须在 @finally 中将未释放的资源释放掉。当然如果该异常直接造成程序崩溃,也就无所谓释放不释放了。

以上即是 ARC 与非 ARC 的内存管理区别,以及 ARC 是如何将手动管理转换为自动管理的。


Reference

[1] 《Objective-C 高级编程 - iOS 与 OS X 多线程和内存管理》
[2] 《Effective Objective-C 2.0》
[3] Objective-C中block实现和技巧学习 http://www.tuicool.com/articles/aQFV7bv

----------------------END END----------------------