探秘Objective-C属性的声明与使用
一、Objective-C 属性的基本概念
在 Objective-C 编程中,属性(Properties)是一种非常重要的特性。它为对象的实例变量提供了一种更加便捷、安全且面向对象的访问方式。属性本质上是一种语法糖,编译器会根据属性的声明自动生成存取方法(getter 和 setter 方法),这大大简化了开发者手动编写访问器方法的工作。
属性的声明通常放在类的接口(.h
文件)部分,它由一个或多个特性(Attributes)、类型和名称组成。例如,下面是一个简单的属性声明:
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@end
在这个例子中,@property
关键字用于声明一个属性,(nonatomic, strong)
是属性的特性,NSString *
是属性的类型,name
是属性的名称。
二、属性的特性
(一)原子性(Atomicity)相关特性
- atomic
- 含义:这是属性的默认原子性特性。当使用
atomic
时,编译器生成的存取方法会保证在多线程环境下,同一时间只有一个线程能够访问该属性。也就是说,对于atomic
属性,其存取操作是线程安全的。 - 实现原理:
atomic
属性的存取方法内部会使用锁机制(通常是自旋锁)来保证线程安全。当一个线程访问属性时,会先获取锁,访问完成后释放锁。这样其他线程在锁被占用时就无法访问该属性,直到锁被释放。 - 代码示例:
- 含义:这是属性的默认原子性特性。当使用
@interface AtomicExample : NSObject
@property (atomic, strong) NSString *atomicString;
@end
@implementation AtomicExample
@end
// 使用示例
AtomicExample *example = [[AtomicExample alloc] init];
dispatch_queue_t queue1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue2 = dispatch_queue_create("queue2", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue1, ^{
example.atomicString = @"Value from queue1";
});
dispatch_async(queue2, ^{
NSString *value = example.atomicString;
NSLog(@"Value from queue2: %@", value);
});
- 注意事项:虽然
atomic
保证了线程安全,但由于锁的开销,在多线程频繁访问属性时,性能可能会受到一定影响。
- nonatomic
- 含义:与
atomic
相反,nonatomic
表示属性的存取操作不是线程安全的。在多线程环境下,多个线程可能同时访问和修改该属性,可能会导致数据竞争等问题。 - 优势:由于不需要锁机制,
nonatomic
属性的存取速度比atomic
属性快。在单线程环境或者对线程安全要求不高的情况下,使用nonatomic
可以提高性能。 - 代码示例:
- 含义:与
@interface NonAtomicExample : NSObject
@property (nonatomic, strong) NSString *nonAtomicString;
@end
@implementation NonAtomicExample
@end
// 使用示例
NonAtomicExample *nonExample = [[NonAtomicExample alloc] init];
dispatch_queue_t nonQueue1 = dispatch_queue_create("nonQueue1", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t nonQueue2 = dispatch_queue_create("nonQueue2", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(nonQueue1, ^{
nonExample.nonAtomicString = @"Value from nonQueue1";
});
dispatch_async(nonQueue2, ^{
NSString *value = nonExample.nonAtomicString;
NSLog(@"Value from nonQueue2: %@", value);
});
- 应用场景:在 iOS 开发中,大部分情况下 UI 操作是在主线程进行,此时使用
nonatomic
属性可以在保证单线程安全的前提下提高性能。比如视图控制器中的属性,通常使用nonatomic
。
(二)内存管理相关特性
- strong
- 含义:
strong
是 iOS 5.0 引入 ARC(自动引用计数)后常用的内存管理特性,表示强引用。当一个对象被一个strong
属性引用时,该对象的引用计数会增加。只要有一个strong
引用指向该对象,对象就不会被释放。 - 示例:
- 含义:
@interface StrongExample : NSObject
@property (nonatomic, strong) NSObject *strongObject;
@end
@implementation StrongExample
@end
// 使用示例
StrongExample *strongEx = [[StrongExample alloc] init];
NSObject *obj = [[NSObject alloc] init];
strongEx.strongObject = obj;
// 此时 obj 的引用计数为 2(自身 + strongEx.strongObject 的引用)
strongEx.strongObject = nil;
// 此时 obj 的引用计数减为 1,当没有其他 strong 引用时,obj 会被释放
- weak
- 含义:
weak
表示弱引用,被weak
属性引用的对象不会增加对象的引用计数。当对象的最后一个strong
引用消失时,对象会被释放,并且所有指向该对象的weak
引用会自动被设置为nil
,从而避免了野指针的产生。 - 应用场景:在解决循环引用问题时经常使用
weak
。比如在视图控制器中,当一个视图控制器持有一个视图,而视图又持有视图控制器时,就会产生循环引用。此时可以将视图对视图控制器的引用设置为weak
。 - 代码示例:
- 含义:
@interface ViewController;
@interface CustomView : UIView
@property (nonatomic, weak) ViewController *viewController;
@end
@interface ViewController : UIViewController
@property (nonatomic, strong) CustomView *customView;
@end
@implementation 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
- assign
- 含义:
assign
用于基本数据类型(如int
、float
、BOOL
等)和 C 结构体。它不会对对象进行引用计数操作,只是简单地赋值。对于对象类型,如果使用assign
来引用对象,当对象被释放后,指向该对象的指针不会被自动设置为nil
,可能会导致野指针问题。 - 示例:
- 含义:
@interface AssignExample : NSObject
@property (nonatomic, assign) int number;
@end
@implementation AssignExample
@end
// 使用示例
AssignExample *assignEx = [[AssignExample alloc] init];
assignEx.number = 10;
NSLog(@"Number: %d", assignEx.number);
- unsafe_unretained
- 含义:与
weak
类似,unsafe_unretained
也是弱引用,不会增加对象的引用计数。但与weak
不同的是,当对象被释放后,指向该对象的unsafe_unretained
指针不会被自动设置为nil
,这就可能导致野指针问题。在 ARC 环境下,一般不推荐使用unsafe_unretained
,除非有特殊需求。 - 示例:
- 含义:与
@interface UnsafeUnretainedExample : NSObject
@property (nonatomic, unsafe_unretained) NSObject *unsafeObject;
@end
@implementation UnsafeUnretainedExample
@end
// 使用示例
UnsafeUnretainedExample *unsafeEx = [[UnsafeUnretainedExample alloc] init];
NSObject *unsafeObj = [[NSObject alloc] init];
unsafeEx.unsafeObject = unsafeObj;
[unsafeObj release];
// 此时 unsafeEx.unsafeObject 指向一个已释放的对象,是野指针
(三)读写权限相关特性
- readwrite
- 含义:这是属性的默认读写权限特性。表示属性同时具有读方法(getter)和写方法(setter)。编译器会自动生成标准的存取方法,开发者可以通过点语法或者直接调用存取方法来访问和修改属性的值。
- 示例:
@interface ReadWriteExample : NSObject
@property (nonatomic, readwrite, strong) NSString *readWriteString;
@end
@implementation ReadWriteExample
@end
// 使用示例
ReadWriteExample *rwEx = [[ReadWriteExample alloc] init];
rwEx.readWriteString = @"Initial value";
NSString *value = rwEx.readWriteString;
NSLog(@"Read - Write value: %@", value);
- readonly
- 含义:表示属性是只读的,编译器只会生成读方法(getter),不会生成写方法(setter)。这意味着在类的外部,只能获取属性的值,不能修改它。在类的内部,可以通过实例变量或者自定义的方法来修改属性的值。
- 示例:
@interface ReadOnlyExample : NSObject
@property (nonatomic, readonly, strong) NSString *readonlyString;
@end
@implementation ReadOnlyExample
- (instancetype)initWithString:(NSString *)string {
self = [super init];
if (self) {
_readonlyString = string;
}
return self;
}
@end
// 使用示例
ReadOnlyExample *roEx = [[ReadOnlyExample alloc] initWithString:@"Read - only value"];
NSString *roValue = roEx.readonlyString;
NSLog(@"Read - Only value: %@", roValue);
// 以下代码会报错,因为没有 setter 方法
// roEx.readonlyString = @"New value";
(四)其他特性
- copy
- 含义:
copy
特性用于字符串等对象,当设置属性值时,会创建一个对象的副本。对于不可变对象(如NSString
、NSArray
、NSDictionary
),copy
操作会返回对象本身,因为不可变对象本身就是不可修改的;对于可变对象(如NSMutableString
、NSMutableArray
、NSMutableDictionary
),copy
操作会创建一个新的不可变副本。 - 作用:使用
copy
可以防止对象被外部修改。例如,当一个类持有一个字符串属性,如果外部传入一个可变字符串,并且后续对该可变字符串进行修改,可能会影响到类内部的逻辑。使用copy
可以确保类内部持有的字符串是不可变的,不受外部修改的影响。 - 示例:
- 含义:
@interface CopyExample : NSObject
@property (nonatomic, copy) NSString *copyString;
@end
@implementation CopyExample
@end
// 使用示例
CopyExample *copyEx = [[CopyExample alloc] init];
NSMutableString *mutableStr = [NSMutableString stringWithString:@"Mutable string"];
copyEx.copyString = mutableStr;
[mutableStr appendString:@" - modified"];
NSLog(@"Copy string: %@", copyEx.copyString);
// 输出的 copyEx.copyString 不会被 mutableStr 的修改影响
- getter =
- 含义:可以通过这个特性自定义属性的读方法(getter)名称。通常用于当默认的
getter
方法名不符合需求,需要一个更具描述性或特殊的方法名时。 - 示例:
- 含义:可以通过这个特性自定义属性的读方法(getter)名称。通常用于当默认的
@interface CustomGetterExample : NSObject
@property (nonatomic, getter = isEnabled) BOOL enabled;
@end
@implementation CustomGetterExample
@end
// 使用示例
CustomGetterExample *cgEx = [[CustomGetterExample alloc] init];
cgEx.enabled = YES;
BOOL isEnabled = cgEx.isEnabled;
NSLog(@"Is enabled: %d", isEnabled);
- setter =
- 含义:类似于
getter = <customGetter>
,这个特性用于自定义属性的写方法(setter)名称。可以在自定义的setter
方法中添加额外的逻辑,如数据验证、日志记录等。 - 示例:
- 含义:类似于
@interface CustomSetterExample : NSObject
@property (nonatomic, setter = setCustomName:) NSString *name;
@end
@implementation CustomSetterExample
- (void)setCustomName:(NSString *)newName {
if (newName.length > 0) {
_name = newName;
} else {
NSLog(@"Name cannot be empty");
}
}
@end
// 使用示例
CustomSetterExample *csEx = [[CustomSetterExample alloc] init];
[csEx setCustomName:@"Valid name"];
[csEx setCustomName:@""];
三、属性的声明与实例变量的关系
在 Objective - C 中,属性和实例变量紧密相关。在 ARC 之前,通常需要手动声明实例变量,并在类的实现文件中手动编写存取方法。例如:
@interface OldStyleClass : NSObject {
NSString *_name;
}
- (NSString *)name;
- (void)setName:(NSString *)name;
@end
@implementation OldStyleClass
- (NSString *)name {
return _name;
}
- (void)setName:(NSString *)name {
if (_name!= name) {
[_name release];
_name = [name retain];
}
}
@end
在 ARC 环境下,使用属性声明简化了这个过程。当声明一个属性时,编译器会自动为我们生成实例变量(默认以下划线 _
开头加上属性名)以及存取方法。例如:
@interface NewStyleClass : NSObject
@property (nonatomic, strong) NSString *name;
@end
@implementation NewStyleClass
@end
这里编译器会自动生成 _name
实例变量,以及 name
的 getter
和 setter
方法。我们可以通过点语法 object.name
来访问属性,这实际上是调用了编译器生成的存取方法。
如果想要自定义实例变量的名称,可以在类的实现文件中手动声明实例变量。例如:
@interface CustomIvarClass : NSObject
@property (nonatomic, strong) NSString *name;
@end
@implementation CustomIvarClass {
NSString *customIvarName;
}
- (NSString *)name {
return customIvarName;
}
- (void)setName:(NSString *)name {
customIvarName = name;
}
@end
在这种情况下,我们手动声明了 customIvarName
作为实例变量,并自定义了存取方法来操作这个实例变量。
四、属性在类继承中的表现
当一个类继承自另一个类时,子类会继承父类的属性。子类可以像使用自己声明的属性一样使用父类的属性。例如:
@interface Animal : NSObject
@property (nonatomic, strong) NSString *species;
@end
@implementation Animal
@end
@interface Dog : Animal
@property (nonatomic, strong) NSString *breed;
@end
@implementation Dog
@end
// 使用示例
Dog *myDog = [[Dog alloc] init];
myDog.species = @"Canine";
myDog.breed = @"Labrador";
NSLog(@"Species: %@, Breed: %@", myDog.species, myDog.breed);
在这个例子中,Dog
类继承自 Animal
类,因此可以访问和设置 Animal
类的 species
属性。
子类也可以重写父类属性的存取方法。例如,如果父类的 setter
方法没有进行数据验证,子类可以重写 setter
方法来添加数据验证逻辑:
@interface ParentClass : NSObject
@property (nonatomic, assign) int value;
@end
@implementation ParentClass
@end
@interface ChildClass : ParentClass
@end
@implementation ChildClass
- (void)setValue:(int)value {
if (value >= 0) {
[super setValue:value];
} else {
NSLog(@"Value cannot be negative");
}
}
@end
// 使用示例
ChildClass *child = [[ChildClass alloc] init];
[child setValue:10];
[child setValue:-5];
在这个例子中,ChildClass
重写了 setValue:
方法,添加了数据验证逻辑,只有当值大于等于 0 时才会调用父类的 setter
方法来设置属性值。
五、属性与 KVO(Key - Value Observing)
KVO 是一种基于观察者模式的机制,它允许开发者监听对象属性值的变化。属性在 KVO 中起着关键作用。
要使用 KVO,首先需要在观察者对象中注册对被观察对象属性的观察。例如:
@interface Observer : NSObject
@end
@implementation Observer
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"name"]) {
NSLog(@"Name has changed: %@", change[NSKeyValueChangeNewKey]);
}
}
@end
@interface ObservableClass : NSObject
@property (nonatomic, strong) NSString *name;
@end
@implementation ObservableClass
@end
// 使用示例
ObservableClass *observable = [[ObservableClass alloc] init];
Observer *observer = [[Observer alloc] init];
[observable addObserver:observer forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
observable.name = @"New name";
[observable removeObserver:observer forKeyPath:@"name"];
在这个例子中,Observer
类通过 addObserver:forKeyPath:options:context:
方法注册对 ObservableClass
的 name
属性的观察。当 name
属性值发生变化时,observeValueForKeyPath:ofObject:change:context:
方法会被调用,我们可以在这个方法中处理属性值变化的逻辑。最后,使用完 KVO 后,需要通过 removeObserver:forKeyPath:
方法移除观察,以避免内存泄漏。
六、属性与归档(Archiving)和序列化(Serialization)
在将对象进行归档(如使用 NSKeyedArchiver
)或序列化(如转换为 JSON 等格式)时,属性也扮演着重要角色。
对于归档,对象需要遵循 NSCoding
协议,并实现 encodeWithCoder:
和 initWithCoder:
方法。在这些方法中,通常会对对象的属性进行编码和解码。例如:
@interface ArchiveableClass : NSObject <NSCoding>
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) int age;
@end
@implementation ArchiveableClass
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:self.name forKey:@"name"];
[aCoder encodeInt:self.age forKey:@"age"];
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
NSString *name = [aDecoder decodeObjectForKey:@"name"];
int age = [aDecoder decodeIntForKey:@"age"];
self = [super init];
if (self) {
self.name = name;
self.age = age;
}
return self;
}
@end
// 使用示例
ArchiveableClass *objToArchive = [[ArchiveableClass alloc] init];
objToArchive.name = @"John";
objToArchive.age = 30;
NSData *archivedData = [NSKeyedArchiver archivedDataWithRootObject:objToArchive];
ArchiveableClass *unarchivedObj = [NSKeyedUnarchiver unarchiveObjectWithData:archivedData];
NSLog(@"Name: %@, Age: %d", unarchivedObj.name, unarchivedObj.age);
在这个例子中,ArchiveableClass
的 name
和 age
属性在归档和解档过程中被正确处理。
在进行序列化时,比如将对象转换为 JSON 格式,也需要处理对象的属性。可以通过手动构建 JSON 字典,将属性值添加到字典中,然后使用相关的 JSON 序列化方法将字典转换为 JSON 字符串。例如:
@interface SerializableClass : NSObject
@property (nonatomic, strong) NSString *title;
@property (nonatomic, NSNumber *) price;
@end
@implementation SerializableClass
@end
// 使用示例
SerializableClass *serializable = [[SerializableClass alloc] init];
serializable.title = @"Product";
serializable.price = @10.99;
NSDictionary *jsonDict = @{
@"title": serializable.title,
@"price": serializable.price
};
NSError *error;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:jsonDict options:NSJSONWritingPrettyPrinted error:&error];
NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
NSLog(@"JSON String: %@", jsonString);
通过这种方式,将对象的属性值转换为 JSON 格式,以便在网络传输或存储等场景中使用。
七、属性使用的最佳实践
- 合理选择原子性特性:在单线程环境或者对线程安全要求不高的场景下,优先使用
nonatomic
,以提高性能。只有在多线程频繁访问且需要保证线程安全的属性上,才使用atomic
。 - 正确使用内存管理特性:对于对象类型,根据对象的生命周期和是否可能产生循环引用,选择合适的内存管理特性。一般情况下,对于持有对象,使用
strong
;在可能产生循环引用的地方,如视图对视图控制器的引用,使用weak
。对于基本数据类型,使用assign
。 - 考虑读写权限:如果属性在类外部不需要被修改,使用
readonly
可以提高代码的安全性和可维护性。 - 谨慎使用
copy
:在处理可能被外部修改的可变对象时,使用copy
来确保对象内部状态的一致性。但要注意copy
的性能开销,特别是对于大型对象。 - 避免过度自定义存取方法:虽然可以自定义属性的存取方法,但除非有特殊需求,尽量使用编译器自动生成的存取方法,这样可以减少代码量,提高代码的一致性和可维护性。
通过遵循这些最佳实践,可以编写出更加健壮、高效且易于维护的 Objective - C 代码。