iOS内存管理-ARC

什么是ARC

ARC(Automatic Reference Counting)是在iOS5.0推出的新功能,是新LLVM 3.0编译器的特性。简单地说,就是代码中自动加入了retain/release,原先需要手动添加的用来处理内存管理的引用计数的代码可以自动地由编译器完成了。

ARC是编译器特性,而不是iOS运行时特性(除了weak指针系统,它会在所指向的内存释放的时候自动将自身置为nil,后面文章会专门说下),它也不是其它语言中的垃圾收集器。因此ARC和手动内存管理性能是一样的,有些时候还能更加快速,因为编译器还可以执行某些优化。

ARC使用规则

ARC下,指针保持对象的生命。只有还有一个变量(strong)指向对象,对象就会保持在内存中。

1
NSString *firstName = self.textField.text;

上图中,firstName现在指向NSString对象,这时这个对象(textField的内容字符串)将被hold住。比如用字符串@“Ray”作为例子(字符串的retainCount规则和普通的对象不一样,这边就把它当作一个普通的对象来看吧…),这个时候firstName持有了@”Ray”。retainCount = 1。

一个对象可以有多个拥有者,如上图:retainCount=2。

当用户修改了文本框的text后,此时text属性会指向新的对象。原来的对象仍有firstName指向

而当firstName指向另外一个对象时,或者超出作用域范围时,@“Ray”这个字符串对象不在有任何所有者,retainCount = 0,将会被释放。

WARNING:上面我们所说的firstName、text这些指针都是“Strong”,能够保持对象的生命。默认的实例、本地变量都是Strong类型。

这边再提下刚刚我们说到的ARC的一个基本规则。

只要某个对象被任一strong指针指向,那么它将不会被销毁。如果对象没有被任何strong指针指向,那么就将被销毁。

weak指针

weak变量仍然指向一个对象,但不是对象的拥有者:

1
__weak NSString *weakName = self.textField.text;

当self.textField.text指向其他对象, 此时weakName会自动变成nil,称为“zeroing” weak pointer:

它解决了MRC下assign指针指向已经释放的对象(我们常说的野指针),对它发送消息Crash的问题。


ARC可以节省代码(MRC下属性赋值),我们不需要关心什么时候retain、release。但是不意味着我们不需要考虑内存管理

1
2
3
id obj = [array objectAtIndex:0];
[array removeObjectAtIndex:0];
NSLog(@"%@", obj);

上面代码在MRC下是错误的。从Array中移除一个对象会使对象不可用,对象不属于Array时会立即被释放。随后NSLog()打印该对象就会导致应用崩溃。

而在ARC中这段代码是完全合法的。因为obj变量是一个strong指针,它成为了对象的拥有者,从Array中移除该对象也不会导致对象被释放。

内存管理其他注意点:

  • 通知、KVO需要释放。(可在dealloc中)
  • NSTimer(rep=true)会强引用传入的target。
  • Block

ARC限制: 用于NSObject对象。

如果应用使用了Core Foundation或malloc()/free(),此时需要你来管理内存(这个我们下面再说)


Xcode的ARC自动迁移

要启用一个项目的ARC,你有以下几种选择:

  • Xcode带了一个自动转换工具,可以迁移源代码至ARC
  • 你可以手动转换源文件
  • 你可以在Xcode中禁用某些文件使用ARC,这点对于第三方库非常有用。

这边说下测试提出的需要将IBOutlet改为Strong的事情。

在ARC中,所有outlet属性都推荐使用weak,这些view对象已经属于View Controller的view hierarchy,不需要再次定义为strong(ARC中效果等同于retain)。唯一应该使用strong的outlet是File’s Owner,连接到nib的顶层对象。


ARC中dealloc与autorelease

dealloc

在ARC下我们一般不需要写dealloc函数。因为我们不需要调用[super dealloc],也不需要release属性。而Strong属性出了作用域(这个class)会被置为nil,对象相应的会被释放销毁。(内存从创建到如何销毁下面文章会写到)

而dealloc函数还是会被调用执行的,在这个函数里面,我们有时候需要释放CoreFoundation对象、remove observer、remove KVO以及一些其他资源( AudioServicesDisposeSystemSoundID(soundID); )

这里要注意的是:

不要在dealloc函数中释放Timer。NSTimer会保持一份target强引用,导致这个Class永远不会被释放。所以需要在对象Class销毁前做处理

在Github上有个开源项目MSWeakTimer

autoreleasepool

ARC仍然保留了AutoreleasePool,但是采用了新的Block语法,于是我们的main函数会如下修改:

1
2
3
4
5
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
int retVal = UIApplicationMain(argc, argv, nil,
NSStringFromClass([AppDelegate class]));
[pool release];
return retVal;

修改为:

1
2
3
4
5
@autoreleasepool {
int retVal = UIApplicationMain(argc, argv, nil,
NSStringFromClass([AppDelegate class]));
return retVal;
}

Core Foundation的内存管理-Toll-Free Bridge

在ARC下,objective-c对象与Core Foundation对象相互转换时,需要用到briage cast。

举个栗子:NSString和CFStringRef就可以同等对待,在任何地方都可以互换使用,背后的设计就是toll-free bridging。

在ARC之前,我们时这么处理的:

1
CFStringRef s1 = (CFStringRef) [[NSString alloc] initWithFormat:@"Hello, %@!", name];

当然,alloc分配了NSString对象,你需要在使用完之后进行释放,注意是释放转换后的CFStringRef对象:

1
CFRelease(s1);

反过来,从Core Foundation到Objective-C的方向也类似:

1
2
3
4
CFStringRef s2 = CFStringCreateWithCString(kCFAllocatorDefault, bytes, kCFStringEncodingMacRoman);
NSString *s3 = (NSString *)s2;
// release the object when you're done
[s3 release];

在ARC下,情况变得不一样!以下代码在手动内存管理中是完全合法的,但在ARC中却存在问题:

1
2
3
4
5
6
7
8
9
- (NSString *)escape:(NSString *)text
{
return [(NSString *)CFURLCreateStringByAddingPercentEscapes(
NULL,
(CFStringRef)text,
NULL,
(CFStringRef)@"!*'();:@&=+$,/?%#[]", // 这里不需要bridging casts,因为这是一个常量,不需要释放!
CFStringConvertNSStringEncodingToEncoding(NSUTF8StringEncoding)) autorelease];
}
在ARC下编译器必须知道由谁来负责释放转换后的对象

如果你把一个NSObject当作Core Foundation对象来使用,则ARC将不再负责释放该对象。但你必须明确地告诉ARC你的这个意图,编译器没办法自己做主。

同样如果你创建一个Core Foundation对象并把它转换为NSObject对象,你也必须告诉ARC占据对象的所有权,并在适当的时候释放该对象。这就是所谓的bridging casts。

上面代码中,
CFURLCreateStringByAddingPercentEscapes()函数的参数需要两个CFStringRef对象,其中常量NSString可以直接转换,因为不需要进行对象释放;但是 text 参数不一样,它是传递进来的一个NSString对象。而函数参数和局部变量一样,都是strong指针,这种对象在函数入口处会被retain,并且对象会持续存在直到指针被销毁(这里也就是函数返回时)。

对于text参数,我们希望ARC保持这个变量的所有权,同时又希望临时将它当作CFStringRef对象来使用。这种情况下可以使用__bridge说明符,它告诉ARC不要更改对象的所有权,按普通规则释放该对象即可。

多数情况下,Objective-C对象和Core Foundation对象之间互相转换时,我们都应该使用__bridge。

但是~它lei来了
但是有时候我们确实需要给予ARC某个对象的所有权,或者解除ARC对某个对象的所有权。这种情况下我们就需要使用另外两种

bridging casts:

  • __bridge_transfer:给予ARC所有权
  • __bridge_retained:解除ARC所有权

因为 CFURLCreateStringByAddingPercentEscapes() 函数创建了一个新的CFStringRef对象,而我们要的是NSString对象,因此我们要强制转换。

1
2
3
CFStringRef result = CFURLCreateStringByAddingPercentEscapes(. . .);
NSString *s = (NSString *)result;
return s;

当我们不需要强制转换为NSObject对象时,我们只需要调用CFRelease方法即可

1
2
// do something with the result 
CGRelease(result)
ARC只能作业于Objective-c对象。那么问题来了 既然我们创建了一个Core Foundation对象 谁来负责释放它?

这里需要我们使用 __bridge_transfer

__bridge_transfer 会告诉ARC:”Hi,ARC同学,好久不见。我这里有个货是一个CFStringRef对象,但是有人把它搞成了NSString对象了,我希望你来管(销毁)它,我这里就撒手不管了(调用CFRelease())”。

如果我们使用 __bridge,就会导致内存泄漏。ARC并不知道自己应该在使用完对象之后释放该对象,也没有人调用CFRelease()。结果这个对象就会永远保留在内存中。因此选择正确的 bridge 说明符是至关重要的。

为了代码更加可读和容易理解,iOS还提供了一个辅助函数:CFBridgingRelease()。函数所做事情和 __bridge_transfer 强制转换完全一样,但更加简洁和清晰。CFBridgingRelease() 函数定义为内联函数,因此不会导致额外的开销。函数之所以命名为CFBridgingRelease(),是因为一般你会在需要使用CFRelease()释放对象的地方,调用CFBridgingRelease()来传递对象的所有权。

清晰明了

因此最后我们的代码如下:

1
2
3
4
5
6
7
8
9
 - (NSString *)escape:(NSString *)text
{
return CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(
NULL,
(__bridge CFStringRef)text,
NULL,
CFSTR("!*'();:@&=+$,/?%#[]"),
CFStringConvertNSStringEncodingToEncoding(NSUTF8StringEncoding)));
}

另一个常见的需要CFBridgingRelease的情况是AddressBook framework:

1
2
3
4
- (NSString *)firstName
{
return CFBridgingRelease(ABRecordCopyCompositeName(...));
}

总结:只要你调用命名为Create, Copy, Retain的Core Foundation函数,你都需要使用 CFBridgingRelease() 安全地将值传递给ARC。

bridge_retained 则正好相反,假设你有一个NSString对象,并且要将它传递给某个Core Foundation API,该函数希望接收这个string对象的所有权。这时候你就不希望ARC也去释放该对象,否则就会对同一对象释放两次,而且必将导致应用崩溃!换句话说,使用 bridge_retained 将对象的所有权给予 Core Foundation,而ARC不再负责释放该对象。

如下面例子所示:

1
2
3
4
5
NSString *s1 = [[NSString alloc] initWithFormat:@"Hello, %@!", name];
CFStringRef s2 = (__bridge_retained CFStringRef)s1;
// do something with s2
// . . .
CFRelease(s2);

一旦 (bridge_retained CFStringRef) 转换完成,ARC就不再负责释放该对象。如果你在这里使用 bridge,应用就很可能会崩溃。ARC可能在Core Foundation正在使用该对象时,释放掉它。

同样 __bridge_retained 也有一个辅助函数:CFBridgingRetain()。从名字就可以看出,这个函数会让Core Foundation执行retain,实际如下:

1
2
3
CFStringRef s2 = CFBridgingRetain(s1);
// . . .
CFRelease(s2);

现在你应该明白了,上面例子的CFRelease()是和CFBridgingRetain()对应的。

总结:

  • 使用 CFBridgingRelease(),从Core Foundation传递所有权给Objective-C;
  • 使用 CFBridgingRetain() ,从Objective-C传递所有权给Core Foundation;
  • 使用__brideg,表示临时使用某种类型,不改变对象的所有权。

Delegate和Weak Property

使用Delegate模式时,通常我们会使用weak property来引用delegate,这样可以避免所有权回环。

retain循环引用的概念,两个对象互相retain时,会导致两个对象都无法被释放,这也是内存泄漏的常见原因之一。因此需要你使用weak指针来避免。

还有Block使用不当导致的循环引用,下面会提到

1
2
3
DetailViewController *controller = [[DetailViewController alloc] initWithNibName:@"DetailViewController" bundle:nil];
controller.delegate = self;
[self presentViewController:controller animated:YES completion:nil];

在上面代码中,MainViewController创建一个DetailViewController,并调用presentViewController将view呈现出来,从而拥有了一个strong指针指向创建的DetailViewController对象。反过来,DetailViewController也通过delegate拥有了一个指向MainViewController的weak指针.

当MainViewController调用dismissViewControllerAnimated:时,就会自动失去DetailViewController的strong引用,这时候DetailViewController对象就会被自动释放。

如果这两个指针都是strong类型,就会出现所有权回环。导致对象无法在适当的时候被释放。

unsafe_unretained

除了strong和weak,还有另外一个unsafe_unretained关键字,一般你不会使用到它。声明为unsafe_unretained的变量或property,编译器不会为其自动添加retain和release。unsafe_unretained只是为了兼容iOS 4,因为iOS 4没有weak pointer system。

这里大家做个了解就可以


ARC和Block


ARC和Singleton

MRC

如果你的应用使用了Singleton,你的实现可能包含以下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 + (id)allocWithZone:(NSZone *)zone
{
return [[self sharedInstance] retain];
}
- (id)copyWithZone:(NSZone *)zone
{
return self;
}
- (id)retain
{
return self;
}
- (NSUInteger)retainCount
{
return NSUIntegerMax;
}
- (oneway void)release
{
// empty
}
- (id)autorelease
{
return self;
}

这是典型的singleton实现模式,retain和release都覆盖掉,使其不能创建多个实例对象。毕竟Singleton就是为了只创建一个全局对象。

ARC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#import "AppManager.h"

static AppManager *_appManager = nil;

@implementation AppManager

+ (instancetype)sharedInstance {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_appManager = [[super allocWithZone:NULL] init];
});
return _appManager;
}

+ (instancetype)allocWithZone:(struct _NSZone *)zone {
return [self sharedInstance];
}

@end
1
2
+ (id)copyWithZone:(struct _NSZone *)zone OBJC_ARC_UNAVAILABLE;
+ (id)mutableCopyWithZone:(struct _NSZone *)zone OBJC_ARC_UNAVAILABLE;

在ARC中,所有指针变量默认都是nil,在ARC之前,只有实例变量才会默认为nil。如果你编写下面代码:

1
2
3
4
5
6
7
 - (void)myMethod
{
int someNumber;
NSLog(@"Number: %d", someNumber);
NSString *someString;
NSLog(@"String: %p", someString);
}

编译器会:”Variable is uninitialized when used here”,而输出则是随机数值:

Woot[2186:207] Number: 67

Woot[2186:207] String: 0x4babb5

但在ARC中,输出则如下:

1
2
Artists[2227:207] Number: 10120117
Artists[2227:207] String: 0x0

int仍然是随机值(这样使用编译器也会警告),但someString的初始值已经是nil,这样的优点是指针永远不会指向非法对象。

autorelease对象可能会比你想象中存活更长时间,在iOS中,每次UI事件(点击按钮等)都会清空一次 autorelease pool(具体应该与RunLoop相关,后面文章会说到),但是如果你的事件处理器进行了大量操作,例如循环地创建许多对象,最好是使用你自己的 autorelease pool,避免应用面临内存不足:

1
2
3
4
5
6
7
for (int i = 0; i < 10000; i++)
{
@autoreleasepool
{
NSString *s = [NSString stringWithFormat:. . .];
}
}

最后

此篇整理于2013年,如有错误,敬请斧正。