Objective-C 属性及其修饰符的使用

366 阅读23分钟

对象除了可以发送消息之外, 还可以通过属性封装数据.

这篇文章讲述如何通过Objective-C语法声明属性, 解释属性是如何通过访问方法合成与实例变量实现的. 如果属性关联了实例变量, 那么实例变量必须在初始化方法中被正确地设置.

如果一个对象需要通过属性管理另外一个对象, 就需要考虑它们之间的关系. 即使对于Objective-C对象的内存管理大部分都可以通过自动引用计数(Automatic Reference Counting - ARC)自动管理, 也要注意避免对象间的强引用循环以防止内存泄漏. 关于这部分, 后面会通过对象的声明周期, 管理对象之间的关系说明.

属性封装了对象的值

大多数对象为了完成他们的任务需要存储信息. 一些对象被设计的初衷就是保存1个或更多的值, 比如NSNumber类会存储数字信息, 或者之前提到的XYZPerson类会存储人的姓与名.

公共属性的声明

Objective-C 属性 提供了一种类封装信息的方式. 属性声明在类接口中可以这样声明:

@interface XYZPerson : NSObject
@property NSString *firstName;
@property NSString *lastName;
@end

在这个例子中, XYZPerson类声明了保存姓/名的字符串属性.

在面向对象编程中一个主要的原则就是封装性, 一个对象需要把它内部的工作隐藏起来, 对外使用接口.

因此通过接口中的属性, 而不是直接访问内部变量与对象交互是很重要的.

使用访问(Accessor)方法以获取(Get)或者设置(Set)属性值

我们可以通过访问方法(accessor methods)获取或者设置对象的属性:

NSString *firstName = [somePerson firstName];
    [somePerson setFirstName:@"Johnny"];

默认这些访问方法会通过编译器自动合成, 所以我们除了在类接口中使用@property声明属性外不用做其它额外工作了.

自动合成的方法遵循如下的名称规则:

  • 取值的方法(Getter 方法)和属性有相同的名称. firstName属性的getter 方法也叫firstName
  • 赋值的方法(Setter 方法)以"set"开头,之后使用首字母大写的属性名称. firstName属性的setter方法名为setFirstName:.

如果我们不希望通过setter方法修改属性值, 可以添加attribute指定属性为只读:

@property (readonly) NSString *fullName;

attributes 不仅可以指明其它对象应当如何使用这个属性, 也会告知编译器如何合成相关的访问方法.

在这个例子中, 编译器会合成一个fullName的getter方法, 但是不会生成setFullName:方法.

Note: 和readonly相反的是readwrite. readwrite不需要显式指定, 它是默认值.

如果我们想要使用一个不同的getter或者setter方法的名称, 可以通过为属性添加attributes来实现. 当使用Boolean类型的属性时, 可以为其设置以is开头的getter方法. 属性为finished, 指定它的getter为isFinished:

@property (getter=isFinished) BOOL finished;

如果我们需要指定多个attribute, 可以通过逗号,分隔它们 :

@property (readonly, getter=isFinished) BOOL finished;

在这个例子中, 编译器只会自动合成isFinished方法, 而不会生成setFinished:方法.

Note: 通常来说, 属性的访问方法应当遵从Key-Value Coding(KVC)的命名规则. 详见我的另一篇博客KVC,或者官方文档.

点语法(Dot syntax).是访问方法的快捷方式

除了直接调用setter和getter方法外, Objective-C提供了点语法. 快捷访问属性.

点语法允许你像这样访问属性:

NSString *firstName = somePerson.firstName;
    somePerson.firstName = @"Johnny";

点语法单纯只是访问器方法的语法糖. 当我们使用点语法的时候, 属性从根源上仍然是通过getter和setter方法访问的.

  • 获取属性值时, somePerson.firstName 等价于[somePerson firstName]
  • 赋值属性值时, somePerson.firstName = @"Johnny" 等价于 [somePerson setFirstName: @"Johnny"]

这就意味着通过点语法访问属性也是被属性的attributes 影响的. 如果属性被标记为readonly ,当我们使用点语法为其赋值时, 编译器会报错.

大多数属性都对应这一个实例变量

默认情况下, 一个readwrite属性会对应一个编译器自动合成的实例变量.

对象的实例变量就是一个在对象的整个生命周期中, 存在并管理保存值的变量. 实例变量的内存空间在对象首次创建(通过alloc)时就已经被开辟了. 直到对象销毁才被回收.

除非我们手动指定, 否则自动合成的实例变量名称就是_ +属性名. 对于名称为firstName的属性, 它对应自动生成的实例变量名称就是_firstName

虽然最好是通过点语法或者访问方法去访问属性值. 但是类实现的实例方法中直接访问实例变量也是可以的.下划线前缀_ 可以很清楚地表明我们正在访问实例变量而不是局部变量:

- (void)someMethod {
    NSString *myString = @"An interesting string";
 
    _someString = myString;
}

在上述例子中, 可以很清楚地分辨出 myString 是一个局部变量而_someString是一个实例变量.

通常来说, 即使是在类的内部, 也应当使用访问方法或者点语法获取属性值:

- (void)someMethod {
    NSString *myString = @"An interesting string";
 
    self.someString = myString;
  // or
    [self setSomeString:myString];
}

有一个例外就是当我们使用初始化方法或者析构方法或者自定义的访问方法时, 这个例外我们在后面会提到.

自定义合成实例变量名称(@synthesize)

像前文描述的那样, 可写的属性默认会使用_+属性名这种格式的实例变量

如果我们希望使用不同的实例变量名称, 那么就需要在实现文件中使用如下语法告诉编译器:

@implementation YourClass
@synthesize propertyName = instanceVariableName;
...
@end

例如:

@synthesize firstName = ivar_firstName;

属性还是会叫做firstName ,仍然可以通过firstNamesetFirstName:或者点语法访问, 但是它将会使用名称为ivar_firstName的实例变量存储数据.

Important: 如果我们使用@synthesize , 但是没有指定实例变量名称, 比如:

@synthesize firstName;

实例变量将和属性使用相同的名称, 在这个例子中实例变量也叫做firstName, 不会有前面的下划线.

可以不通过属性, 直接定义实例变量

当我们希望保存值或者其它对象的信息时, 最好是使用属性.

但是如果确实需要定义实例变量而不声明属性, 我们可以在类声明或者实现的上方用花括号{} 中添加实例变量:

@interface SomeClass : NSObject {
    NSString *_myNonPropertyInstanceVariable;
}
...
@end
 
@implementation SomeClass {
    NSString *_anotherCustomInstanceVariable;
}
...
@end

Note: 也可以在类扩展(Extension)上方添加实例变量, 之后的文章会讲述, 或者参考官方文档.

直接通过初始化方法访问实例变量

Setter 方法可能有额外的副作用, 他们会触发KVC通知,或者在我们自定义了setter方法时,可能会执行额外的工作.

在初始化方法中, 应当总是直接访问实例变量. 原因在于: 当属性准备好时, 对象的其它部分可能还没有初始化完全. 即使我们没有提供任何自定义方法或者我们了解自己的类中的所有可能的副作用, 但子类仍可能会重写这部分行为.

经典的初始化方法可能是这样的:

- (id)init {
    self = [super init];
 
    if (self) {
        // initialize instance variables here
    }
 
    return self;
}

初始化方法中应当调用父类的初始化方法, 并将结果赋值给self, 父类可能会初始化失败并且返回nil, 因此我们在初始化前需要确保self不是nil

调用[super init]后, 会沿着继承链从根类开始一层一层地按照顺序调用init方法.

Image

一个对象或者是通过调用init方法初始化, 或者调用一个传入了初始化值参数的方法来初始化对象.

XYZPerson类的例子中, 提供设置对象初始firstNamelastName的初始化方法是有意义的.

- (id)initWithFirstName:(NSString *)aFirstName lastName:(NSString *)aLastName;

方法实现如下:

- (id)initWithFirstName:(NSString *)aFirstName lastName:(NSString *)aLastName {
    self = [super init];
 
    if (self) {
        _firstName = aFirstName;
        _lastName = aLastName;
    }
 
    return self;
}

指定初始化方法(Designated Initializer)是主要的初始化方法

如果一个对象声明了一个或者多个初始化方法, 就需要确定指定初始化方法.这个方法通常是提供了最多选择/参数的初始化方法, 并且所有其它初始化方法都调用了它. 我们也应当重写init方法, 在init方法中调用我们提供的指定初始化方法,并提供合适的默认值.

如果XYZPerson拥有dateOfBirth属性, 它的指定初始化方法可能是这样的:

- (id)initWithFirstName:(NSString *)aFirstName lastName:(NSString *)aLastName
                                            dateOfBirth:(NSDate *)aDOB;

这个方法的实现和上面的方法类似, 会设置好对应的实例变量. 如果我们仍然需要仅仅提供firstNamelastName的便捷初始化方法, 我们实现这个方法时需要在其中调用我们的指定初始化方法:

- (id)initWithFirstName:(NSString *)aFirstName lastName:(NSString *)aLastName {
    return [self initWithFirstName:aFirstName lastName:aLastName dateOfBirth:nil];
}

同时, 我们也需要重写init方法, 并为实例变量提供合适的默认值:

- (id)init {
    return [self initWithFirstName:@"John" lastName:@"Doe" dateOfBirth:nil];
}

我们在为使用了多个初始化方法的子类定义初始化方法时, 或者我们需要重写父类的指定初始化方法以执行我们自定义的初始化方法, 或者我们需要添加额外的初始化方法, 但是不论如何, 我们都应当在调用自定义初始化方法前调用父类的指定初始化方法.

自定义访问方法

属性不总是有一个对应的实例变量存储它的值.

比如: XYZPerson类可能会为fullName定义一个只读属性.

@property (readonly) NSString *fullName;

使用自定义的getter方法根据firstNamelastName拼接fullName是很方便的, 否则每当firstName或者lastName发生改变时, 我们都不得不更新fullName的值.

- (NSString *)fullName {
    return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
}

上面的例子使用了格式化字符串, 将firstNamelastName以空格分隔并拼接成一个新字符串.

Note: 上面的例子只是为了方便的举例, 实际开发中要考虑到姓名是和地域相关(locale-specific)的, 上述fullName格式只适合名字放在姓氏前的地区.

当我们需要自定义使用了实例变量的访问方法时, 我们在自定义方法中必须直接访问实例变量以修改属性值. 比如常见的"懒加载"(lazy accessor), 尽量延迟初始化, 直到属性被访问:

- (XYZObject *)someImportantObject {
    if (!_someImportantObject) {
        _someImportantObject = [[XYZObject alloc] init];
    }
 
    return _someImportantObject;
}

在返回值之前, 方法会先检查_someImportantObject实例变量是否为nil, 如果为nil,会创建一个对象.

Note: 编译器只有在至少自动合成了一个访问方法(getter/setter)时, 才会自动合成实例变量. 如果我们为一个readwrite属性自定义了settergetter或者readonly属性的getter, 编译器就会认为我们接管了属性的实现, 从而不会自动合成实例变量. 如果我们确实需要一个实例变量, 就需要手动指定要合成的实例变量.

@synthesize property _property

属性默认是具备原子性(Atomic)的

Objective-C的属性默认是具备原子性的(atomic).

@interface XYZObject : NSObject
@property NSObject *implicitAtomicObject;          // atomic by default - 默认为atomic
@property (atomic) NSObject *explicitAtomicObject; // explicitly marked atomic - 显式指定为atomic
@end

原子性表明合成的访问方法保证可以完整地调用getter/setter方法时, 即使访问方法同时在不同线程被调用.

由于原子性访问方法的内部实现和同步的实现是不对外开放的(private), 混合合成的访问方法和我们自定义的访问方法是不可行的.比如当我们尝试为一个atomic, readwrite属性自定义一个setter方法, 但是让编译器合成getter方法时, 我们会接收到一个编译器的警告.

我们可以通过nonatomicattribute 去指定合成的访问方法, 表明它只是简单地设置或者返回一个值, 并不保证访问方法在多个线程同时访问时会被完整地调用. 因此, 访问nonatomic属性比atomic属性更快, 也可以混合合成的访问方法和自定义的方法:

@interface XYZObject : NSObject
@property (nonatomic) NSObject *nonatomicObject;
@end
@implementation XYZObject
- (NSObject *)nonatomicObject {
    return _nonatomicObject;
}
// setter will be synthesized automatically
@end

Note: 属性的原子性(atomicity)并不等价于对象的线程安全(thread safety).

考虑如下情况: 有一个XYZPerson对象, 它的firstNamelastName 在一个线程上通过原子性的访问方法被修改. 如果另外一个线程在同时也访问了这两个属性值, 原子性的getter方法会返回完整的字符串(不会Crash), 但是无法保证这些值是属于彼此的正确的值. 如果firstName获取的是修改前的值,而lastName获取的是修改后的值,我们就会获取到一对不正确的姓名.

上面举的例子很简单, 但是当考虑到对象之间的复杂关系时, 线程安全就变得复杂得多. 线程安全的详细讲解见: 苹果官方文档-并发编程(之后会建新专栏结合我自己的理解翻译这个文档, 敬请期待 ^-^)

通过对象之间的所属关系(Ownership)和职责(Responsibility)管理对象图(Object Graph)

正如前面所述, Objective-C对象的内存是动态分配的(在堆上), 正因如此我们需要使用指针去存储对象地址. 和值类型不同, 指针类型的对象的生命周期并不总是可以通过作用域决定, 只要有其它对象需要使用它, 它就必须在内存中可以访问.

相比手动管理每个对象的生命周期, 我们更应当考虑对象之间的关系.

XYZPerson实例对象举例来说, 它"拥有(owned)"两个字符串属性: firstNamelastName.

所以只要这个XYZPerson实例对象存在, 它的两个属性就必须在内存中可以访问.

当对象A以这种方式"拥有"另外一个对象B时, 就可以说对象A强引用了对象B. 在Objective-C中, 一个对象只要被至少1个其它对象强引用时, 这个对象就会被保存在内存中. XYZPerson实例和它的两个字符串属性的关系如下图:

Image

XYZPerson从内存中释放(deallocated), 如果没有其它外界强引用的话, 它对应的两个字符串对象也会被释放.

考虑如下稍微更复杂些的应用场景:

Image

当用户点击Update按钮时, 预览(Preview)中会根据文本框中内容更新.

当用户首次输入了姓名并且点击了Update按钮时, 关系如下图所示:

Image

当用户修改了firstName:

Image

此时,虽然XYZPerson拥有了一个不同的firstName,但是由于XYZBadgeView实例保持着对于@"John"字符串的强引用关系, @"John"字符串将仍然保留在内存中.

一旦用户再次点击了Update按钮, XYZBadgeView实例需要更新内部属性以与XYZPerson对象保持一致, 如下图:

Image

在这个时候, 不再有任何对象强引用原来的@"John" 对象了, 所以它从内存中释放了.

默认情况下, Objective-C 的属性和变量对于对象都是强引用的. 这在大多数情况下是没有问题的, 但是在某些情况下可能会形成强引用循环(strong reference cycles).

避免强引用循环

虽然强引用在单方向的对象关系中表现很好, 但是我们需要小心, 当我们使用很多互相关联的对象时, 如果这些对象的强引用形成了循环, 他们之间彼此维持强引用, 因此在即使没有外界对他们进行强引用时,他们仍然会存留在内存之中.

一个明显的可能产生强循环引用的例子就是TableView对象(iOS中的UITableView和OS X的NSTableView) 和它的代理(delegate). 为了让TableView更具通用性,它将一些决定交给了外部对象代理实现. 也就是让另外一个对象决定它显示的内容或者如果用户与TableView交互时如何处理这些交互.

常见的场景就是TableView持有对于delegate的引用,而代理反过来又对TableView有引用,如图所示:

Image

当图中的Other Objects不再持有对于TableView和Delegate的强引用时, 问题就出现了:

Image

即使这两个对象已经没有必要在内存中保留了 -- 除了他们相互之间, 已经没有外部对象需要他们了, 而两者间的强引用相互维持了对方在内存中继续保留. 这就是强引用循环(strong reference cycle).

解决这个问题的方法就是使用***弱引用(weak reference)***来代替这其中一个强引用.弱引用不代表两个对象的从属关系, 因此也不会维持对象的生命周期.

如果TableView对于delegate使用弱引用持有, (UITableViewNSTableView 实际就是如此实现的), 就会如下图所示:

Image

当图中的Other Objects不再强引用TableView和delegate时, delegate就不再被任何对象强引用了:

Image

这样, delegate对象就会被释放, 从而释放了对于TableView的强引用, 随后TableView也会被释放:

Image

使用强/弱声明 管理对象从属关系

默认对象属性是这样声明的:

@property id delegate;

这会默认对自动合成的实例变量使用强引用. 可以通过weak attribute 声明一个弱引用.

@property (weak) id delegate;

Note: 与weak相对的attribute是strong. 由于strong是默认值, 所以并不需要显式声明.

局部变量(和非属性实例变量)对于对象默认也是强引用的. 这就意味着如下代码可以如预期运行:

    NSDate *originalDate = self.lastModificationDate;
    self.lastModificationDate = [NSDate date];
    NSLog(@"Last modification date changed from %@ to %@",
                        originalDate, self.lastModificationDate);

在这个例子中, 局部变量originDate维持了对于初始的lastModificationDate的强引用. 当lastModificationDate属性改变时, lastModification属性不再强引用原来的Date对象, 但是原来的Date对象被originalDate强引用, 还会保存在内存之中.

Note: 变量会维持对于另一个对象的强引用, 只要变量在它的作用域内并且没有被重新另一个对象或者nil赋值.

如果不想变量使用强引用, 可以通过__weak使用弱引用:

    NSObject * __weak weakVariable;

由于使用弱引用不能维持对象在内存中存留, 因此可能存在对象被释放, 但是引用仍然存在的情况. 为了避免危险的悬垂指针(dangling pointer) - 指向被释放对象的内存区域, 当弱引用对象被释放时, 引用了它的变量会被自动设置为nil

如果修改上面的例子说明:

    NSDate * __weak originalDate = self.lastModificationDate;
    self.lastModificationDate = [NSDate date];

这里的originalDate就会被设置为nil. 当self.lastModificationDate重新被赋值之后,这个属性不再对原Date对象进行强引用. 如果没有其它的强引用指向原Date对象, 原Date对象就会在内存中被释放, originalDate就会被设置为nil.

weak变量可能会引起疑惑, 比如如下代码:

    NSObject * __weak someObject = [[NSObject alloc] init];

在这个例子中, 新分配内存的NSObject对象没有被任何对象强引用, 因此它立即就被释放了, 并且someObject被自动置为nil.

Note: 与__weak相对的修饰符是__strong. 由于__strong是默认值, 因此不需要显式声明.

如果代码中需要多次访问一个weak属性时, 就需要考虑潜在可能的结果:

- (void)someMethod {
    [self.weakProperty doSomething];
    ...
    [self.weakProperty doSomethingElse];
}

在这种情况下,可能需要缓存弱引用对象到强引用类型的变量中, 以保证之后我们需要使用它时,在内存中可以访问到它.

- (void)someMethod {
    NSObject *cachedObject = self.weakProperty;
    [cachedObject doSomething];
    ...
    [cachedObject doSomethingElse];
}

在上面的例子中, cachedObject维持了一个指向weak属性值的强引用, 因此只要cacheObject在作用域中并且没有被重新赋值, 就可以保证原weak属性值不会被释放.

在使用weak属性前确定它不为空是十分重要的. 仅仅添加条件判断是不够的:

    if (self.someWeakProperty) {
        [someObject doSomethingImportantWith:self.someWeakProperty];
    }

因为如果在多线程应用中, 这个weak属性可能在条件判断和方法调用之间被释放, 使得判断无效. 因此, 我们需要声明一个强引用局部变量来缓存这个值:

    NSObject *cachedObject = self.someWeakProperty;           // 1
    if (cachedObject) {                                       // 2
        [someObject doSomethingImportantWith:cachedObject];   // 3
    }                                                         // 4
    cachedObject = nil;                                       // 5

在这个例子中, 在第1行带代码中创建了对于原值的强引用, 可以保证原值在条件判断和方法调用期间在内存中都是存在的. 在第5行, cacheObject被置为nil, 也就是释放了对原值的强引用 . 如果在此时原值不再被其它任何对象强引用, 原值将会在内存中被释放, 并且someWeakProperty将会被置为nil.

使用不安全不持有(Unsafe Unretained)引用

有一些Cocoa或者Cocoa Touch类还不支持弱引用, 那样我们就无法声明对应的weak属性或者weak变量了. 比如NSTextView, NSFont, NSColorSpace等等. 可以在Transitioning to ARC Release Notes中查看完整的列表.

如果我们需要在这种类中使用弱引用, 就不得不使用这种不安全的引用. 在属性中使用unsafe_unretained attribute:

@property (unsafe_unretained) NSObject *unsafeProperty;

对于变量, 我们需要使用__unsafe_unretained:

NSObject * __unsafe_unretained unsafeReference;

被unsafe_unretained修饰的属性和变量和weak引用很相似, 都不会保持对象在内存中存留, 但是和weak不同的是, 当对象内存被释放时, 对于对象的引用不会被设置为nil .这样的话, 原来对于此对象的引用就是不安全的(unsafe), 指向原对象的指针现在就会指向一块内存已被释放的区域, 变成悬垂指针(dangling pointer). 像悬垂指针发送消息会导致程序崩溃.

使用复制(Copy)维护它们的副本

在一些情况下, 对象希望设置它们的属性时, 他们可以自己管理一份设置的对象的副本.

比如: XYZBadgeView的类接口:

@interface XYZBadgeView : NSView
@property NSString *firstName;
@property NSString *lastName;
@end

声明了两个NSString属性, 默认都使用被所属对象强引用.

当另一个对象创建了一个字符串并设置为XYZBadgeView实例对象的属性时 :

    NSMutableString *nameString = [NSMutableString stringWithString:@"John"];
    self.badgeView.firstName = nameString;

由于NSMutableStringNSString的子类, 这是完全有效的. 虽然XYZBadgeView的实例对象认为它正在处理一个NSString对象, 但实际上它处理的是一个NSMutableString对象.

这就意味着这个字符串可以被改变:

    [nameString appendString:@"ny"];

在这种情况下, 虽然初始时为XYZBadgeView的实例对象设置的值是"John", 但是由于NSMutableString的值改变了, 所以现在XYZBadgeView中存储的是"Johnny"了.

因此我们可能希望XYZBadgeView实例管理一份自己的firstName或者lastName副本, 我们可以通过copyattribute在设置属性时捕获对象并创建副本.

@interface XYZBadgeView : NSView
@property (copy) NSString *firstName;
@property (copy) NSString *lastName;
@end

现在XYZBadgeView拥有了自己管理的两份字符串的副本. 即使使用NSMutableString为其赋值,并不断改变NSMutableString的值, XYZBadgeView只会捕获为其赋值之时的字符串值:

    NSMutableString *nameString = [NSMutableString stringWithString:@"John"];
    self.badgeView.firstName = nameString;
    [nameString appendString:@"ny"];

此时, XYZBadgeView实例的firstName属性不再会被影响, 仍然保持着原来的字符串"John"的副本.

copy attribute 表明属性会使用强引用, 因为它必须持有它自己新创建的那个对象副本.

Note: 只有支持并实现了NSCopying的对象才可以被设置copy attribute, 因此对象必须遵循了NSCopying协议.

协议和NSCopying的相关官方文档见: Protocols Define Messaging Contracts, NSCopying, Advanced Memory Management Programming Guide. (之后的文章也会翻译相关部分, 敬请期待! ^-^)

如果需要为用copy attribute 修饰的实例变量直接赋值(比如在初始化方法中), 不要忘记使用原值的copy方法生成副本后再为其赋值:

- (id)initWithSomeOriginalString:(NSString *)aString {
    self = [super init];
    if (self) {
        _instanceVariableForCopyProperty = [aString copy];
    }
    return self;
}

练习:

  1. 修改XYZPerson类的sayHello方法, 使其输出firstNamelastName的组合.

  2. XYZPerson声明并且实现一个新的指定初始化方法(Designated initializer)以创建包含firstName,lastName,dateOfBirth三个参数的初始化方法, 另外实现一个对应的类工厂方法.

    Tip: 不要忘记重写init方法.

  3. 测试一下当我们为firstName设置了一个NSMutableString实例后, 在调用修改后的sayHello方法前, 修改那个NSMutableString实例的值会发生什么. 为firstName添加copy attribute后再测试一下.

  4. main()函数中尝试使用一些strong和weak变量创建XYZPerson类. 证实一下strong变量可以维持XYZPerson变量在内存中存留足够长的时间.

    Tip: 为帮助验证XYZPerson实例从内存中释放的时机, 可以在XYZPerson的实现中提供dealloc方法. 这个方法在Objective-C对象从内存中释放时会被自动调用. 一般来说, 这个方法用来释放之前手动分配的内存空间(比如通过C的malloc方法分配内存, 详见Advanced Memory Management Programming Guide).

    在这个练习中, 可以像这样重写dealloc方法, 并打印日志信息:

- (void)dealloc {
    NSLog(@"XYZPerson is being deallocated");
}

尝试把每个XYZPerson的指针设置为nil以验证对象内存被释放.

Note: 在XCode项目的Command Line Tool 模板中会在main()函数中使用@autoreleasepool{}代码块. 为了使用编译器的ARC特性, 我们的代码需要写在@autoreleasepool{}代码块中.

自动释放池(Autorelease pools)不在本文讨论范围内, 在Advanced Memory Management Programming Guide中有详细说明. 当我们在编写Cocoa或者Cocoa Touch应用时, 我们一般不用手动创建autoreleasepool, 这个设置在框架中已经生效了.

  1. 修改XYZPerson 类的接口, 添加对于伙伴或者配偶的信息. 此时我们需要决定模型之间的关系, 仔细思考并管理对象之间的关系.

参考资料: Encapsulating Data