OC @property 知多少

翻译自 Ry’s Objective-C Tutorial


一个对象的 property 能让其他对象检测或者改变该对象的状态。但是,一个设计良好的面向对象程序,应该避免直接让对象的内部状态被直接访问。相反,应该使用访问器方法(accessor methods, getters and setters),来作为一种抽象,跟对象的底层数据进行交互。看图:


image

使用 accessor 方法来跟 property 交互


@property指令的目的,是通过自动生成这些 accessor 方法,来让这些 property 的创建以及设置变得简单。它允许你在语义层面(semantic level)上来指定 public property 的行为,并且它会承担相关的实现细节。


@property指令中可以使用许多属性,来让你改变 getter 和 setter 的行为。有一些属性会决定 property 如何处理它们的底层内存,所以这篇文章也会同时介绍一些 Objective-C 内存管理。想要直到更多的关于内存管理的细节,可以阅读这里Memory Management

@property 指令

首先,让我们来瞧瞧当我们在使用@property指令的时候实际上在底层会发生什么事情。考虑如下例子,哟一个简单的 Car 类,还有它的相关实现。

1
2
3
4
5
6
7
8
//Car.h
#import <Foundation/Foundation.h>

@interface Car : NSObject

@property BOOL running;

@end

1
2
3
4
5
6
7
8
//Car.m
#import "Car.h"

@implementation Car

@synthesize running = _running; //Xcode 4.4+ 可以忽略

@end

编译器会为 running 属性生成相应的 getter 还有 setter 方法。默认的命名惯例是,使用属性本身作为 getter 方法,在它的前面加上前缀 set 作为 setter 方法,在它前面加上下划线 _ 则是它的实例变量,如下:

1
2
3
4
5
6
7
- (BOOL)running {
return _reunning;
}

- (void)setRunning:(BOOL)newValue {
_running = newValue;
}

在使用@property指令声明了属性之后,你就可以调用这些 setter getter 方法了,就好像他们在你的 .h 跟 .m 文件被编写进去了一样。你也可以在 Car.m 文件里面重写它们来提供自定义的 getter/setter 方法,但如果这样的话就必须要编写 @synthesize 指令的相关代码了(???)。然而,你已经很少情况需要自定义 accessor 方法,因为@property能让你在一个抽象层面完成这些工作。


通过点运算符来访问的属性会根据所在的场景,被翻译成对应的 accessor 方法。所以下面的代码中的 honda.running 语句,在你向它分配一个值时实际上是在调用 setRunning: ,当你读取它的值时实际上是在调用 running 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
//main.m
#import <Foundation/Foundation.h>
#import "Car.h"

int main(int argc, const char * argv[]) {
@autoreleasepool {
Car *honda = [[Car alloc] init];
honda.runing = YES; // [honda setRunning: YES];
NSLog(@"%d", honda.running); // [honda running]
}

return 0;
}

要改变自动生成的 accessor 的行为,你可以在@property指令后面的圆括号指定一些属性。这篇文章剩下的部分将会介绍这些可用的属性。

getter= 跟 setter=

如果你不喜欢@property默认的命名规范,你可以通过 getter= 还有 setter= 这两个属性来改变 getter 跟 setter 方法的名字。通常我们会在 Boolean 类型的 property 中使用,因为它们的 getter 按照惯例通常会有 is 作为前缀。例子如下:

1
@property (getter=isRunning) BOOL running;

现在,生成的 accessor 就会被命名为 isRunning 跟 setRunning 方法。要注意,property 的名字仍然为 running,并且这是你应该在点运算符后面使用的:

1
2
3
4
Car *honda = [[Car alloc] init];
honda.running = YES; // [honda setRunning: YES]
NSLog(@"%d", honda.running); // [honda isRunning]
NSLog(@"%d", [honda running]); // Error: method no longer exists

它们俩是唯一带参数的属性——accessor 方法的名字。剩下的其他属性都是 Boolean 标识。

readonly

readonly 属性能让一个属性变成只读。它删除了 setter 方法并且禁止通过点运算符来对属性分配值,但是 getter 不会受影响。举个栗子,让我们像下面的代码那样修改 Car 类。注意如何通过逗号来分割指定的多个属性。

1
2
3
4
5
6
7
8
#import <Foundation/Foundation.h>

@interface Car : NSObject

@property (getter=isRunning, readonly) BOOL running;

- (void)startEngine;
- (void)stopEngine;

我们通过 startEngine 跟 stopEngine 方法的实现,来在类内部设置 running property,而不是让其他对象直接显式对它进行修改。相关的方法实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Car.m
#import "Car.h"

@implementation Car

- (void)startEngine {
_running = YES;
}

- (void)stopEngine {
_running = NO;
}

@end

记住,@property同时也为我们生成一个实例变量,这就是为什么我们可以方位 _running 而不用在某处显式声明它。在上面的程序中,我们不能用 self.running 来对 _running 分配值,因为 running property 是 readonly 的。我们可以在 main.m 中添加如下代码片段来测试新的 Car 类。

1
2
3
4
Car *honda = [[Car alloc] init]
[honda startEngine];
NSLog(@"Running: %d", honda.running);
honda.running = NO; // Error: read-only property

直到这里,property 对我们来说仅仅是让我们避免编写四班的 getter 跟 setter 方法。但是对于剩下的 attribute 来说就不是这样了,它们会明显地改变 property 的行为。同时,它们只能在 Objective-C 对象类型的 property 中使用,而在原始的 C 数据类型中则不可使用。

nonatomic

原子性(Atomicity)跟 property 在一个线程环境中的行为有关。当你拥有不止一个线程,可能在某个时间点,setter 跟 getter 会被同时调用。这就意味着,getter/setter 能被另外一个 setter/getter 操作打断,而因此导致数据冲突。


atomic property 会将底层对象锁住来阻止这种情况发生,保证了 get 或者 set 操作工作时对象的值是完整的。然而,重要的是,这只是线程安全的一个方面,并不是说使用 atomic property 就意味着你的代码一定是线程安全的。


使用@property声明的property默认具有 atomic 属性,并且这会导致一些开销。所以,如果你并不是在多线程环境(或者你正在实现自己的线程安全),你可以用 nonatomic 属性来重写这个行为,如下:

1
@property (nonatomic) NSString *model;

这里有一些关于 atomic property 的小小警告。Atomic property 的 accessor 方法(getter和setter)需要成对被自动生成或者用户自己定义。换句话说,你要么不定义,要定义就将 setter 跟 getter 一起定义。只有 non-atomic property 才能让你混搭 synthesized accessor 跟自定义的 accessor 方法。想清楚地了解,你可以将上面代码里面的 nonatomic 移除,然后再 Car.m 里面添加自定义的 getter 方法。

内存管理

在所有面向对象编程(OOP)语言中,对象存在于计算机的内存中,而内存是稀缺资源,在移动设备中尤为这样。内存管理系统的目的是,以一种有效率的方法,来确保程序不会占有的内存空间不会比他们实际所需要的更多。


很多语言通过垃圾回收机制来实现内存管理,但是 Objective-C 使用一种更加有效率的替代方法,称为 对象拥有关系(object ownership)。当你开始与一个对象进行交互,你会被认为拥有(own)该对象,这意味着只要你在使用着它,它就会一直存在。当你完成操作之后,你将释放对该对象的拥有关系(ownship)。而在释放了之后,如果该对象没有其他 owner,操作系统将会销毁该对象,并且释放底层的内存空间。


image

销毁没有 owner 的对象


而在 Automatic Reference Counting 出现之后,编译器将自动管理你的所有对象 ownership 。这就意味着,在大多数情况下,你将不再需要担忧系统实际上如何进行内存管理。但是,你应该了解 @property 里面的 strong,weak 和 copy 这几个属性,因为他们会告诉编译器相应的对象之间会拥有怎样的关系。

strong

strong 属性会对被分配到 property 的对象创建拥有关系(owning relationship)。这是所有对象类型的 property 的默认属性,同时也是安全的默认属性,因为它确保了被分配给 property 的对象不会意外被释放。


下面通过一个新的 Person 类来观察它是怎样使用的。Person 类仅生命了一个 name property:

1
2
3
4
5
6
7
8
// Person.h
#import <Foundation/Foundation.h>

@interface Person : NSObject

@property (nonatomic) NSString *name;

@end

它的实现如下。Person 类使用@property生成的默认 accesor 方法。它同时重写了 NSObject 的 description 方法,该方法将返回一个代表该对象的字符串。

1
2
3
4
5
6
7
8
9
10
// Person.m
#import "Person.h"

@implementation Person

- (NSString *)description {
return self.name;
}

@end

下一步,让我们在 Car 类中添加一个 Person 类型的 property。将 Car.h 修改如下:

1
2
3
4
5
6
7
8
9
10
// Car.h
#import <Foundation/Foundation.h>
#import "Person.h"

@interface Car : NSObject

@property (nonatomic) NSString *model;
@property (nonatomic, strong) Person *driver;

@end

然后,考虑在 main.m 中的如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// main.m
#import <Foundation/Foundation.h>
#import "Car.h"
#import "Persion.h"

int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *john = [[Person alloc] int];
john.name = @"John";

Car *honda = [[Car alloc] init];
honda.model = @"Honda Civic";
honda.driver = john;

NSLog(@"%@ is driving the %@", honda.driver, honda.model);
}

return 0;
}

由于 driver 是强关系(strong relationship),对象 honda 拥有 john 的 ownership ,这保证了只要 honda 需要它,就不会被释放掉。

weak

strong 属性是 object property 中使用得最普遍的。然而 strong reference 会引来一个问题。例如,当我们需要引用 driver 正在驾驶的 Car 对象。首先,让我们在 Person.h 中增加一个 Car 类型 property:

1
2
3
4
5
6
7
8
9
10
11
// Person.h
#import <Foundation/Foundation.h>

@class Car;

@interface Person : NSObject

@property (nonatomic) NSString *name;
@property (nonatomic, strong) Car *car;

@end

@class Car 代码行是对 Car 类的超前声明(forward declaration)。它像是在告诉编译器:“相信我,Car 类是存在的,所以现在先不要试图马上去寻找它”。我们使用这行代码,而不是使用我们习惯的 #import 语句,是因为 Car 同样也 import Person.h,否则将会导致一个无穷无尽的 import 循环。(编译器不喜欢 endless loop)


下一步,在 honda.driver 赋值语句后面加入下面代码:

1
2
honda.driver = john;
john.car = honda; // Add this line

我们现在有从 honda 指向 john 的关系,同时也有从 john 到 honda 的关系。这就意味着双方彼此拥有,因此内存管理系统永远不能释放他们,即使他们不再被需要。


image
在 Car 跟 Person 类之间的 retain cycle


这被称为 retain cycle ,是一种内存泄露,如我们所知内存泄露是多么的糟糕。幸运的是,要解决这个问题非常简单,只需要让其中一个 property 对其对象的引用更改为 weak
reference
。在 Person.h,如下更改 car property 的声明。

1
@property (nonatomic, weak) Car *car;

weak 属性创建了一种对 car 的“非拥有关系(non-owning relationship)”。这允许 john 拥有指向 honda 的关系,同时避免了 retain cycle。但是,这也同时可能会导致这样的一种情况:john 仍然持有指向 honda 的引用,但是 honda 已经被销毁了。如果这种情况发生了, weak 属性会贴心地将 car 设置为 nil,来避免悬空指针(dangling pointer)出现。


image
从 Person 类指向 Car 类的 weak reference


一种普遍的使用 weak 属性的场景是在父子关系的数据结构中。一般情况下,父对象应该持有指向孩子的 strong reference,而子对象应该持有指向父对象的 weak reference。weak reference 同样也是在 delegate 设计模式中固有的一部分。


值得再次提醒,两个对象之间不应该彼此持有指向对方的 strong reference。weak 属性让我们得以维持一个循环的指向关系但是不会引入 retain cycle。

copy

copy 属性是 strong 的另一种替代方式。copy 会对分配给 property 的对象创建一个拷贝,然后持有该拷贝的 ownership,而不是持有原对象的 ownership。只有遵从了 NSCopying 协议的对象能使用它。


代表值的 property(而不是连接或者引用)可以使用 copy 属性。例如,开发者通常拷贝 NSString property 而不是持有它们的 strong reference:

1
2
// Car.h
@property (nonatomic, copy) NSString *model;

现在,Car 会持有我们分配给 model 的对象的一个新拷贝的 strong reference。如果你 working with mutable values,这样做有一个好处:copy 让你得以“冷藏”该可变值,让 property 持有被赋值时的值,而后面原可变值如何改变都不再影响此 property。例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// main.m
#import <Foundation/Foundation.h>
#import "Car.h"

int main(int argc, const char *argv[]) {
@autoreleasepool {
Car *honda = [[Car alloc] init];
NSMutableString *model = [NSMutableString stringWithString:@"Honda Civic"];
honda.model = model;

NSLog(@"%@", honda.model);
[model setString:@"Nissa Versa"];
NSLog("%@", honda.model); // Still "Honda Civic"
}

return 0;
}

NSMutableString 是 NSString 的子类,它可被修改。如果 model 属性没有创建对原对象的拷贝,我们将会看到 NSLog 打印出来的内容为“Nissa Versa”。

Other Attributes

上面提到的 @property 的 attribute 是你现在设计 Objective-C 应用所需要的(iOS5+),但还有一些其他的 attribute,你可能会在一些比较旧的库或者文档中遇到。

retain

retain 属性是 MRR(Manual Retain Release) 版本的 strong,它会产生相同的作用:获得被分配的值的 ownership。在 ARC 环境下你不应该使用这个值。

unsafe_unretained

使用 unsafe_unretained 属性的 property 跟使用 weak 属性的 property 类似,但是它在引用的对象被销毁的时候并不会自动将 property 设置为 nil。你唯一一个使用 unsafe_unretained 的理由应该是——你在不支持 weak 属性的环境中编码。

assign

assign 属性在 property 被赋予一个新的值时,不会执行任何内存管理的调用。这是原始数据类型 property 的默认属性。在 iOS5 之前,它也被用来作为 weak reference 的一种实现方式。像 retain 一样,你不应该在 ARC 环境中使用它。用于OC对象时,assignweak 类似,但是在所指向的对象被销毁之后,不会将 property 清空。

总结

Attribute Description
getter= 为 getter 方法取一个自定义的名字
setter= 为 setter 方法取一个自定义的名字
readonly 编译器不会为你生成 setter 方法
nonatomic 不保证在多线程环境中,对属性的访问的完整性。但它比默认的 atomic 更加高效
strong 在 property 与被分配的值之间创建拥有关系。它是 object property 的默认属性
weak 在 property 与被分配的值之间创建非拥有关系。用它来避免 retain cycle
copy 创建被分配的值的拷贝,而不是直接引用原对象



以上