之前介绍过,只要指针变量指向了某个对象,那么相应的对象就会多一个拥有者,并且不会被程序释放。这种指针特性(attribute)称为强引用(strong reference)。
程序也可以选择让指针变量不影响其指向对象的拥有者个数。这种不会改变对象拥有者个数的指针特性称为弱引用(weak reference)。
弱引用非常适合解决一种称为强引用循环(strong reference cycle,有时也称为保留循环)的内存管理问题。当两个或两个以上的对象相互之间有强引用特性的指针关联时,就会产生强引用循环。强引用循环会导致内存泄露。当两个对象互相拥有时,将无法通过ARC机制来释放。即使应用中的其他对象都释放了针对这两个对象的所有权,这两个对象及其拥有的所有对象也无法被释放。
因此在编写应用时,程序员必须自己做一些额外的工作,才能帮助ARC解决强引用循环所导致的内存泄露问题。解决强引用循环的途径是将某个指针的特性设置为弱引用。
下面为RandomItems加入一处强引用循环,借以向读者介绍如何解决此类问题。首先,让BNRItem对象能够保存另外一个BNRItem对象(这样就能表现背包和钱包这样的物品关系)。此外,BNRItem对象会有一个指针实例变量,指回包含该对象的BNRItem对象。
在BNRItem.h中,增加两个实例变量和相应的存取方法,代码如下:
@interface BNRItem : NSObject
{
NSString *_itemName;
NSString *_serialNumber;
int _valueInDollars;
NSDate *_dateCreated;
BNRItem *_containedItem;
BNRItem *_container;
}
+ (instancetype)randomItem;
- (instancetype)initWithItemName:(NSString *)name
valueInDollars:(int)value
serialNumber:(NSString *)sNumber;
- (instancetype)initWithItemName:(NSString *)name;
- (void)setContainedItem:(BNRItem *)item;
- (BNRItem *)containedItem;
- (void)setContainer:(BNRItem *)item;
- (BNRItem *)container;
在BNRItem.m中实现新加入的存取方法,代码如下:
- (void)setContainedItem:(BNRItem *)item
{
_containedItem = item;
// 将item加入容纳它的BNRItem对象时,
// 会将它的container实例变量指向容纳它的对象。
item.container = self;
}
- (BNRItem *)containedItem
{
return _containedItem;
}
- (void)setContainer:(BNRItem *)item
{
_container = item;
}
- (BNRItem *)container
{
return _container;
}
在main.m中,首先删除创建并枚举多个随机BNRItem对象的代码。然后创建两个新的BNRItem对象并加入items数组。最后将这两个对象用指针关联起来,代码如下:
int main (int argc, const char * argv)
{
@autoreleasepool {
NSMutableArray *items = [[NSMutableArray alloc] init];
for (int i = 0; i < 10; i++) {
BNRItem *item = [BNRItem randomItem];
[items addObject:item];
}
BNRItem *backpack = [[BNRItem alloc] initWithItemName:@/"Backpack/"];
[items addObject:backpack];
BNRItem *calculator = [[BNRItem alloc] initWithItemName:@/"Calculator/"];
[items addObject:calculator];
backpack.containedItem = calculator;
backpack = nil;
calculator = nil;
for (BNRItem *item in items)
NSLog(@/"%@/", item);
NSLog(@/"Setting items to nil.../");
items = nil;
}
return 0;
}
修改后的RandomItems的对象图如图3-4所示。
>
图3-4 存在强引用循环问题的RondomItems
构建并运行应用,检查控制台输出,会发现应用并没有输出释放这些对象的提示信息。
RondomItems无法正确地释放对象是由强引用循环引起的:backpack变量指向的对象和calculator变量指向的对象都有强引用特性的指针,并指向彼此。图3-5显示的是在应用将items设置为nil后,没有被释放并继续占用内存的所有对象。
图3-5 对象没有被释放,造成内存泄露
在RandomItems将items设置为nil后,除了新创建的两个对象自身外,应用的其余部分(例如这段代码中的main函数)都将无法使用这两个对象,并且这两个无法被使用的对象会一直占用应用的内存。此外,因为应用不会释放这两个对象,所以,如果这些对象的实例变量还指向了其他对象,那么这些被指向的对象也不会被释放。
要解决RandomItems的强引用循环问题,需要将新创建的两个BNRItem对象之间的某个指针改为弱引用特性。在决定将哪个指针改为弱引用前,可以先为存在强引用循环问题的多个对象决定相应的父-子关系(parent-child relationship)。确定父-子关系后,就可以让父对象拥有子对象,并确保子对象不会拥有父对象。以RandomItems的强引用循环为例,backpack是父对象,calculator是子对象。根据以上规则,可以将backpack指向calculator(_containedItem)的指针保留为强引用特性,并将calculator指向backpack(_container)的指针改为弱引用特性。
使用__weak关键字,可以将某个变量声明为具有弱引用特性。在BNRItem.h中,为实例变量container增加弱引用特性,代码如下:
_ _weak BNRItem *_container;
构建并运行应用,修改后的RandomItems能正确地释放新创建的两个BNRItem对象。
大部分强引用循环问题都可以为其确定一个父-子关系。通常情况下,父对象应该使用具有强引用特性的指针,指向子对象。而子对象则应该使用具有弱引用特性的指针,指回父对象。这样就可以避免强引用循环问题。
如果某个子对象具有一个强引用特性的指针,指向父对象的父对象,一样也会导致强引用循环。所以上述规则也适用于:如果某个子对象需要有一个指针,指向父对象的父对象(或者是父对象的父对象的父对象,等等),那么该指针必须具有弱引用特性。
Xcode的Leaks工具可以帮助我们找出强引用循环问题。第14章会介绍如何使用Leaks工具。
具有弱引用特性的指针指向的对象被释放后,指针会自动设置为nil。以RandomItems为例,当应用释放backpack后,会自动地将calculator的实例变量_container设置为nil。这就是弱引用的好处:如果_container没有被设置为nil,backpack对象被释放后会留下一个空指针,访问该指针就会引起程序崩溃。
修正强引用循环问题后,RandomItems的新对象图如图3-6所示。注意,图中代表指针变量_container的箭头已经改为虚线。虚线代表具有弱引用特性的指针。强引用特性的指针变量仍然用实线表示。
图3-6 解决强引用循环问题后的RondomItems