上一篇文章中,我们主要
介绍了基于MRC环境下的内存管理。这篇文章主要介绍基于ARC环境下的内存管理。从WWDC2011到现在已经有近5年的时间,ACR机制的应用已经十分成熟,如今在Xcode中新建项目,都默认开启ARC。下面我会从ARC的原理到使用进行详细讲解。

一、什么是ARC

ARC——Automatic Reference Counting,自动引用计数。它不是运行时特性,不是垃圾回收器(GC),而是一种编译时特性

Automatic Reference Counting (ARC) is a compiler-level feature that simplifies the process of managing object lifetimes (memory management) in Cocoa applications.

与MRC模式相比,在ARC模式下会减少相应的工作量。为什么这样说呢?因为在ARC模式下编写代码,不需要写retainreleaseautorelease这三个关键字来对实例对象进行手动管理内存,这会减少很多代码。当开启ARC时,编译器在编译代码时会自动在代码合适的地方插入retainreleaseautorelease。也就是说,原来在MRC模式下需要写的类似于[obj release] 这样的代码,在ARC模式下编译器会自动帮我们完成,不需要我们去写,这就是所谓的自动引用计数。这样会相应地提高开发效率。

二、ARC工作原理

ARC模式的基本原理与MRC相同,都是引用计数原理,只是书写方式不同。在MRC模式下,如果想要保持一个对象使其不被释放,需要使用retain关键字。在ARC模式下要做的就是用一个指针指向这个对象,只要指针没有被置空,对象就会一直保持在堆上。当将指针指向新值时,原来的对象会被release一次。

ARC可以为开发者节省很多代码,使用ARC以后再也不需要关心什么时候retain,什么时候release,但是这并不意味你可以不思考内存管理,我们需要经常性地问自己这个问题:谁持有这个对象?

1.“持有”概念

在ARC中,我们说对象A“持有”对象B,就是说对象A“强引用”对象B。写法如下:

1
2
NSObject * obj = [[NSObject alloc] init];
NSObject * A = obj; //A指向obj,此时A对obj有一个引用,强引用

引用又分为强引用弱引用。被 strong 关键字修饰的对象A,如果指向对象obj,即obj被一个 strong 指针指向,obj被强引用,则obj不会销毁。如果对象没有任何 strong 指针指向,那么就讲销毁。被 weak 关键字修饰的对象B,如果指向对象obj,那么对象obj被一个 weak 指针指向,obj被弱引用,obj是否销毁与其无关。

一个 weak 指针P指向一个对象obj,并没有增加P的引用计数。另外,在ARC模式下,所有对象指针类型默认为 strong 类型

2.理解strong和weak

strongweak 类似于MRC模式下的 retainassign 。请看下图:
strongImg

在上图中,有两个 strong 类型指针A和B指向O,一个 weak 类型指针C指向O。每有一个 strong 类型指针指向O,在编译时,对象O会进行 [O retain] 一次,此时对象O的引用计数为2。weak 指针对其引用计数没有影响。当对象A或者对象B不再指向O时,对象O的引用计数减1,当没有对象持有时,进行释放。说到底,ARC模式的管理方式还是基于引用计数。

三、ARC修饰符

在ARC环境下,有4个与内存相关的变量所有权修饰符,他们分别是:

  • __strong
  • __weak
  • __autoreleasing
  • __unsafe_unretained

这里所说的变量所有权修饰符,与属性(property)中的属性修饰符不同,他们有如下对应关系:

  • assign 对应的所有权类型是 __unsafe_unretained
  • copy 对应的所有权类型是 __strong
  • retain 对应的所有权类型是 __strong
  • strong 对应的所有权类型是 __strong
  • unsafe_unretained 对应的所有权类型是 __unsafe_unretained
  • weak 对应的所有权类型是 __weak

关于属性修饰符,后面我会写一篇关于 property 的文章进行详细介绍,在此暂时不做介绍。接下来主要介绍一下4个变量所有权修饰符

1.__strong

__strong 表示引用为强引用。对应定义 property 时用到的 strong 。当对象没有任何一个强引用指向它时,它才会被释放。如果在声明引用时不加修饰符,那么引用将默认是强引用。当需要释放强引用指向的对象时,需要保证所有指向对象强引用置为 nil。__strong 修饰符是 id 类型和对象类型默认的所有权修饰符。

__strong修饰的变量会自动初始化为 nil

2.__weak

__weak 表示弱引用,对应定义 property 时用到的 weak__weak 最常见的一个作用就是用来避免强引用循环。但是需要注意的是,__weak 修饰符只能用于 iOS5 以上的版本,在 iOS4 及更低的版本中使用 __unsafe_unretained 修饰符来代替。关于 __weak,有以下几点需要注意:

(1)弱引用不会影响对象的释放,而当对象被释放时,所有指向它的弱引用都会自定被置为 nil,这样可以防止野指针。如下图:

weakImg
对于对象N,开始有一个强引用指针A和一个弱引用指针B指向它,之后A指向M,没有强引用指针指向N,N被释放,此时弱引用指针B自动被置为 nil,防止变为野指针。

(2)__weak 主要用来避免循环引用,主要有以下几个应用场景:

  • 在使用 delegate 时,我们需要将 delegate 的属性定义为 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
31
32
ClassOneVC:
@interface ClassOneViewController ()

@property (nonatomic, copy) NSString *name;
@property (nonatomic,weak) id <myDelegate> delegate;

@end

@implementation TestViewController
- (void)viewDidLoad {

[super viewDidLoad];
[self.delegate func];
}
@end

ClassTwoVC:
@interface ClassOneViewController ()

@end

@implementation TestViewController
- (void)viewDidLoad {
[super viewDidLoad];

ClassOneViewController * classOneVC = [ClassOneViewController new];
classOneVC.delegate = self //delegate为weak类型,不会对self强引用,避免循环引用。
}

//delegate func
- (void)func{};
@end
  • 在 Block 中防止强引用循环,后面细讲。
  • 用来修饰指向由 Interface Builder 创建的控件。比如:@property (weak, nonatomic) IBOutlet UIImageView myImgV。

    对于在类中使用的UIKit控件,一般为 strong 类型,至于为什么在Interface Builder或者StoryBoard中创建的控件可以用 weak ,有一种解释是在Interface Builder或者StoryBoard中进行了strong,具体什么原理还请大神解答。

3.__autoreleasing

__autoreleasing 修饰一个对象,表示这个对象被添加到 autorelease pool中自动释放引用。这和MRC模式下的 autorelease 的用法相同。只不过在MRC模式下,不能够再显示使用 autorelease 方法了,但是 autorelease 的机制还是有效的,即通过使用 autorelease 修饰对象。

下面两行代码意义相同:

1
2
NSString * str = [[[NSString alloc] initWithFormat:@"hello"] autorelease];  //MRC
NSString * __autoreleasing str = [[NSString alloc] initWithFormat:@"hello"]; //ARC

另外,定义property时不能使用这个修饰符,因为任何一个对象的property都不应该是 autorelese 类型。

在ARC模式下,使用(隐式使用)__autoreleasing 的几个场景:

  • 方法返回值
  • 访问 __weak 修饰的变量
  • id类型指针
  • 指针的指针(id *)
  • 某些类方法隐式创建自己的 autorelease pool

id 类型类似于(NSObject ),所以(id )类似于(NSObject ** )。

(1)方法返回值

请看下面代码:

1
2
3
4
- (NSObject *)myObject {
NSObject *obj = [[NSObject alloc] init];
return obj;
}

在这个方法中,obj 的默认所有权修饰符为 __strong 。当return时,使 obj超出其作用域,它强引用持有的对象本应该释放,但是由于该对象作为方法的返回值,所以一般情况下编译器会自动将 obj 注册到 Autorelease Pool中。这样就延长了对象的生命周期,使其出了作用域之后,还能够使用。当Autorelease Pool 被销毁的时候,对象的生命周期才会结束。

Autorelease Pool 是与线程一一映射的,这就是说一个 autoreleased 的对象的延迟释放是发生在它所在的 Autorelease Pool 对应的线程上的。。因此,在方法返回值的这个场景中,如果 Autorelease Pool 的 drain 方法没有在接收方和提供方交接的过程中触发,那么 autoreleased 对象是不会被释放的。所以不必担心 “Autorelease Pool 都销毁了,接收方还没接收到对象”这样的问题。

关于Autorelease Pool何时释放,生命周期的问题,实现原理等问题,可以参考这篇文章:黑幕背后的Autorelease

#####(2)访问 __weak 修饰变量
当访问由 __weak 修饰的变量时,实际访问的是注册到 Autorelease Pool中的对象,例如下面两段代码意义相同:

1
2
3
4
5
6
7
8
9
NSObject *obj0 = [NSObject new];

id __weak obj1 = obj0;
NSLog(@"class=%@", [obj1 class]);

// 编译时会处理成以下这样:
id __weak obj1 = obj0;
id __autoreleasing A = obj1;
NSLog(@"class=%@", [A class]);

这样做是为了延长对象的生命周期。因为在 __weak 修饰符只持有对象的弱引用,而在访问对象的过程中,该对象有可能被废弃,如果把被访问的对象注册到 Autorelease Pool 中,就能保证 Autorelease Pool 被销毁前对象是存在的。

(3) id类型指针

一个被引用过几百遍的例子,如在使用NSError时:

1
2
3
4
5
NSError *__autoreleasing error; 
if (![data writeToFile:filename options:NSDataWritingAtomic error:&error])
{
  NSLog(@"Error: %@", error);
}

在上面的代码中,如果error定义为 strong类型,即使不用 __autoreleasing 修饰,编译器也会帮你自动添加,保证你传入的是一个 autoreleaing 类型的引用,如下(意义与上段代码相同):

1
2
3
4
5
6
7
NSError *error; 
NSError *__autoreleasing tempError = error; // 编译器自动添加
if (![data writeToFile:filename options:NSDataWritingAtomic error:&tempError])
{
  error = tempError; // 编译器自动添加添加
  NSLog(@"Error: %@", error);
}

但是为了提高程序效率,我们在定义的error的时候,一般都声明为 autoreleasing 类型。

(4)指针的指针

在ARC环境下,所有种指针的指针类型(id *)的函数参数如果不加修饰符,编译器会默认将他们认定为 __autoreleasing 类型。例如下面两段代码等价:

1
2
3
4
- (void)myFunc:(NSObject **)obj
{
// do something
}
1
2
3
4
- (void)myFunc:(NSObject * __autoreleasing *)obj
{
// do something
}
(5)类方法隐式创建 Autorelease Pool

某些类的方法会隐式地使用自己的Autorelease Pool,例如NSDictionary的[enumerateKeysAndObjectsUsingBlock]方法:

1
2
3
4
5
6
7
8
9
10
11
12
- (void)loopThroughDictionary:(NSDictionary *)dict error:(NSError **)error
{
[dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop){

// do stuff
if (there is some error && error != nil)
{
*error = [NSError errorWithDomain:@"MyError" code:1 userInfo:nil];
}

}];
}

上面代码中,会隐式创建一个Autorelease Pool,等价于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)loopThroughDictionary:(NSDictionary *)dict error:(NSError **)error
{
[dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop){

@autoreleasepool // 被隐式创建
      {
if (there is some error && error != nil)
{
*error = [NSError errorWithDomain:@"MyError" code:1 userInfo:nil];
}
}
}];

// *error 在这里已经被dict的做枚举遍历时创建的autorelease pool释放掉了
}

为了能够正常的使用*error,我们需要一个strong型的临时引用,在dict的枚举Block中是用这个临时引用,保证引用指向的对象不会在出了dict的枚举Block后被释放,正确的方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)loopThroughDictionary:(NSDictionary *)dict error:(NSError **)error
{
  __block NSError* tempError; // 加__block保证可以在Block内被修改
  [dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop)
  {
    if (there is some error)
    {
      *tempError = [NSError errorWithDomain:@"MyError" code:1 userInfo:nil];
    }

  }]

  if (error != nil)
  {
    *error = tempError;
  }
}

4.__unsafe_unretained

ARC是在iOS 5引入的,而这个修饰符主要是为了在ARC刚发布时兼容iOS 4以及版本更低的设备,因为这些版本的设备没有weak pointer system,简单的理解这个系统就是我们上面讲weak时提到的,能够在 weak 引用指向对象被释放后,把引用值自动设为 nil 的系统。这个修饰符在定义property时对应的是”unsafe_unretained”,实际可以将它理解为MRC时代的 assign :纯粹只是将引用指向对象,没有任何额外的操作,在指向对象被释放时依然原原本本地指向原来被释放的对象(所在的内存区域)。所以非常不安全。

现在可以完全忽略掉这个修饰符了,因为iOS 4早已退出历史舞台,目前的APP基本都不会再去兼容iOS4。

四、ARC中的Block

一般情况下,block捕获的外部变量,可以在block内部使用,但是无法修改,例如下面代码:

1
2
3
4
5
6
7
8
9
10
11
{
NSString * str = @"hello";

void (^ block)();
block = ^ (void){
NSLog(@"%@",str);
//str = @"change";
};

block();
}

注:static的变量和全局变量不需要加__block就可以在Block中修改

如果修改 str,编译器会报错。如果想要修改 str ,需要用 __block 修饰符修饰要修改的变量,但也会引入新的问题,请看下面示例:

1
2
3
4
5
6
7
{
MyViewController * __block myController = [[MyViewController alloc] init…];

myController.completionHandler = ^(NSInteger result) {
[myController dismissViewControllerAnimated:YES completion:nil];
};
}

在上面这段代码中,myControllercompletionHandler 调用了 myController 的方法[dismissViewController…],这时 completionHandler 会对 myControllerretain 操作。而我们知道,myControllercompletionHandler 也至少有一个retain(一般准确讲是copy),这时就出现了在内存管理中最糟糕的情况:循环引用!

简单点说就是:myController retain了completionHandler,而completionHandler也retain了myController。循环引用导致了myController和completionHandler最终都不能被释放。

针对以上问题,如果循环引用已经产生了,我们可以这样去解决:

1
2
3
4
5
6
7
8
{
__block MyViewController * myController = [[MyViewController alloc] init…];

myController.completionHandler = ^(NSInteger result) {
[myController dismissViewControllerAnimated:YES completion:nil];
myController = nil; // 注意这里,将myController置为nil,保证了block结束myController强引用的解除
};
}

为了避免循环引用,大家可能想到这样一个方法:

1
2
3
4
5
6
7
8
9
10
{
MyViewController *myController = [[MyViewController alloc] init…];

MyViewController * __weak weakMyController = myController;

myController.completionHandler = ^(NSInteger result) {
[weakMyViewController dismissViewControllerAnimated:YES completion:nil];
};

}

在上述代码中,我们让block捕获了一个弱引用,即 weakMyController。但是问题又来了:block如果捕获一弱引用,在编译后会将其捕获在自己的函数栈中,当block函数执行完毕,就会释放这个弱引用。那么当myController指向的对象在completionHandler被调用前释放,那么completionHandler就不能正常的运作了。在一般的单线程环境中,这种问题出现的可能性不大,但是到了多线程环境,就很不好说了。

针对这个问题,有引入了下面的最佳解决方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
MyViewController *myController = [[MyViewController alloc] init…];
// ...
MyViewController * __weak weakMyController = myController;

myController.completionHandler = ^(NSInteger result) {
MyViewController *strongMyController = weakMyController;

  if (strongMyController) {
// ...
[strongMyController dismissViewControllerAnimated:YES completion:nil];
// ...
}
else {
// do something...
}
};

}

block内部定义了一个强引用,这就保证捕获的弱引用 weakMyController 在block函数栈运行结束后不会释放。如果说block存在于堆上,那么 strongMyController 作为block的成员,也会存在于堆上,只有在blokc销毁时,它才会销毁。

关于理解被Block捕获的引用和在Block内定义的引用的区别,及block底层原理,请看唐巧这篇关于block的文章。

最后关于block再说一点。__block在MRC时代有两个作用:

  • 说明变量可改
  • 说明指针指向的对象不做这个隐式的retain操作,用于避免循环引用。

在ARC模式下,__block修饰符只说明变量可修改

五、ARC与Toll-Free Bridging

Toll-Free Briding 保证了在程序中,可以方便和谐的使用 Core Foundation 类型的对象和Objective-C 类型的对象。

1.问题的引入

在 MRC 时代,由于 Objective-C 类型的对象和 Core Foundation 类型的对象都是相同的 release 和 retain 操作规则,所以 Toll-Free Bridging 的使用比较简单,但是自从切换到 ARC 后,Objective-C 类型的对象内存管理规则改变了,不能使用release和retain操作,而 Core Foundation 依然是之前的机制,也就是说,Core Foundation 不支持 ARC

这时候我们就需要解决一个问题:在做 Core Foundation 与 Objective-C 类型转换的时候,我们不仅要做类型转换,还要将其内存管理规则进行转换。

于是苹果在引入 ARC 之后对 Toll-Free Bridging 的操作也加入了对应的方法与修饰符,用来指明用哪种规则管理内存,或者说是内存管理权的归属。这些方法和修饰符分别是:

  • __bridge(修饰符)
  • __bridge_retained(修饰符) or CFBridgingRetain(函数)
  • __bridge_transfer(修饰符) or CFBridgingRelease(函数)

#####(1)__bridge

只是声明类型准换,不做内存管理规则转换。例如:

1
2
3
4
5
6
7
{
//NSString 转换为 CFStringRef
CFStringRef s1 = (__bridge CFStringRef) [[NSString alloc] initWithFormat:@"Hello"];

//其他对象类型转换
CFTypeRef s2 = (__bridge CFTypeRef)[NSObject new];
}

只是做了类型的转化,但管理规则未变,依然要用 Objective-C 类型的 ARC 来管理 s1,你不能用 CFRelease() 去释放 s1。

#####(2)__bridge_retained or CFBridgingRetain
表示将指针类型转变的同时,将内存管理的责任由原来的 Objective-C 交给Core Foundation 来处理,也就是,将 ARC 转变为 MRC。例如:

1
2
3
4
5
6
{
NSString *s1 = [[NSString alloc] initWithFormat:@"Hello"];

CFStringRef s2 = (__bridge_retained CFStringRef)s1; //将内存管理权交给s2
CFRelease(s2); // 注意要在使用结束后加这个
}

这时内存管理规则由ARC变为了MRC,我们需要手动的来管理s2的内存,而对于s1,我们即使将其置为nil,也不能释放内存。

上面代码也等价于:

1
2
3
4
5
6
{
NSString *s1 = [[NSString alloc] initWithFormat:@"Hello, %@!", name];

CFStringRef s2 = (CFStringRef)CFBridgingRetain(s1);
CFRelease(s2); // 注意要在使用结束后加这个
}
(3)__bridge_transfer(修饰符) or CFBridgingRelease(函数)

这个修饰符和函数的功能和上面那个__bridge_retained相反,它表示将管理的责任由Core Foundation转交给Objective-C,即将管理方式由MRC转变为ARC。

1
2
3
4
5
6
{
CFStringRef cfStr = CFURLCreateStringByAddingPercentEscapes(. . .);
NSString *str = (__bridge_transfer NSString *)cfStr;
//or NSString *str = (NSString *)CFBridgingRelease(cfStr);
return str;
}

这里我们将result的管理责任交给了ARC来处理,我们就不需要再显式调用CFRelease()了。

六、循环引用

在ARC模式下,不用我们去手动管理内存,这方便了很多,也减少了很多工作量。但是ARC模式也有它自己需要注意的问题,那就是循环引用

1.什么是循环引用

如下图中,对象A和对象B,相互引用对方作为自己的成员变量,只有当对象销毁时,才会将成员变量的计数器减1。但是对象A的销毁依赖于对象B的销毁,对象B的销毁依赖于对象A的销毁。他们互相依赖,谁都不能销毁,这就造成了循环引用。这样即使没有其他强引用指针指向它们,它们也不会销毁。

retainCircleImg

简单代码示例:

1
2
3
4
5
6
7
8
9
- (void)viewDidLoad {
[super viewDidLoad];
//两个数组,arr1和arr2互相引用
NSMutableArray *arr1 = [NSMutableArray new];
NSMutableArray *arr2 = [NSMutableArray new];

[arr1 addObject:arr2];
[arr2 addObject:arr1];
}

使用Instruments测试结果:
leaksTestImg

还有一种复杂的循环引用情景,那就是多个对象间依次持有,形成一个环状,这也会造成循环引用问题。例如下图中的情况:
retainCircle2Img
在实际项目开发中,项目的环境比较大,所以一旦产生这种多个对象之间的循环引用,修改起来十分繁琐,所以在实际开发中,应当注意。

2.容易产生循环引用场景

iOS开发中,有三个场景容易造成循环引用:

  • block 使用
  • delegate 使用
  • NSTimer 使用

具体如何产生于解除,请看这篇文章

3.避免和解除循环引用

1.如果想要避免产生循环引用,最长见的就是使用弱引用 (weak reference)。弱引用虽然持有对象,但是不增加引用计数,这样就避免了循环引用的产生。

2.如果循环引用已经产生,想要解除循环引用的话,需要开发者手动断开依赖对象。所以如果知道在什么时候断开循环引用回收内存,那就在相应的位置将对象手动置为 nil

有关ARC模式下内存管理的内容,就写到这里。还请大家勘误。下一篇将介绍几种简单的内存优化方案。

参考

1.Beginning ARC in iOS 5 Tutorial Part 1

2.iOS开发ARC内存管理技术要点

3.iOS ARC 内存管理要点

4.iOS开发进阶