Objective-C 采用的是引用计数式的内存管理方式:
在 ARC 环境下,id 类型和对象类型和 C 语言其他类型不同,类型前必须加上所有权的修饰符。 所有权修饰符总共有4种:
__strong
表示强引用,对应定义 property
时用到的 strong
。当对象没有任何一个强引用指向它时,它才会被释放。如果在声明引用时不加修饰符,那么引用将默认是强引用。当需要释放强引用指向的对象时,需要保证所有指向对象强引用置为 nil
。__strong
修饰符是 id
类型和对象类型默认的所有权修饰符。
__weak
表示弱引用,对应定义 property
时用到的 weak
。弱引用不会影响对象的释放,而当对象被释放时,所有指向它的弱引用都会自定被置为 nil
,这样可以防止野指针。__weak
最常见的一个作用就是用来避免强引用循环。
__weak
的几个使用场景:
Delegate
关系中防止强引用循环。在 ARC 特性下,通常我们应该设置 Delegate
属性为 weak
的。但是这里有一个疑问,我们常用到的 UITableView
的 delegate
属性是这样定义的: @property (nonatomic, assign) id<UITableViewDelegate> delegate;
,为什么用的修饰符是 assign
而不是 weak
?其实这个 assign
在 ARC 中意义等同于 __unsafe_unretained
(后面会讲到),它是为了在 ARC
特性下兼容 iOS4
及更低版本来实现弱引用机制。一般情况下,你应该尽量使用 weak
。Block
中防止强引用循环。Interface Builder
创建的控件。比如:@property (nonatomic, weak) IBOutlet UIButton *testButton;
。另外,__weak
修饰符的变量,会被注册到 autoreleasePool
中。
{
id __weak obj1 = obj;
NSLog(@"obj2-%@",obj1);
}
编译器转换上述代码如下:
id obj1;
objc_initweak(&obj1,obj);
id tmp = objc_loadWeakRetained(&obj1);
objc_autorelease(tmp);
NSLog(@"%@",tmp);
objc_destroyWeak(&obj1);
objc_loadWeakRetained
函数获取附有 __weak
修饰符变量所引用的对象并 retain
, objc_autorelease
函数将对象放入 autoreleasePool
中,据此当我们访问 weak
修饰指针指向的对象时,实际上是访问注册到自动释放池的对象。因此,如果大量使用 weak
的话,在我们去访问 weak
修饰的对象时,会有大量对象注册到自动释放池,这会影响程序的性能。
解决方案:
要访问 weak
修饰的变量时,先将其赋给一个 strong
变量,然后进行访问。
为什么访问 weak
修饰的对象就会访问注册到自动释放池的对象呢?
因为 weak
不会引起对象的引用计数器变化,因此,该对象在运行过程中很有可能会被释放。所以,需要将对象注册到自动释放池中并在 autoreleasePool
销毁时释放对象占用的内存。
在 ARC
模式下,我们不能显示的使用 autorelease
方法了,但是 autorelease
的机制还是有效的,通过将对象赋给 __autoreleasing
修饰的变量就能达到在 MRC
模式下调用对象的 autorelease
方法同样的效果。
__autoreleasing
修饰的对象会被注册到 Autorelease Pool
中,并在 Autorelease Pool
销毁时被释放。
注意:定义 property
时不能使用这个修饰符,因为任何一个对象的 property
都不应该是 autorelease
类型的。
ARC
是在 iOS5
引入的,而 __unsafe_unretained
这个修饰符主要是为了在 ARC
刚发布时兼容 iOS4
以及版本更低的系统,因为这些版本没有弱引用机制。这个修饰符在定义 property
时对应的是 unsafe_unretained
。__unsafe_unretained
修饰的指针纯粹只是指向对象,没有任何额外的操作,不会去持有对象使得对象的 retainCount +1
。而在指向的对象被释放时依然原原本本地指向原来的对象地址,不会被自动置为 nil
,所以成为了野指针,非常不安全。
__unsafe_unretained
的应用场景:
__unsafe_unretained
替代 __weak
解决强引用循环的问题。总结, autorelease
的机制却依然在很多地方默默起着作用,我们来看看这些场景:
首先,我们看这个方法:
- (NSObject *)object {
NSObject *obj = [[NSObject alloc] init];
return obj;
}
这里 obj
的所有权修饰符是默认的 __strong
。由于 return
使得 obj
超出其作用域,它强引用持有的对象本该被释放,但是由于该对象作为函数返回值,所以一般情况下编译器会自动将其注册到 Autorelease Pool
中(注意这里是一般情况下,在一些特定情况下,ARC
机制提出了巧妙的运行时优化方案来跳过 autorelease
机制,见后面章节)。
那么这里有一个问题:为什么方法返回值的时候需要用到 autorelease 机制呢?
这涉及到两个角色的问题。一个角色是调用方法接收返回值的接收方。当参数被作为返回值 return 之后,接收方如果要接着使用它就需要强引用它,使它 retainCount +1,用完后再清理,使它 retainCount -1。有持有就有清理,这是接收方的责任。另一个角色就是返回对象的方法,即提供方。在方法中创建了对象并作为返回值时,一方面你创建了这个对象你就得负责释放它,有创建就有释放,这是创建者的责任。另一方面你得保证返回时对象没被释放以便方法外的接收方能拿到有效的对象,否则你返回的是 nil,有何意义呢。所以就需要找一个合理的机制既能延长这个对象的生命周期,又能保证对其释放。这个机制就是 autorelease 机制
。
当对象作为参数从方法返回时,会被放到正在使用的 Autorelease Pool 中,由这个 Autorelease Pool 强引用这个对象而不是立即释放,从而延长了对象的生命周期,Autorelease Pool 自己销毁的时候会把它里面的对象都顺手清理掉,从而保证了对象会被释放。但是这里也引出另一个问题:既然会延长对象的生命周期到 Autorelease Pool 被销毁的时候,那么 Autorelease Pool 的生命周期是多久呢?会不会在 Autorelease Pool 都销毁了,接收方还没接收到对象呢?
在访问 __weak
修饰的变量时,实际上必定会访问注册到 Autorelease Pool 的对象。如下来年两段代码是相同的效果:
id __weak obj1 = obj0;
NSLog(@"class=%@", [obj1 class]);
// 等同于:
id __weak obj1 = obj0;
id __autoreleasing tmp = obj1;
NSLog(@"class=%@", [tmp class]);
为什么会这样呢?因为 __weak
修饰符只持有对象的弱引用,而在访问对象的过程中,该对象有可能被废弃,如果把被访问的对象注册到 Autorelease Pool 中,就能保证 Autorelease Pool 被销毁前对象是存在的。
另一个隐式地使用 __autoreleasing
的例子就是使用 id 的指针或对象的指针(id *) 的时候。
看一个最常见的例子:
NSError *__autoreleasing error;
if (![data writeToFile:filename options:NSDataWritingAtomic error:&error]) {
NSLog(@"Error: %@", error);
}
// 即使上面你没有写 __autoreleasing 来修饰 error,编译器也会帮你做下面的事情:
NSError *error;
NSError *__autoreleasing tempError = error; // 编译器添加
if (![data writeToFile:filename options:NSDataWritingAtomic error:&tempError])
{
error = tempError; // 编译器添加
NSLog(@"Error: %@", error);
}
error 对象在你调用的方法中被创建,然后被放到 Autorelease Pool 中,等到使用结束后随着 Autorelease Pool 的销毁而释放,所以函数外 error 对象的使用者不需要关心它的释放。
在 ARC 中,所有这种指针的指针类型(id *)的函数参数如果不加修饰符,编译器会默认将他们认定为 __autoreleasing
类型。
有一点特别需要注意的是,某些类的方法会隐式地使用自己的 Autorelease Pool,在这种时候使用 __autoreleasing
类型要特别小心。比如 NSDictionary 的 enumerateKeysAndObjectsUsingBlock 方法:
- (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,类似于:
- (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 后被释放,正确的方式如下:
- (void)loopThroughDictionary:(NSDictionary *)dict error:(NSError **)error {
NSError * __block 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;
}
}