关于AutoReleasePool和ARC的一些研究

本文将会阐述AutoReleasePool和ARC所有权修饰符的实现原理和自己的一些理解,并解决如下几个问题:

  1. AutoReleasePool和ARC是怎么配合完成内存管理的;
  2. __weak修饰的变量引用和释放问题;
  3. ARC的实现原理;
  4. BAD_ACCESS的出现原因;
  5. 局部变量的释放问题;

AutoReleasePool运行原理

AutoRelease顾名思义会在某个时机自动释放管理在池内的变量,看上去很像ARC,甚至新手会将这两个概念混淆。但其实它更像C语言局部变量的特性。

首先我们来看下面的代码段:

1
2
3
4
{
int i = 5; //栈上
id obj = [[NSObject alloc] init]; //堆上
}

这段代码在ARC环境下,是没有问题的,但是在MRC环境下obj变量持有的对象不会正确释放从而造成内存泄露,而i会被正确释放。AutoRelease会像C语言一样,用类似管理局部变量的方式来完成堆上对象的释放操作。但与C语言不同的是,我们可以设定池的作用域。

自释放池的运行原理如下图:

avatar

NSAutoReleasePool对象会在每次Runloop开始前生成,期间我们可以往自释放池内添加对象,当自释放池对象声明周期结束时会对池内的注册对象进行了一次Release操作。

利用Autorelease达到延时释放

内存管理的规则是,谁持有谁释放,但是Foundation中有类似[NSMutableArray array]类方法取得对象存在,但自己不持有,这是怎么做到的呢

1
2
3
4
5
6
7
8
9
10
11
- (id)instance {
id obj = [[NSObject alloc] init]; //自己持有对象
[obj autorelease]; //取得对象存在,自己不持有
return obj;
}

- (void)viewDidLoad {
[super viewDidLoad];
self.obj1 = [self instance]; //_obj1持有谁都没有持有的对象
[self instance]; //无人持有该生成对象,在一次runloop后被回收
}

-instance方法将会返回一个NSObject类型的对象,同时该对象的内存分配和实例化都在方法中实现,在-instance方法中实例化的对象会被放入自动释放池,若在一个生命周期内无人持有该对象,那该对象在生命周期后会被回收,这样的延时释放机制是很有用的

我们来看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
- (id)instance {
id obj = [[NSObject alloc] init]; //自己持有对象
[obj autorelease]; //取得对象存在,自己不持有
return obj;
}

- (void)viewDidLoad {
[super viewDidLoad];

[[NSObject alloc] init]; //生成NSObject对象1
[self instance]; //生成NSObject对象2
}

以上代码,我们用两种方式生成了NSObject对象,都无变量持有,都会被释放,但是两种生成方式生成的对象在内存管理上的差别在哪里呢。NSObject对象1,在实例化时由于无变量持有,引用计数不会+1,所以他会被立即释放;而NSObject对象2,实际上在-instance方法内,被局部变量obj持有引用计数+1,不会被立即释放,所以他会在一次Runloop后被自释放池释放

Autorelease在不同编译环境的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#pragma mark - MRC
//1. 使用公用池
[NSAutoreleasePool addObject:autoReleasedObj];

//2. 使用NSAutoreleasePool对象
NSAutoreleasePool *pool=[[NSAutoreleasePool alloc] init];

ClassA *a = [[[ClassA alloc] init] autorelease];
ClassB *b = [[[ClassB alloc] init] autorelease];
...
[pool release];

#pragma mark - ARC
@autoreleasepool {
//your code here will be autoreleased
id obj = [[NSObject alloc] init];
}

自动释放池的使用场景

目前常用的自动释放池使用场景有两处:

  • 当产生大量Autorelease局部变量时
1
2
3
4
5
6
7
8
9
NSArray *urls = <# An array of file URLs #>;
for (NSURL *url in urls) {
@autoreleasepool {
NSError *error;
NSString *fileContents = [NSString stringWithContentsOfURL:url
encoding:NSUTF8StringEncoding
error:&error];
}
}

这些对象在Runloop结束之前都不会被释放,那么必然会导致这个Runloop周期内的内存峰值过大,我们可以使用自释放池包裹每次循环的内容,那么当一次循环结束,池内的临时对象都将被销毁

  • 需要自己管理一个辅助线程时
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)subThread {
// 创建子线程
NSThread *subThread = [[NSThread alloc] initWithTarget:self selector:@selector(subThreadEntryPoint) object:nil];
[subThread setName:@"Secondary"];
[subThread start];
self.subThread = subThread;
}

- (void)subThreadEntryPoint {
// 启动Runloop保活线程,添加自释放池
@autoreleasepool {
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
[runLoop run];
}
}

关于自动释放池的官方文档中有提到:

1
2
3
4
5
6
7
8
9
10
11
12
If you spawn a secondary thread.
You must create your own autorelease pool block as soon as the thread begins executing;
otherwise, your application will leak objects. (See Autorelease Pool Blocks and Threads for details.)

Each thread in a Cocoa application maintains its own stack of autorelease pool blocks.

If you are writing a Foundation-only program or if you detach a thread, you need to create your own autorelease pool block.

If your application or thread is long-lived and potentially generates a lot of autoreleased objects,
you should use autorelease pool blocks (like AppKit and UIKit do on the main thread);
otherwise, autoreleased objects accumulate and your memory footprint grows.
If your detached thread does not make Cocoa calls, you do not need to use an autorelease pool block.

在Cocoa应用程序中,每个线程独立维护自己的自释放堆栈。如果需要自己生成管理一个辅助线程,必须创建自己的自释放池,否则一旦线程执行,该线程将会产生内存泄露对象。

Autorelease是怎么配合ARC完成内存管理

所以我们可以来回答文章开始提出的问题,既然有了ARC,为什么还需要Autorelease呢。正常情况下,设置了ARC编译期会自动插入内存管理代码,这是编译期特性,但我们依然需要一个更灵活的管理对象生命周期的方式。

例如延时释放的场景,非alloc/new/copy/mutableCopy生成对象在超出代码域时对对象进行保活;
例如为了降低内存峰值,提前释放因为循环语句生成的大量临时变量;
例如对赋有__weak修饰符变量的引用。

其实ARC也可以舍弃Autorelease这个概念,并且规定所有从方法中返回的对象保留引用计数比期望值多1,但是这么做破坏了向后的兼容性,需要考虑到不适用ARC的代码。

所以笔者的理解,Autorelease是对ARC内存管理一种兼容性优化方案。并且由于ARC下我们无法对对象的引用计数进行操作,Autorelease灵活补充了ARC对对象生命周期的控制,使开发者可以在一个Runloop的范围内对对象进行延迟或提前释放控制。

ARC的实现

XCode4.2默认设置ARC有效。ARC环境下编译期LLVM会自动添加内存管理代码,并且id类型必须要加上所有权修饰

  • __strong
  • __weak
  • __unsafe_unretained
  • __autoreleasing

苹果的官方说明中称,ARC是“由编译期进行内存管理”的技术,其本质和JAVA的垃圾回收机制有区别。并且编译器并不能完全胜任内存管理,在此基础上还需要Objective-C运行时库的协助。所以ARC的实现条件是:

  • clang-3.0以上
  • objc4-493.9以上

__strong修饰符

__strong修饰符是id类型默认的所有权修饰符,当声明变量时未指定对象所有权修饰,那么__strong会被默认添加

1
2
3
4
5
{
id obj = [[NSObject alloc] init];
// 等价于
id __strong obj = [[NSObject alloc] init];
}

那么我们来看下声明强引用的编译期代码,当代码作用域结束时,编译器会自动插入release

1
2
3
4
5
6
7
8
9
10
{
id obj = [[NSObject alloc] init];
}

// 转换成编译器模拟代码
{
id obj = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
objc_release(obj);
}

非alloc/new/copy/mutableCopy方法生成对象

1
2
3
4
5
6
7
8
9
10
{
id __strong obj = [NSMutableArray array];
}

// 转换成编译器模拟代码
{
id obj = objc_msgSend(NSMutableArray, @selector(array));
objc_retainAutoreleasedReturnValue(obj);
objc_release(obj);
}

中间使用objc_retainAutoreleasedReturnValue()方法是什么意思呢。我们知道,将内存管理语义在方法名中表示出来是Objective-C的惯例,而ARC将之确立为硬性规定,若方法名为下列词语开头alloc/new/copy/mutableCopy,则返回对象归调用者所有。而array方法生成的NSMutableArray对象明显不符合这个规定,我们来看看如果不使用objc_retainAutoreleasedReturnValue()方法会变成什么样:

1
2
3
4
5
6
7
8
9
10
11
12
13
/** NSMutableArray.m */
+ (instancetype)array {
id obj = [[NSMutableArray alloc] init];
[obj autorelease];
return obj;
}

/** main.m */
void main() {
NSMutableArray * obj = [[NSMutableArray alloc] init];
[obj retain];
[obj release];
}

+array方法返回的是一个autorelease对象,如果我们要引用这个对象,编译器在设置其值得时候还需要进行一次retain操作。可以看出,+array方法中的autorelease和main()中的retain都是多余的,也就是autorelease紧跟其后就retain。为了优化代码,可以省略autorelease的注册,改为调用objc_autoreleaseReturnValue,它要比调用autorelease和retain更快。

avatar

代码改写为如下:

1
2
3
4
5
6
7
8
9
10
11
12
/** NSMutableArray.m */
+ (instancetype)array {
id obj = [[NSMutableArray alloc] init];
return objc_autoreleaseReturnValue(obj);
}

/** main.m */
void main() {
NSMutableArray * obj = [NSMutableArray array];
objc_retainAutoreleasedReturnValue(obj);
[obj release];
}

__weak修饰符

__weak修饰符提供了两个特性:

  • 若附有__weak修饰符的变量所引用的对象被废弃时,则将该变量赋值为nil
  • 使用附有__weak修饰符的变量,就是使用注册到autoreleasepool中的对象
1
2
3
4
5
6
7
8
9
10
{
id __weak obj1 = obj;
}

// 转换成编译器模拟代码
{
id obj1;
objc_initWeak(&obj1, obj);
objc_destroyWeak(&obj1);
}

这段模拟代码表明,编译器使用objc_initWeak()函数初始化weak变量,在作用域结束时使用objc_destroyWeak()释放该变量。以上代码可以再进行下面的改写

1
2
3
4
5
{
id obj1 = 0;
objc_storeWeak(&obj1, obj);
objc_storeWeak(&obj1, 0); // 如果第二个变量为0,则把&obj1从表中删除
}

__weak修饰的实质,就是以被引用对象为键,引用指针地址为值插入Weak散列表,这样一个对象就可以同时赋值给多个__weak修饰符变量。当被引用对象释放时,又可以根据散列表的结构特性快速找到所有的变量地址,将其置为nil并移除该记录。

现在来讲下__weak修饰符带来的另外一个特性,当使用__weak修饰符变量时,即是使用注册到autoreleasepool中的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
id __weak obj1 = obj;
NSLog(@"%@", obj1);
}

// 转换成编译器模拟代码
{
id obj1;
objc_initWeak(&obj1, obj);
id tmp = objc_loadWeakRetained(%obj1);
objc_autorelease(tmp);
NSLog(@"%@", tmp);
objc_destroyWeak(&obj1);
}

当使用弱引用变量时,会调用objc_loadWeakRetained()方法取出__weak变量所引用的对象并retain,并将该对象加入autoreleasepool。所以很多论坛上说__weak不会使引用对象计数+1是不规范和完整的说法。

这个特性的好处是,在@autoreleasepool块结束前,都可以放心使用该变量,而不用担心被引用对象被释放。坏处是,如果多处使用该变量,那么被应用对象也会被重复注册到autoreleasepool中。

例如如下代码:

1
2
3
4
5
6
id _weak o = obj;
NSLog(@"1 %@", o);
NSLog(@"2 %@", o);
NSLog(@"3 %@", o);
NSLog(@"4 %@", o);
NSLog(@"5 %@", o);

在使用变量o时,0x6719e40会被注册到autoreleasepool内5次

avatar

所以将附有__weak修饰符的变量o再赋值给附有__strong修饰符的变量,可以避免这个问题

1
2
3
4
5
6
7
id __weak o = obj;
id tmp = o;
NSLog(@"1 %@", tmp);
NSLog(@"2 %@", tmp);
NSLog(@"3 %@", tmp);
NSLog(@"4 %@", tmp);
NSLog(@"5 %@", tmp);

tmp = o;时,对象仅会被注册到autoreleasepool中1次

avatar

__autoreleasing修饰符

使用__autoreleasing,等同于将对象加入autoreleasepool延迟释放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
@autoreleasepool {
id __autoreleasing obj = [[NSObject alloc] init];
}
}

// 转换成编译器模拟代码
{
id pool = objc_autoreleasePoolPush();
id obj = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
objc_autorelease(obj);
objc_autoreleasePoolPop(pool);
}

在非alloc/new/copy/mutableCopy生成对象用__autoreleasing修饰:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
@autoreleasepool {
id __autoreleasing obj = [NSMutableArray array];
}
}

// 转换成编译器模拟代码
{
id pool = objc_autoreleasePoolPush();
id obj = objc_msgSend(NSObject, @selector(array));
objc_retainAutoreleasedReturnValue(obj);
objc_autorelease(obj);
objc_autoreleasePoolPop(pool);
}

unsafe_unretained修饰符

__unsafe_unretained和名字一样,是不安全的所有权修饰符,它的用法和__weak类似。当对象销毁时,会依然指向之前的内存空间(野指针问题)。

那么有同学会问,既然是unsafe的,为什么还要使用__ unsafe_unretained修饰符呢。原因有两点:

  • __weak只支持iOS5.0以上的系统(当然对于现在,这个原因已经可以无视了)
  • __weak对性能会有一定的消耗,在上一个section我们介绍了其实现,知道它是通过一个散列表来管理弱引用关系的。所以__unsafe_unretained__weak要快,而且一个对象有大量的__weak引用变量的时候,当对象被废弃那么此时就要遍历Weak表,把对应链表的所有节点地址置空,消耗cpu资源。

所以我们在使用__unsafe_unretained修饰符时,要保证被引用对象附有__strong修饰符并确实存在,否则会出现EXC_BAD_ACCESS野指针问题。不过笔者认为,使用__unsafe_unretained修饰符带来的性能提升几乎可以忽略,而且野指针问题调试可能会非常棘手,所以还是使用__weak吧 ☺️