翻译自 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
9
//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 创建被分配的值的拷贝,而不是直接引用原对象

以上