Objective-C属性修饰符(strong、weak、copy)详解
一、Objective-C 属性修饰符概述
在Objective-C 编程中,属性(properties)是一种用于封装对象数据的机制。属性修饰符则用于定义属性的行为,比如内存管理方式、访问控制等。其中,strong
、weak
和 copy
是非常重要且常用的内存管理相关的修饰符,它们决定了对象之间的引用关系以及对象何时被释放,深刻理解它们对于写出健壮、高效且内存安全的代码至关重要。
二、strong
修饰符
2.1 strong
的含义
strong
修饰符表示一种强引用关系。当一个对象被一个 strong
类型的属性引用时,这个对象的引用计数会增加。只要至少有一个 strong
引用指向该对象,这个对象就不会被释放。也就是说,strong
引用会“持有”对象,确保对象在需要时不会被销毁。
2.2 strong
的内存管理机制
在ARC(自动引用计数)环境下,strong
修饰符的内存管理是由编译器自动处理的。当一个 strong
引用指向一个新的对象时,对象的引用计数增加;当 strong
引用被释放(比如属性被赋值为 nil
或者包含该属性的对象被销毁)时,对象的引用计数减少。当对象的引用计数降为 0 时,对象的内存就会被自动释放。
下面通过一段简单的代码示例来展示 strong
修饰符的工作原理:
#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.name = @"John";
NSLog(@"Person's name: %@", person.name);
}
return 0;
}
在上述代码中,Person
类有一个 strong
修饰的 name
属性。当我们创建一个 Person
对象并为 name
属性赋值为 @"John"
时,@"John"
这个字符串对象的引用计数增加。在 @autoreleasepool
块结束时,person
对象被销毁,其 name
属性的 strong
引用也被释放,@"John"
字符串对象的引用计数减少。由于此时没有其他 strong
引用指向 @"John"
,它的引用计数降为 0 并被释放(在ARC环境下自动完成)。
2.3 strong
的适用场景
strong
修饰符适用于大多数需要持有对象的情况。比如,当一个对象包含另一个对象,并且希望被包含的对象的生命周期与包含它的对象相关联时,就应该使用 strong
。例如,一个 Car
类包含一个 Engine
类的实例,Car
对象持有 Engine
对象,只要 Car
对象存在,Engine
对象就应该存在,这时就可以使用 strong
修饰 Engine
属性。
#import <Foundation/Foundation.h>
@interface Engine : NSObject
@end
@implementation Engine
@end
@interface Car : NSObject
@property (nonatomic, strong) Engine *engine;
@end
@implementation Car
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Car *car = [[Car alloc] init];
car.engine = [[Engine alloc] init];
NSLog(@"Car has an engine.");
}
return 0;
}
在这个例子中,Car
对象通过 strong
引用持有 Engine
对象,确保在 Car
对象的生命周期内 Engine
对象不会被释放。
2.4 strong
可能导致的问题 - 循环引用
虽然 strong
修饰符在大多数情况下很有用,但如果使用不当,会导致循环引用的问题。循环引用是指两个或多个对象之间相互持有 strong
引用,使得它们的引用计数永远不会降为 0,从而导致内存泄漏。
以下是一个简单的循环引用示例:
#import <Foundation/Foundation.h>
@interface ClassA : NSObject
@property (nonatomic, strong) ClassB *classB;
@end
@implementation ClassA
@end
@interface ClassB : NSObject
@property (nonatomic, strong) ClassA *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;
}
return 0;
}
在上述代码中,ClassA
和 ClassB
相互持有对方的 strong
引用。当 @autoreleasepool
块结束时,a
和 b
变量超出作用域,它们对 ClassA
和 ClassB
对象的 strong
引用被释放。但是,由于 ClassA
对象持有 ClassB
对象的 strong
引用,ClassB
对象也持有 ClassA
对象的 strong
引用,这两个对象的引用计数都不会降为 0,从而导致内存泄漏。
三、weak
修饰符
3.1 weak
的含义
weak
修饰符表示一种弱引用关系。与 strong
不同,weak
引用不会增加对象的引用计数。当对象的所有 strong
引用都被释放后,对象会被销毁,此时所有指向该对象的 weak
引用会自动被设置为 nil
,这就是所谓的“弱引用归零”特性。
3.2 weak
的内存管理机制
weak
修饰符在内存管理方面依赖于运行时系统。运行时会维护一个全局的弱引用表,记录所有的 weak
引用及其指向的对象。当对象的引用计数变为 0 并即将被释放时,运行时会遍历弱引用表,将所有指向该对象的 weak
引用设置为 nil
。
以下是一个展示 weak
修饰符工作原理的代码示例:
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@end
@implementation Person
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
__weak Person *weakPerson;
{
Person *strongPerson = [[Person alloc] init];
strongPerson.name = @"Jane";
weakPerson = strongPerson;
NSLog(@"Weak person's name: %@", weakPerson.name);
}
NSLog(@"Weak person after strong person is deallocated: %@", weakPerson);
}
return 0;
}
在上述代码中,我们首先定义了一个 __weak
类型的 weakPerson
变量。在一个内部块中,我们创建了一个 strongPerson
对象,并将 weakPerson
指向 strongPerson
。此时,weakPerson
是一个弱引用,不会增加 strongPerson
的引用计数。当内部块结束时,strongPerson
超出作用域,其 strong
引用被释放,strongPerson
对象被销毁。由于 weakPerson
是弱引用,它会自动被设置为 nil
,所以最后打印 weakPerson
时,输出为 nil
。
3.3 weak
的适用场景
weak
修饰符常用于解决循环引用的问题。例如,在视图控制器(UIViewController
)和视图(UIView
)之间的关系中,视图控制器通常通过 strong
引用持有视图,而视图不需要持有视图控制器,否则会导致循环引用。这时,视图可以使用 weak
引用指向视图控制器。
#import <UIKit/UIKit.h>
@interface CustomView : UIView
@property (nonatomic, weak) UIViewController *viewController;
@end
@implementation CustomView
@end
@interface ViewController : UIViewController
@property (nonatomic, strong) CustomView *customView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.customView = [[CustomView alloc] initWithFrame:self.view.bounds];
self.customView.viewController = self;
[self.view addSubview:self.customView];
}
@end
在上述代码中,ViewController
通过 strong
引用持有 CustomView
,而 CustomView
通过 weak
引用指向 ViewController
,避免了循环引用。
另外,weak
修饰符还适用于当一个对象的生命周期不应该影响另一个对象的生命周期,但是又需要在某些情况下引用它的场景。比如,一个通知中心(NSNotificationCenter
)的观察者可能希望观察某个对象的通知,但不希望持有该对象,以免影响其正常的生命周期,这时就可以使用 weak
引用。
3.4 weak
的局限性
虽然 weak
修饰符在解决循环引用等问题上非常有用,但它也有一些局限性。由于 weak
引用不会增加对象的引用计数,所以如果对象的唯一引用是 weak
引用,那么对象可能会在不经意间被销毁。此外,在多线程环境下,由于 weak
引用的归零操作是在运行时异步进行的,可能会出现短暂的“悬垂指针”问题,即 weak
引用还未被设置为 nil
时,对象已经被销毁,这需要开发者在多线程编程中特别注意。
四、copy
修饰符
4.1 copy
的含义
copy
修饰符用于创建对象的副本。当一个属性被声明为 copy
时,在赋值操作时,会对赋值的对象进行复制操作,而不是简单地增加引用计数。这意味着,赋值后,属性持有一个新的对象副本,而不是原始对象的引用。
4.2 copy
的内存管理机制
copy
操作涉及到对象的复制,具体的复制行为取决于对象遵循的协议。对于遵循 NSCopying
协议的对象,会调用 copyWithZone:
方法进行复制;对于遵循 NSMutableCopying
协议的对象,会调用 mutableCopyWithZone:
方法进行可变复制。
以 NSString
为例,NSString
是不可变的,遵循 NSCopying
协议。当使用 copy
修饰的 NSString
属性进行赋值时,会创建一个新的 NSString
对象副本。
#import <Foundation/Foundation.h>
@interface Document : NSObject
@property (nonatomic, copy) NSString *title;
@end
@implementation Document
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSMutableString *mutableTitle = [NSMutableString stringWithString:@"Original Title"];
Document *document = [[Document alloc] init];
document.title = mutableTitle;
NSLog(@"Document title: %@", document.title);
[mutableTitle setString:@"Modified Title"];
NSLog(@"Document title after modifying mutable string: %@", document.title);
}
return 0;
}
在上述代码中,我们创建了一个 NSMutableString
对象 mutableTitle
,然后将其赋值给 Document
对象的 copy
修饰的 title
属性。此时,title
属性持有一个 NSString
对象副本。当我们修改 mutableTitle
时,document.title
不会受到影响,因为它是一个独立的副本。
4.3 copy
的适用场景
copy
修饰符主要用于防止对象被意外修改的场景。比如,在一个类中,如果希望某个属性的值不被外部修改(即使外部传递进来的是一个可变对象),就可以使用 copy
修饰符。常见的应用场景包括处理字符串、数组、字典等对象。
对于可变数组 NSMutableArray
,如果使用 strong
修饰,外部对原始可变数组的修改会影响到类内部持有该数组的属性。而使用 copy
修饰,类内部持有一个不可变数组 NSArray
的副本,外部对原始可变数组的修改不会影响到类内部。
#import <Foundation/Foundation.h>
@interface DataContainer : NSObject
@property (nonatomic, copy) NSArray *items;
@end
@implementation DataContainer
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSMutableArray *mutableItems = [NSMutableArray arrayWithObjects:@"Item1", @"Item2", nil];
DataContainer *container = [[DataContainer alloc] init];
container.items = mutableItems;
NSLog(@"Container items: %@", container.items);
[mutableItems addObject:@"Item3"];
NSLog(@"Container items after modifying mutable array: %@", container.items);
}
return 0;
}
在这个例子中,DataContainer
的 items
属性使用 copy
修饰,所以即使外部修改了 mutableItems
,container.items
仍然保持不变。
4.4 copy
需要注意的问题
使用 copy
修饰符时,需要确保被复制的对象遵循相应的复制协议(NSCopying
或 NSMutableCopying
)。如果对象没有正确实现这些协议,调用 copy
操作可能会导致运行时错误。另外,由于 copy
操作会创建新的对象,可能会带来一定的性能开销,特别是在处理大量数据时,需要谨慎使用。
五、strong
、weak
和 copy
的对比
- 内存管理方面:
strong
增加对象的引用计数,持有对象,直到所有strong
引用都被释放对象才会被销毁。weak
不增加对象的引用计数,对象销毁时,所有weak
引用自动归零。copy
创建对象副本,新副本有自己独立的引用计数。
- 适用场景方面:
strong
适用于对象之间有强关联,希望对象生命周期相互关联的场景。weak
用于解决循环引用问题,以及对象之间不需要强关联,一个对象的生命周期不影响另一个对象生命周期的场景。copy
用于防止对象被意外修改,希望持有一个独立副本的场景。
- 性能方面:
strong
和weak
主要涉及引用计数的操作,性能开销相对较小。copy
由于涉及对象的复制操作,特别是对于复杂对象,性能开销可能较大。
在实际编程中,需要根据具体的业务需求和对象之间的关系,准确选择合适的属性修饰符,以确保代码的正确性、稳定性和性能。通过合理运用 strong
、weak
和 copy
修饰符,可以编写出高质量的Objective - C 代码,避免常见的内存管理问题和程序错误。