Objective-C中的__weak与__strong变量修饰符语义
一、Objective-C 内存管理基础回顾
在深入探讨 __weak
与 __strong
变量修饰符之前,先简单回顾一下 Objective-C 的内存管理机制。Objective-C 使用引用计数(Reference Counting)来管理对象的生命周期。当一个对象被创建时,它的引用计数初始化为 1。每当有一个新的变量指向这个对象时,引用计数就会增加 1;而当指向该对象的变量不再使用(例如超出作用域或被赋值为 nil
)时,引用计数就会减 1。当对象的引用计数变为 0 时,系统会自动释放该对象所占用的内存。
例如,以下是一个简单的对象创建和释放的示例:
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 创建一个 NSString 对象
NSString *string = [[NSString alloc] initWithString:@"Hello, World!"];
// 这里 string 指向的对象引用计数为 1
NSLog(@"%@", string);
// string 超出作用域,对象引用计数减 1,由于此时引用计数变为 0,对象被释放
}
return 0;
}
在 ARC(自动引用计数,Automatic Reference Counting)出现之前,开发者需要手动调用 retain
来增加引用计数,调用 release
来减少引用计数。ARC 则让编译器自动插入这些 retain
和 release
调用,大大减轻了开发者的负担。
二、__strong 变量修饰符
1. __strong 的基本语义
__strong
是 ARC 下对象变量的默认修饰符。一个被 __strong
修饰的变量会持有指向对象的强引用。这意味着只要有一个 __strong
变量指向某个对象,该对象的引用计数就会至少为 1,不会被释放。
看下面的代码示例:
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@end
@implementation Person
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
// person 是一个 __strong 变量,持有 Person 对象的强引用
person.name = @"John";
NSLog(@"Person's name: %@", person.name);
// person 超出作用域,Person 对象引用计数减 1,由于 name 也是 __strong 类型,持有 NSString 对象的强引用,所以 NSString 对象不会被释放
}
return 0;
}
在上述代码中,person
变量是 __strong
类型,它持有 Person
对象的强引用。Person
类中的 name
属性默认也是 __strong
类型,它持有 NSString
对象的强引用。所以在 person
超出作用域之前,Person
对象和 NSString
对象都不会被释放。
2. __strong 变量之间的赋值
当一个 __strong
变量被赋值给另一个 __strong
变量时,会发生引用计数的变化。具体来说,目标变量会持有源变量所指向对象的强引用,源变量对该对象的引用计数会减 1。如果源变量之前是唯一指向该对象的 __strong
变量,那么对象的引用计数会变为 0 并被释放。
以下是示例代码:
#import <Foundation/Foundation.h>
@interface MyClass : NSObject
@end
@implementation MyClass
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
MyClass *obj1 = [[MyClass alloc] init];
// obj1 持有 MyClass 对象的强引用,引用计数为 1
MyClass *obj2 = obj1;
// obj2 也持有 MyClass 对象的强引用,引用计数变为 2,obj1 引用计数不变
obj1 = nil;
// obj1 对 MyClass 对象的引用计数减 1,此时 MyClass 对象引用计数为 1,因为 obj2 还持有强引用,所以对象不会被释放
NSLog(@"obj2 still exists");
}
return 0;
}
在这个例子中,obj1
首先创建并指向 MyClass
对象,引用计数为 1。当 obj2 = obj1
时,obj2
也指向该对象,引用计数变为 2。当 obj1 = nil
时,obj1
对对象的引用计数减 1,但由于 obj2
仍然持有强引用,所以 MyClass
对象不会被释放。
3. __strong 变量与方法参数
当一个 __strong
变量作为方法参数传递时,方法内部会对该对象持有一个强引用。这意味着在方法执行期间,对象不会被释放。
例如:
#import <Foundation/Foundation.h>
@interface Helper : NSObject
+ (void)printObject:(id)obj;
@end
@implementation Helper
+ (void)printObject:(id)obj {
NSLog(@"Object: %@", obj);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSString *string = [[NSString alloc] initWithString:@"Test"];
[Helper printObject:string];
// 方法执行完毕后,虽然方法内部对 string 对象的强引用消失,但外部 string 变量仍然持有强引用,所以 string 对象不会被释放
}
return 0;
}
在 printObject:
方法中,obj
参数是一个 __strong
类型(默认),它持有传递进来的 string
对象的强引用。方法执行完毕后,obj
对 string
对象的引用计数减 1,但由于外部的 string
变量仍然持有强引用,所以 string
对象不会被释放。
三、__weak 变量修饰符
1. __weak 的基本语义
__weak
变量修饰符用于创建一个指向对象的弱引用。与 __strong
不同,__weak
变量不会增加对象的引用计数。当对象的所有 __strong
引用都消失,对象被释放时,所有指向该对象的 __weak
变量会自动被设置为 nil
。
以下是一个简单的示例:
#import <Foundation/Foundation.h>
@interface MyObject : NSObject
@end
@implementation MyObject
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
__strong MyObject *strongObj = [[MyObject alloc] init];
__weak MyObject *weakObj = strongObj;
// weakObj 指向 strongObj 所指向的对象,但不增加对象的引用计数
NSLog(@"weakObj: %@", weakObj);
strongObj = nil;
// strongObj 对 MyObject 对象的强引用消失,对象被释放,此时 weakObj 自动被设置为 nil
NSLog(@"weakObj after strongObj is nil: %@", weakObj);
}
return 0;
}
在上述代码中,weakObj
是一个 __weak
变量,它指向 strongObj
所指向的 MyObject
对象,但不会增加对象的引用计数。当 strongObj
被设置为 nil
时,MyObject
对象的引用计数变为 0 并被释放,同时 weakObj
自动被设置为 nil
。这样可以避免野指针(dangling pointer)的问题,提高程序的安全性。
2. __weak 用于解决循环引用问题
循环引用是在内存管理中常见的问题,特别是在对象之间存在相互引用时容易发生。例如,假设有两个类 ClassA
和 ClassB
,ClassA
持有一个 ClassB
的实例,ClassB
又持有一个 ClassA
的实例,如果这两个引用都是 __strong
类型,就会形成循环引用,导致对象无法被释放。
以下是循环引用的示例代码:
#import <Foundation/Foundation.h>
@interface ClassB;
@interface ClassA : NSObject
@property (nonatomic, strong) ClassB *classB;
@end
@interface ClassB : NSObject
@property (nonatomic, strong) ClassA *classA;
@end
@implementation ClassA
@end
@implementation ClassB
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
ClassA *a = [[ClassA alloc] init];
ClassB *b = [[ClassB alloc] init];
a.classB = b;
b.classA = a;
// a 和 b 之间形成循环引用,即使 a 和 b 超出作用域,它们所指向的对象也不会被释放
}
return 0;
}
为了解决这个问题,可以将其中一个引用改为 __weak
类型。例如:
#import <Foundation/Foundation.h>
@interface ClassB;
@interface ClassA : NSObject
@property (nonatomic, strong) ClassB *classB;
@end
@interface ClassB : NSObject
@property (nonatomic, weak) ClassA *classA;
@end
@implementation ClassA
@end
@implementation ClassB
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
ClassA *a = [[ClassA alloc] init];
ClassB *b = [[ClassB alloc] init];
a.classB = b;
b.classA = a;
// 这里 classB 是 __strong 引用,classA 是 __weak 引用,不会形成循环引用,当 a 和 b 超出作用域时,它们所指向的对象会被正确释放
}
return 0;
}
在这个修改后的代码中,ClassB
中的 classA
属性被声明为 __weak
类型,这样就打破了循环引用。当 a
和 b
超出作用域时,ClassA
对象的引用计数会因为 a
的消失而变为 0 并被释放,此时 ClassB
对象中 classA
指向的 ClassA
对象已经不存在,classA
会自动被设置为 nil
。接着,ClassB
对象的引用计数也会因为 b
的消失和 classA
不再持有强引用而变为 0 并被释放。
3. __weak 变量在闭包(Block)中的使用
在使用闭包(Block)时,如果闭包捕获了一个 __strong
变量,可能会导致循环引用。例如:
#import <Foundation/Foundation.h>
@interface MyViewController : UIViewController
@property (nonatomic, strong) NSString *titleText;
@end
@implementation MyViewController
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf) {
strongSelf.titleText = @"New Title";
}
});
}
@end
在上述代码中,MyViewController
类有一个 titleText
属性。在 viewDidLoad
方法中,创建了一个延迟执行的闭包。如果直接在闭包中使用 self
,会捕获 self
的强引用,形成循环引用(因为闭包被持有在队列中,而闭包又持有 self
的强引用)。为了避免这种情况,先创建一个 __weak
类型的 weakSelf
来捕获 self
。在闭包内部,再将 weakSelf
赋值给一个 __strong
类型的 strongSelf
,这样在闭包执行期间,self
不会被意外释放,同时也避免了循环引用。
四、__weak 与 __strong 的性能考量
1. __strong 的性能
__strong
变量的操作相对简单直接,因为它只是增加和减少对象的引用计数。编译器在处理 __strong
变量时,会在合适的位置插入 retain
和 release
调用(在 ARC 下自动完成)。由于 __strong
变量持有对象的强引用,只要有 __strong
变量指向对象,对象就不会被释放,这在某些情况下可能会导致不必要的内存占用。
例如,在一个频繁创建和销毁对象的场景中,如果对象被 __strong
变量长时间持有,可能会影响内存的回收效率,导致内存峰值升高。
2. __weak 的性能
__weak
变量的实现相对复杂一些。由于 __weak
变量需要在对象被释放时自动设置为 nil
,系统需要额外的机制来跟踪对象的生命周期。这通常涉及到一个弱引用表(weak reference table),当对象的引用计数变为 0 时,系统会遍历这个表,将所有指向该对象的 __weak
变量设置为 nil
。
虽然 __weak
变量不会增加对象的引用计数,有助于避免循环引用和及时释放不再使用的对象,但由于其实现机制,在性能上会有一定的开销。特别是在大量使用 __weak
变量的情况下,对弱引用表的操作可能会成为性能瓶颈。
因此,在选择使用 __weak
还是 __strong
变量时,不仅要考虑内存管理的正确性,还要根据具体的应用场景和性能需求来权衡。如果对象之间的关系比较简单,不存在循环引用的风险,使用 __strong
变量通常可以获得更好的性能。而在可能出现循环引用的情况下,或者需要对象被释放后自动清理引用的场景,__weak
变量则是更好的选择。
五、__weak 和 __strong 在多线程环境下的行为
1. __strong 在多线程环境下
在多线程环境中,__strong
变量的引用计数操作并非原子性的。这意味着如果多个线程同时对同一个对象的 __strong
变量进行赋值或释放操作,可能会导致数据竞争和未定义行为。
例如,假设线程 A 正在释放一个对象(减少其引用计数),而线程 B 同时尝试访问该对象,由于引用计数操作不是原子的,可能在线程 A 减少引用计数到 0 并释放对象后,线程 B 仍然认为对象存在并尝试访问,从而导致程序崩溃。
为了避免这种情况,在多线程环境下对 __strong
变量的操作通常需要使用锁(如 @synchronized
或 NSLock
)来保证线程安全。
以下是一个简单的示例,展示如何使用 @synchronized
来保护 __strong
变量的操作:
#import <Foundation/Foundation.h>
@interface SharedObject : NSObject
@property (nonatomic, strong) NSString *data;
@end
@implementation SharedObject
@end
void threadFunction(SharedObject *sharedObj) {
@synchronized(sharedObj) {
NSString *localData = sharedObj.data;
// 对 localData 进行操作,这里假设是一些复杂的计算
NSLog(@"Thread processed data: %@", localData);
}
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
SharedObject *sharedObject = [[SharedObject alloc] init];
sharedObject.data = @"Initial Data";
NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(threadFunction:) object:sharedObject];
NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(threadFunction:) object:sharedObject];
[thread1 start];
[thread2 start];
[thread1 join];
[thread2 join];
}
return 0;
}
在这个示例中,@synchronized(sharedObj)
确保了在同一时间只有一个线程可以访问和操作 sharedObj
的 data
属性,从而避免了数据竞争。
2. __weak 在多线程环境下
__weak
变量在多线程环境下也存在一些潜在的问题。由于 __weak
变量的更新(当对象被释放时设置为 nil
)涉及到系统对弱引用表的操作,多个线程同时访问和修改弱引用表可能会导致竞争条件。
例如,假设线程 A 正在释放一个对象,系统开始更新弱引用表,将所有指向该对象的 __weak
变量设置为 nil
。同时,线程 B 可能正在读取其中一个 __weak
变量的值,由于弱引用表的更新不是原子的,线程 B 可能会读取到不一致的数据,即 __weak
变量还没有被完全更新为 nil
时的值。
为了在多线程环境中安全地使用 __weak
变量,同样需要采取同步措施。可以使用与 __strong
变量类似的锁机制,确保在对 __weak
变量进行操作时,不会发生数据竞争。
另外,在多线程环境下,使用 __weak
变量时还需要注意对象的生命周期管理。由于多个线程可能同时影响对象的引用计数,需要更加谨慎地确保对象在合适的时机被释放,并且 __weak
变量能够正确地更新为 nil
。
六、__weak 和 __strong 与其他内存管理概念的结合
1. __weak 和 __strong 与 autorelease
autorelease
是 Objective-C 内存管理中的一个重要概念,它允许对象在自动释放池(@autoreleasepool
)结束时自动释放。在 ARC 环境下,编译器会自动插入 autorelease
调用。
__strong
和 __weak
变量与 autorelease
相互配合。当一个对象被发送 autorelease
消息时,它的引用计数会增加(但增加的方式与 retain
略有不同),并且会被放入最近的自动释放池中。当自动释放池被销毁时,池中的所有对象会收到 release
消息。
例如:
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSString *string = [[[NSString alloc] initWithString:@"Autorelease Example"] autorelease];
// string 是 __strong 类型,持有 NSString 对象的强引用,同时对象被放入自动释放池
NSLog(@"%@", string);
}
// 自动释放池结束,string 所指向的对象收到 release 消息,如果此时对象的引用计数变为 0,则会被释放
return 0;
}
在这个例子中,string
是 __strong
类型,持有 NSString
对象的强引用。同时,对象通过 autorelease
被放入自动释放池。当自动释放池结束时,对象会收到 release
消息。如果此时对象的引用计数(除了 string
的强引用外,可能还有其他引用)变为 0,对象就会被释放。
对于 __weak
变量,如果一个对象被 autorelease
后,在自动释放池销毁前,对象的所有 __strong
引用都消失,对象被释放,那么指向该对象的 __weak
变量会自动被设置为 nil
。
2. __weak 和 __strong 与内存缓存
在内存缓存(如 NSCache
)的场景中,__strong
和 __weak
变量也有着不同的应用。NSCache
是一个类似于字典的容器,用于缓存对象。它的特点是当系统内存不足时,会自动释放缓存中的对象。
通常,缓存中的对象会被 __strong
引用,这样可以确保对象在缓存中不会被意外释放。例如:
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSCache *cache = [[NSCache alloc] init];
NSString *key = @"SomeKey";
NSString *value = [[NSString alloc] initWithString:@"Cached Value"];
[cache setObject:value forKey:key];
// cache 使用 __strong 引用持有 value 对象,确保对象在缓存中不会被释放
NSString *retrievedValue = [cache objectForKey:key];
NSLog(@"Retrieved value: %@", retrievedValue);
}
return 0;
}
然而,在某些情况下,可能需要使用 __weak
引用。例如,如果缓存中的对象本身又持有其他对象,并且希望在缓存对象被释放时,这些被持有的对象也能正确释放,避免循环引用,可以考虑使用 __weak
。
另外,在处理缓存的淘汰策略时,如果希望在缓存对象被淘汰后,与之相关的其他对象(通过 __weak
引用)能自动清理,__weak
变量就非常有用。
七、总结 __weak 与 __strong 的使用场景
- 使用 __strong 的场景
- 简单对象持有:当一个对象需要被另一个对象直接持有,并且不存在循环引用风险时,使用
__strong
是最简单和高效的选择。例如,一个视图控制器持有它所管理的视图对象,这些视图对象通常不会反过来持有视图控制器,所以可以使用__strong
引用。 - 方法参数和局部变量:在方法内部作为参数传递或定义的局部变量,如果需要在方法执行期间确保对象不会被释放,使用
__strong
。因为__strong
变量的操作简单直接,性能开销小。
- 简单对象持有:当一个对象需要被另一个对象直接持有,并且不存在循环引用风险时,使用
- 使用 __weak 的场景
- 解决循环引用:在对象之间可能存在相互引用,容易形成循环引用的情况下,使用
__weak
来打破循环。如前面提到的ClassA
和ClassB
相互引用的例子,将其中一个引用改为__weak
可以确保对象能被正确释放。 - 避免野指针:当希望在对象被释放后,指向该对象的变量自动变为
nil
,避免野指针错误时,使用__weak
。这在处理视图控制器之间的父子关系,或者对象之间的临时关联时非常有用。 - 闭包中的使用:在闭包捕获对象时,为了避免循环引用,通常先使用
__weak
捕获对象,然后在闭包内部再根据需要转换为__strong
引用,以确保在闭包执行期间对象不会被释放。
- 解决循环引用:在对象之间可能存在相互引用,容易形成循环引用的情况下,使用
总之,正确理解和使用 __weak
与 __strong
变量修饰符对于编写高效、健壮的 Objective-C 代码至关重要。在实际编程中,需要根据具体的对象关系、内存管理需求以及性能要求来合理选择使用哪种修饰符。