自动置空的弱引用

Mike Ash Friday Q&A 中文译文:自动置空的弱引用

作者 TommyWu
封面圖片: 自动置空的弱引用

译文 · 原文: Friday Q&A 2010-07-16: Zeroing Weak References in Objective-C · 作者 Mike Ash

原文:https://www.mikeash.com/pyblog/friday-qa-2010-07-16-zeroing-weak-references-in-objective-c.html 发布:2010-07-16 作者:Mike Ash 译者:MiMo(mimo-v2.5-pro);代码块保留英文原样


又是两周一次的专栏时间。在本周的 Friday Q & A 中,Mike Shields 建议我谈谈 Objective-C 中的弱引用(weak references),特别是归零弱引用(zeroing weak references)。我更进一步,实际实现了一个类,该类使用手动内存管理在 Objective-C 中提供归零弱引用。

首先,什么是弱引用?简而言之,弱引用就是一个指向对象的引用(在 Objective-C 中是指针),但它不参与保持该对象的存活。例如,在内存管理下,下面的 setter 方法就创建了一个指向新对象的弱引用:

- (void)setFoo: (id)newFoo
{
_foo = newFoo;
}

弱引用在 Cocoa 中很常见,用以处理保留环(retain cycles)。Cocoa 中的委托对象(delegate)几乎总是采用弱引用,正是出于这个原因。

零弱引用
弱引用对于避免保留环之类问题很有用,但其效用受限于内在的危险性。在 Objective-C 中使用普通弱引用时,当目标对象被销毁后,会留下一个悬垂指针(dangling pointer)。如果你的代码尝试使用这个指针,程序就会崩溃,甚至更糟。

零弱引用(Zeroing weak references)消除了这种危险。它们的工作方式与普通弱引用完全一样,区别在于当目标对象被销毁时,它们会自动变为 nil。任何时候通过零弱引用访问对象,你都能保证要么访问到一个有效的、存活的对象,要么得到 nil。只要你的代码能够处理 nil,那么就绝对安全。

由于这种安全性,零弱引用(zeroing weak reference)的用途远比非安全类型广泛。一个典型例子是对象缓存(object cache)。使用弱引用的对象缓存可以在对象存活期间引用它们,并在不再需要时让对象正常释放。如果客户端请求一个仍存活的对象,缓存可以直接提供该对象而无需创建新实例;若对象已被销毁,缓存则可以安全地创建新对象。

它们也可以用于更普通的场景,适用于任何你想保持对某个对象的引用,但又不希望该对象超出其正常生命周期继续占用内存的情况。例如,你可能需要跟踪一个窗口,但又不希望窗口关闭后它仍然驻留在内存中。你可以通过设置通知观察者来监听窗口消失的事件,但使用零弱引用(zeroing weak reference)是更简洁的做法。再比如,在代码块(block)中使用指向 self 的零弱引用,既能防止循环引用(retain cycle),又能确保如果代码块在 self 被释放后调用时程序不会崩溃。即使是标准的委托指针,也能通过零弱引用得到优化,因为它消除了当委托对象先于指向它的对象被释放时可能出现的罕见但烦人的 bug。

如果你在 Objective-C 中使用垃圾回收,那么有个好消息!Objective-C 垃圾回收器已经支持通过类型修饰符 __weak 来实现零弱引用。你可以这样声明任何实例变量:

__weak id _foo;

那么,如果你没有使用垃圾回收怎么办?虽然我们都能使用它会很棒,但很多人由于各种原因无法使用,其中一个最常见的原因是 iOS 根本不支持垃圾回收。好吧,在 Objective-C 中使用手动内存管理时,你一直无法使用归零弱引用。

介绍 MAZeroingWeakRef

我们这些使用手动内存管理的人现在可以受益于归零弱引用了!MAZeroingWeakRef 实现了以下接口:

@interface MAZeroingWeakRef : NSObject
{
id _target;
}
+ (id)refWithTarget: (id)target;
- (id)initWithTarget: (id)target;
- (void)setCleanupBlock: (void (^)(id target))block;
- (id)target;
@end

-setCleanupBlock: 方法适用于更高级的使用场景。通常,自动置零弱引用是一个被动对象。你可以随时查询其目标对象,它要么返回一个有效对象,要么返回 nil。但有时你希望在引用被置零时执行一些额外操作,比如注销通知观察者。传递给 -setCleanupBlock: 的块会在引用被置零时执行,从而允许你设置此类附加操作。

例如,以下是如何使用 MAZeroingWeakRef 实现标准的委托模式:

// instance variable
MAZeroingWeakRef *_delegateRef;
// setter
- (void)setDelegate: (id)newDelegate
{
[_delegateRef release];
_delegateRef = [[MAZeroingWeakRef alloc] initWithTarget: newDelegate];
}
- (void)doSomethingAndCallDelegate
{
[self _doSomething];
id delegate = [_delegateRef target];
if([delegate respondsToSelector: @selector(someDelegateMethod)])
[delegate someDelegateMethod];
}

MAZeroingWeakRef 是完全线程安全的,无论是在多线程环境中访问它,还是当目标对象(target object)在一个线程中被销毁而弱引用(weak reference)在另一个线程中被访问时,都是安全的。

工作原理 清零弱引用(zeroing weak reference)的工作原理概念相当直接。追踪(track)所有指向目标的这类引用。当一个对象被销毁时,在调用 dealloc 方法之前,将所有这些引用清零(zero out)。用锁包裹一切操作以实现线程安全。

然而,如何完成每一步的细节可能会变得有些棘手。

追踪一个目标的所有清零弱引用并不算太难。一个全局的 CFMutableDictionary 将目标映射到 CFMutableSet 对象,这些集合持有每个目标对应的清零弱引用。我使用 CF 类以便能够自定义内存管理;我不希望目标或弱引用被 retain。

在调用 dealloc 之前清零所有弱引用则会更棘手一些……

答案是采用动态子类化(dynamic subclassing),就像键值观察(Key-Value Observing)的实现那样。当一个对象成为零弱引用(zeroing weak reference)的目标时,会创建该对象所属类的一个新子类。这个新子类的 -dealloc 方法会负责将所有弱引用置零,然后调用父类的实现,以便正常的释放链能够继续执行。新子类还会覆写 -release 方法以加锁,从而确保线程安全。(如果不覆写该方法,可能会出现这样的情况:一个线程正在释放一个引用计数为 1 的对象,而同时另一个线程正在从 MAZeroingWeakRef 中获取该对象。后者可能会尝试在对象已被标记销毁后将其复活,这是非法的。)

当然,你不会想为每一个目标对象都创建一个新子类,而是每个目标类只需要一个子类。通过维护一个小型的被覆写类表,可以确保每个普通类最多只创建一个新子类。

最后一步,是将目标对象的 isa 指针设置为这个新子类,从而确保新方法生效。

CoreFoundation 的棘手之处 上述策略在处理免费桥接类(toll-free bridged classes)如 NSCFString 时会遇到障碍。由于这些类的实现方式,一旦尝试使用它们,更改此类对象的类(class)会导致无限递归(infinite recursion)和崩溃。CoreFoundation 代码会检测到被更改的类,将其视为纯粹的 Objective-C 类,进而转而调用对应的 Objective-C 方法。随后,NSCF 方法又会回调至 CoreFoundation,崩溃随即发生。

尽管我确实找到了解决此问题的方法,但它极其复杂棘手,因此我将另起一篇文章详细说明,该文将在两周后发布。

代码 一如既往,你可以从我的公共 Subversion 仓库中获取 MAZeroingWeakRef 的代码:

svn co http://mikeash.com/svn/ZeroingWeakRef/

我将通过一个稍作简化的版本来讲解 MAZeroingWeakRef 的实现。由于前面提到的 CoreFoundation hackery(CoreFoundation hackery)具有相当疯狂的特性,本周我将跳过这些部分,只讨论合理的 Objective-C 实现细节。有一个名为 COREFOUNDATION_HACK_LEVEL 的宏可用于控制启用多少 CoreFoundation hackery。在级别 2 下,代码会全力运用 hackery,完整支持对 CoreFoundation 对象的弱引用(weak reference)。在级别 1 下,会引用并使用一些不太重要的私有符号(private symbol)来可靠判断对象是否经过桥接,且在尝试对桥接对象创建弱引用时,代码会直接断言(assert)。在级别 0 下,代码会在尝试对桥接对象创建弱引用时断言,并通过检查类名是否以 NSCF 为前缀来简单判断是否经过桥接。本周的讨论将基于代码以级别 0 编译的情况。

全局变量(Globals) MAZeroingWeakRef 使用一些全局变量进行各种管理用途。首先是互斥锁(mutex):

static pthread_mutex_t gMutex;

接下来,需要一个 CFMutableDictionary(可变字典)来映射目标对象(target objects)到指向它们的弱引用(weak references):

static CFMutableDictionaryRef gObjectWeakRefsMap; // maps (non-retained) objects to CFMutableSetRefs containing weak refs
static NSMutableSet *gCustomSubclasses;
static NSMutableDictionary *gCustomSubclassMap; // maps regular classes to their custom subclasses
+ (void)initialize
{
if(self == [MAZeroingWeakRef class])
{
CFStringCreateMutable(NULL, 0);
pthread_mutexattr_t mutexattr;
pthread_mutexattr_init(&mutexattr;);
pthread_mutexattr_settype(&mutexattr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&gMutex, &mutexattr;);
pthread_mutexattr_destroy(&mutexattr;);
gCustomSubclasses = [[NSMutableSet alloc] init];
gCustomSubclassMap = [[NSMutableDictionary alloc] init];
}
}
static void WhileLocked(void (^block)(void))
{
pthread_mutex_lock(&gMutex;);
block();
pthread_mutex_unlock(&gMutex;);
}
static void AddWeakRefToObject(id obj, MAZeroingWeakRef *ref)
{
CFMutableSetRef set = (void *)CFDictionaryGetValue(gObjectWeakRefsMap, obj);
if(!set)
{
set = CFSetCreateMutable(NULL, 0, NULL);
CFDictionarySetValue(gObjectWeakRefsMap, obj, set);
CFRelease(set);
}
CFSetAddValue(set, ref);
}
static void RemoveWeakRefFromObject(id obj, MAZeroingWeakRef *ref)
{
CFMutableSetRef set = (void *)CFDictionaryGetValue(gObjectWeakRefsMap, obj);
CFSetRemoveValue(set, ref);
}
static void ClearWeakRefsForObject(id obj)
{
CFMutableSetRef set = (void *)CFDictionaryGetValue(gObjectWeakRefsMap, obj);
[(NSSet *)set makeObjectsPerformSelector: @selector(_zeroTarget)];
CFDictionaryRemoveValue(gObjectWeakRefsMap, obj);
}

首先是便利构造函数(convenience constructor)和初始化方法(initializer)。大部分是直接明了的:

+ (id)refWithTarget: (id)target
{
return [[[self alloc] initWithTarget: target] autorelease];
}
- (id)initWithTarget: (id)target
{
if((self = [self init]))
{
_target = target;
RegisterRef(self, target);
}
return self;
}

dealloc 方法的实现同样调用一个工具函数来移除弱引用对象:

- (void)dealloc
{
UnregisterRef(self);
[_cleanupBlock release];
[super dealloc];
}
- (NSString *)description
{
return [NSString stringWithFormat: @"<%@: %p -> %@>", [self class], self, [self target]];
}
- (void)setCleanupBlock: (void (^)(id target))block
{
block = [block copy];
[_cleanupBlock release];
_cleanupBlock = block;
}
- (id)target
{
__block id ret;
WhileLocked(^{
ret = [_target retain];
});
return [ret autorelease];
}
- (void)_zeroTarget
{
if(_cleanupBlock)
{
_cleanupBlock(_target);
[_cleanupBlock release];
_cleanupBlock = nil;
}
_target = nil;
}

UnregisterRef 的实现很简单。从 MAZeroingWeakRef 中取出目标对象(target),获取指向该目标对象的引用表(table of references),然后移除给定的引用。整个过程用锁(lock)包裹以确保目标对象不会在该操作过程中被释放:

static void UnregisterRef(MAZeroingWeakRef *ref)
{
WhileLocked(^{
id target = ref->_target;
if(target)
RemoveWeakRefFromObject(target, ref);
});
}
static void RegisterRef(MAZeroingWeakRef *ref, id target)
{
WhileLocked(^{
EnsureCustomSubclass(target);
AddWeakRefToObject(target, ref);
});
}
static void EnsureCustomSubclass(id obj)
{
if(!GetCustomSubclass(obj))
{
Class class = object_getClass(obj);
Class subclass = [gCustomSubclassMap objectForKey: class];
if(!subclass)
{
subclass = CreateCustomSubclass(class, obj);
[gCustomSubclassMap setObject: subclass forKey: class];
[gCustomSubclasses addObject: subclass];
}
object_setClass(obj, subclass);
}
}
static Class GetCustomSubclass(id obj)
{
Class class = object_getClass(obj);
while(class && ![gCustomSubclasses containsObject: class])
class = class_getSuperclass(class);
return class;
}
static Class CreateCustomSubclass(Class class, id obj)
{
if(IsTollFreeBridged(class, obj))
{
NSCAssert(0, @"Cannot create zeroing weak reference to object of type %@ with COREFOUNDATION_HACK_LEVEL set to %d", class, COREFOUNDATION_HACK_LEVEL);
return class;
}
else
{

IsTollFreeBridged 的实现仅检查类名是否以 NSCF 开头:

static BOOL IsTollFreeBridged(Class class, id obj)
{
return [NSStringFromClass(class) hasPrefix: @"NSCF"];
}
NSString *newName = [NSString stringWithFormat: @"%s_MAZeroingWeakRefSubclass", class_getName(class)];
const char *newNameC = [newName UTF8String];
Class subclass = objc_allocateClassPair(class, newNameC, 0);
Method release = class_getInstanceMethod(class, @selector(release));
Method dealloc = class_getInstanceMethod(class, @selector(dealloc));
class_addMethod(subclass, @selector(release), (IMP)CustomSubclassRelease, method_getTypeEncoding(release));
class_addMethod(subclass, @selector(dealloc), (IMP)CustomSubclassDealloc, method_getTypeEncoding(dealloc));
objc_registerClassPair(subclass);
return subclass;
}
}

问题在于,直接编写 [super release] 是行不通的,因为编译器只允许在真正的编译期方法实现(compile-time method implementation)中使用它。为了执行等效的操作,必须找出自定义弱引用子类(custom weak reference subclass)的超类。这可以通过一个简单的辅助函数实现,该函数会调用 GetCustomSubclass 并返回该类的超类:

static Class GetRealSuperclass(id obj)
{
Class class = GetCustomSubclass(obj);
NSCAssert(class, @"Coudn't find ZeroingWeakRef subclass in hierarchy starting from %@, should never happen", object_getClass(obj));
return class_getSuperclass(class);
}
static void CustomSubclassRelease(id self, SEL _cmd)
{
Class superclass = GetRealSuperclass(self);
IMP superRelease = class_getMethodImplementation(superclass, @selector(release));
WhileLocked(^{
((void (*)(id, SEL))superRelease)(self, _cmd);
});
}
static void CustomSubclassDealloc(id self, SEL _cmd)
{
ClearWeakRefsForObject(self);
Class superclass = GetRealSuperclass(self);
IMP superDealloc = class_getMethodImplementation(superclass, @selector(dealloc));
((void (*)(id, SEL))superDealloc)(self, _cmd);
}

示例:MAZeroingWeakRef 的基本用法很简单:创建一个对象,然后使用 alloc/init 创建一个针对它的零化弱引用(zeroing weak reference)。

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSObject *obj = [[NSObject alloc] init];
MAZeroingWeakRef *ref = [[MAZeroingWeakRef alloc] initWithTarget: obj];
NSLog(@"%@", [ref target]);
[obj release];
[pool release];
NSLog(@"%@", [ref target]);

使用清理块(cleanup block)同样简单:

NSObject *obj = [[NSObject alloc] init];
MAZeroingWeakRef *ref = [[MAZeroingWeakRef alloc] initWithTarget: obj];
[ref setCleanupBlock: ^(id target) { NSLog(@"Cleaned object %p!", target); }];
[obj release];

将普通实例变量(instance variable)转换为零弱引用(zeroing weak reference)的一种简单方法是,在 getter 和 setter 方法中使用 MAZeroingWeakRef,并确保在其他代码中始终通过 getter 访问该变量:

// ivar
MAZeroingWeakRef *_somethingWeakRef;
// accessors
- (void)setSomething: (Something *)newSomething
{
[_somethingWeakRef release];
_somethingWeakRef = [[MAZeroingWeakRef alloc] initWithTarget: newSomething];
}
- (Something *)something
{
return [_somethingWeakRef target];
}
// use
- (void)doThing
{
[[self something] doThingWithObject: self];
}

对于更高级的用法,这里是对 NSNotificationCenter 的一个补充,它消除了在 dealloc 中手动移除观察者的需要。

@implementation NSNotificationCenter (MAZeroingWeakRefAdditions)
- (void)addWeakObserver: (id)observer selector: (SEL)selector name: (NSString *)name object: (NSString *)object
{
[self addObserver: observer selector: selector name: name object: object];
MAZeroingWeakRef *ref = [[MAZeroingWeakRef alloc] initWithTarget: observer];
[ref setCleanupBlock: ^(id target) {
[self removeObserver: target name: name object: object];
[ref autorelease];
}];
}
@end

类似地,如果你厌倦了因 NSTableView 数据源对象先于视图本身被释放而导致的莫名崩溃,你可以轻松解决这个问题:

@implementation NSTableView (MAZeroingWeakRefAdditions)
- (void)setWeakDataSource: (id <NSTableViewDataSource>)source
{
[self setDataSource: source];
MAZeroingWeakRef *ref = [[MAZeroingWeakRef alloc] initWithTarget: observer];
[ref setCleanupBlock: ^(id target) {
if([self dataSource] == target) // double check for safety
[self setDataSource: nil];
[ref autorelease];
}];
}
@end

本质上,任何时候当你持有一个弱引用(weak reference,即未进行 retain 或 copy 的对象引用)时,都应该使用 MAZeroingWeakRef 替代原始的未保留指针。这能为你省去许多麻烦和痛苦,并且使用起来非常简单。

ZeroingCollections
仓库中包含了 MAWeakArray 和 MAWeakDictionary,它们分别是 NSMutableArray 和 NSMutableDictionary 的子类,对其中的内容使用归零弱引用(zeroing weak references)。MAWeakDictionary 使用强引用键(strong keys)映射到弱引用对象,这对许多缓存场景(caching scenarios)很有用。这里我不再详述它们的代码,因为它们很简单,如果你感兴趣,可以自行查看仓库中的代码。

虽然我没有编写,但完全可以创建一个弱键版本的 NSMutableSet 和 NSMutableDictionary,使用弱引用的键(weak keys)来替代或补充弱引用的对象。由于弱引用带来的哈希和相等性问题(hashing / equality issues),这类实现会更棘手,但无疑是可以做到的。

结论

零化弱引用(Zeroing weak references)是众多编程语言中一种极为实用的特性。即便在运行垃圾收集(garbage collection)机制时 Objective-C 也支持它们,但在没有 GC 的环境下,Objective-C 代码曾长期被迫使用非零化弱引用(non-zeroing weak references),这既棘手又危险。

MAZeroingWeakRef 将零化弱引用引入了手动内存管理的 Objective-C 环境。尽管内部实现运用了一些技巧,但其 API 使用起来极为简便。通过自动清零弱引用,你可以避免许多潜在的崩溃和数据损坏问题。零化弱引用还可用于诸如对象缓存之类的场景 —— 在这些场景下,非零化弱引用根本不实用。

该代码遵循 BSD 许可证发布。

在两周后的下一期 Friday Q & A 中,我将探讨 MAZeroingWeakRef 如何绕过 CoreFoundation 对象的相关问题。届时再会!


#Original (English)

Source: https://www.mikeash.com/pyblog/friday-qa-2010-07-16-zeroing-weak-references-in-objective-c.html

It’s that time of the biweek again. For this week’s Friday Q&A, Mike Shields has suggested that I talk about weak references in Objective-C, and specifically zeroing weak references. I’ve gone a bit further and actually implemented a class that provides zeroing weak references in Objective-C using manual memory management.

Weak References First, what is a weak reference? Simply put, a weak reference is a reference (pointer, in Objective-C land) to an object which does not participate in keeping that object alive. For example, using memory management, this setter creates a weak reference to the new object:

- (void)setFoo: (id)newFoo
{
_foo = newFoo;
}

Weak references are common in Cocoa in order to deal with retain cycles. Delegates in Cocoa are almost always weak references for exactly this reason.

Zeroing Weak References Weak references are useful for things like avoiding retain cycles, but their utility is limited due to their inherent danger. With a plain weak reference in Objective-C, when the target object is destroyed, you’re left with a dangling pointer. If your code tries to use that pointer, it will crash or worse.

Zeroing weak references eliminate this danger. They work just like a regular weak reference, except that when the target object is destroyed, they automatically become nil. At any time you access an object through a zeroing weak reference, you’re guaranteed to either access a valid, live object, or get nil. As long as your code can handle nil, then you’re perfectly safe.

Because of this safety, a zeroing weak reference can be useful for much more than the unsafe kind. One example is an object cache. An object cache using weak references can refer to objects as long as they’re alive, and then let them deallocate when no longer needed. If a client requests an object that’s still alive, it can obtain it without having to create a new object. If the object has already been destroyed, the cache can safely create a new object.

They can be used for much more mundane purposes as well, for any case where you want to keep a reference to an object but don’t want to keep that object in memory beyond its normal lifetime. For example, you might track a window but not want to keep it in memory after it’s closed. You could deal with this by setting up a notification observer and seeing when the window goes away, but a zeroing weak reference is a much simpler way to do it. As another example, a zeroing weak reference to self used in a block can prevent a retain cycle while ensuring that your program doesn’t crash if the block is called after self is deallocated. Even a standard delegate pointer is made better with a zeroing weak reference, as it eliminates rare but annoying bugs which can appear if the delegate is deallocated before the object that points to it.

If you’re using garbage collection in Objective-C, then good news! The Objective-C garbage collector already supports zeroing weak references using the type modifier __weak. You can just declare any instance variable like so:

__weak id _foo;

What if you aren’t using garbage collection, though? While it would be great if we all could, many of us can’t for various reasons, one of the most common being that garbage collection simply isn’t supported on iOS. Well, until now you’ve been out of luck when it comes to zeroing weak references with manual memory management in Objective-C.

Introducing MAZeroingWeakRef Those of us who use manual memory management can now benefit from zeroing weak references! MAZeroingWeakRef implements the following interface:

@interface MAZeroingWeakRef : NSObject
{
id _target;
}
+ (id)refWithTarget: (id)target;
- (id)initWithTarget: (id)target;
- (void)setCleanupBlock: (void (^)(id target))block;
- (id)target;
@end

The -setCleanupBlock: method exists for more advanced uses. Normally a zeroing weak reference is a passive object. You can query its target at any time, and it either gives you an object or nil. But sometimes you want to take some additional action when the reference is zeroed out, such as unregistering a notification observer. The block passed to -setCleanupBlock: runs when the reference is zeroed out, allowin gyou to set up additional actions like that.

As an example, here’s how to write the standard delegate pattern using MAZeroingWeakRef:

// instance variable
MAZeroingWeakRef *_delegateRef;
// setter
- (void)setDelegate: (id)newDelegate
{
[_delegateRef release];
_delegateRef = [[MAZeroingWeakRef alloc] initWithTarget: newDelegate];
}
- (void)doSomethingAndCallDelegate
{
[self _doSomething];
id delegate = [_delegateRef target];
if([delegate respondsToSelector: @selector(someDelegateMethod)])
[delegate someDelegateMethod];
}

MAZeroingWeakRef is completely thread safe, both in terms of accessing it from multiple threads, and in terms of having the target object be destroyed in one thread while the weak reference is accessed from another thread.

How Does it Work? The concept of how a zeroing weak reference works is pretty straightforward. Track all such references to a target. When an object is destroyed, zero out all of those references before calling dealloc. Wrap everything in a lock so that it’s thread safe.

The details of how to accomplish each step can get tricky, though.

Tracking all zeroing weak references to a target isn’t too tough. A global CFMutableDictionary maps targets to CFMutableSet objects which hold the zeroing weak references to each target. I use the CF classes so that I can customize the memory management; I don’t want the targets or weak references to be retained.

Zeroing all of the weak references before calling dealloc gets a little trickier…

The answer to that is to use dynamic subclassing, as done in the implementation of Key-Value Observing. When an object is targeted by a zeroing weak reference, a new subclass of that object’s class is created. The -dealloc method of the new subclass takes care of zeroing out all of the weak references and then calls through to super so that the normal chain of deallocations can occur. The new subclass also overrides -release to take a lock so that everything is thread safe. (Without that override, it would be possible for one thread to release an object with a retain count of 1 at the same time that another thread retrieved the object from a MAZeroingWeakRef. The retrieval would then try to resurrect the object after it had already been marked for destruction, which is illegal.)

Of course you don’t want to make a new subclass for every single targeted object, but only one subclass is necessary per target class. A small table of overridden classes ensures that no more than one new subclass is created for each normal class.

As the final step, the class of the target object is set to be the new subclass, ensuring that the new methods take effect.

CoreFoundation Trickiness The above strategy runs into a snag with toll-free bridged classes like NSCFString. Because of the way they’re implemented, changing the class of such an object causes infinite recursion and a crash the moment that something tries to use them. The CoreFoundation code sees the changed class, assumes it’s a pure Objective-C class, and calls through to the equivalent Objective-C method. The NSCF method then calls back to CoreFoundation. A crash rapidly ensues.

While I did figure out a solution to this problem, it is so hairy and complicated that I will save it for a separate article to be posted in two weeks.

Code As usual, you can get the code for MAZeroingWeakRef from my public Subversion repository:

svn co http://mikeash.com/svn/ZeroingWeakRef/

I will be walking through a somewhat abbreviated version of MAZeroingWeakRef. Due to the crazy nature of the CoreFoundation workaround I mentioned above, I will skip over those parts and only discuss the sane Objective-C bits this week. There is a macro called COREFOUNDATION_HACK_LEVEL which allows control over how much CoreFoundation hackery is enabled. At level 2 you get full-on hackery with full support for weak references to CoreFoundation objects. With level 1, some less important private symbols are referenced and used to reliably decide whether an object is bridged or not, and the code simply asserts if trying to create a weak reference to a bridged object. At level 0, the code asserts when trying to create a weak reference to a bridged object, and checks for bridging simply by looking for a prefix of NSCF in the class name. For this week, I will be discussing the code as if it were compiled with level 0.

Globals MAZeroingWeakRef makes use of some global variables for various housekeeping uses. First off is a mutex:

static pthread_mutex_t gMutex;

Next up, a CFMutableDictionary is needed to map the target objects to the weak references which target them:

static CFMutableDictionaryRef gObjectWeakRefsMap; // maps (non-retained) objects to CFMutableSetRefs containing weak refs
static NSMutableSet *gCustomSubclasses;
static NSMutableDictionary *gCustomSubclassMap; // maps regular classes to their custom subclasses
+ (void)initialize
{
if(self == [MAZeroingWeakRef class])
{
CFStringCreateMutable(NULL, 0);
pthread_mutexattr_t mutexattr;
pthread_mutexattr_init(&mutexattr;);
pthread_mutexattr_settype(&mutexattr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&gMutex, &mutexattr;);
pthread_mutexattr_destroy(&mutexattr;);
gCustomSubclasses = [[NSMutableSet alloc] init];
gCustomSubclassMap = [[NSMutableDictionary alloc] init];
}
}
static void WhileLocked(void (^block)(void))
{
pthread_mutex_lock(&gMutex;);
block();
pthread_mutex_unlock(&gMutex;);
}
static void AddWeakRefToObject(id obj, MAZeroingWeakRef *ref)
{
CFMutableSetRef set = (void *)CFDictionaryGetValue(gObjectWeakRefsMap, obj);
if(!set)
{
set = CFSetCreateMutable(NULL, 0, NULL);
CFDictionarySetValue(gObjectWeakRefsMap, obj, set);
CFRelease(set);
}
CFSetAddValue(set, ref);
}
static void RemoveWeakRefFromObject(id obj, MAZeroingWeakRef *ref)
{
CFMutableSetRef set = (void *)CFDictionaryGetValue(gObjectWeakRefsMap, obj);
CFSetRemoveValue(set, ref);
}
static void ClearWeakRefsForObject(id obj)
{
CFMutableSetRef set = (void *)CFDictionaryGetValue(gObjectWeakRefsMap, obj);
[(NSSet *)set makeObjectsPerformSelector: @selector(_zeroTarget)];
CFDictionaryRemoveValue(gObjectWeakRefsMap, obj);
}

First, the convenience constructor and initializer. Mostly straightforward:

+ (id)refWithTarget: (id)target
{
return [[[self alloc] initWithTarget: target] autorelease];
}
- (id)initWithTarget: (id)target
{
if((self = [self init]))
{
_target = target;
RegisterRef(self, target);
}
return self;
}

The dealloc implementation similarly calls a utility function to remove the weak reference object:

- (void)dealloc
{
UnregisterRef(self);
[_cleanupBlock release];
[super dealloc];
}
- (NSString *)description
{
return [NSString stringWithFormat: @"<%@: %p -> %@>", [self class], self, [self target]];
}
- (void)setCleanupBlock: (void (^)(id target))block
{
block = [block copy];
[_cleanupBlock release];
_cleanupBlock = block;
}
- (id)target
{
__block id ret;
WhileLocked(^{
ret = [_target retain];
});
return [ret autorelease];
}
- (void)_zeroTarget
{
if(_cleanupBlock)
{
_cleanupBlock(_target);
[_cleanupBlock release];
_cleanupBlock = nil;
}
_target = nil;
}

Implementation of Utility Functions The implementation of UnregisterRef is simple. Get the target out of the MAZeroingWeakRef, get the table of references to the target, and remove the given reference. Wrap it all in a lock to ensure that the target can’t be deallocated in the middle of this operation:

static void UnregisterRef(MAZeroingWeakRef *ref)
{
WhileLocked(^{
id target = ref->_target;
if(target)
RemoveWeakRefFromObject(target, ref);
});
}
static void RegisterRef(MAZeroingWeakRef *ref, id target)
{
WhileLocked(^{
EnsureCustomSubclass(target);
AddWeakRefToObject(target, ref);
});
}
static void EnsureCustomSubclass(id obj)
{
if(!GetCustomSubclass(obj))
{
Class class = object_getClass(obj);
Class subclass = [gCustomSubclassMap objectForKey: class];
if(!subclass)
{
subclass = CreateCustomSubclass(class, obj);
[gCustomSubclassMap setObject: subclass forKey: class];
[gCustomSubclasses addObject: subclass];
}
object_setClass(obj, subclass);
}
}
static Class GetCustomSubclass(id obj)
{
Class class = object_getClass(obj);
while(class && ![gCustomSubclasses containsObject: class])
class = class_getSuperclass(class);
return class;
}
static Class CreateCustomSubclass(Class class, id obj)
{
if(IsTollFreeBridged(class, obj))
{
NSCAssert(0, @"Cannot create zeroing weak reference to object of type %@ with COREFOUNDATION_HACK_LEVEL set to %d", class, COREFOUNDATION_HACK_LEVEL);
return class;
}
else
{

The implementation of IsTollFreeBridged simply checks to see if the class name starts with NSCF:

static BOOL IsTollFreeBridged(Class class, id obj)
{
return [NSStringFromClass(class) hasPrefix: @"NSCF"];
}
NSString *newName = [NSString stringWithFormat: @"%s_MAZeroingWeakRefSubclass", class_getName(class)];
const char *newNameC = [newName UTF8String];
Class subclass = objc_allocateClassPair(class, newNameC, 0);
Method release = class_getInstanceMethod(class, @selector(release));
Method dealloc = class_getInstanceMethod(class, @selector(dealloc));
class_addMethod(subclass, @selector(release), (IMP)CustomSubclassRelease, method_getTypeEncoding(release));
class_addMethod(subclass, @selector(dealloc), (IMP)CustomSubclassDealloc, method_getTypeEncoding(dealloc));
objc_registerClassPair(subclass);
return subclass;
}
}

The trouble is that simply writing [super release] won’t work, because the compiler only allows that in a true, compile-time method implementation. In order to perform the equivalent action, it’s necessary to figure out the superclass of the custom weak reference subclass. This is done using a simple helper function which calls GetCustomSubclass and returns the superclass of that class:

static Class GetRealSuperclass(id obj)
{
Class class = GetCustomSubclass(obj);
NSCAssert(class, @"Coudn't find ZeroingWeakRef subclass in hierarchy starting from %@, should never happen", object_getClass(obj));
return class_getSuperclass(class);
}
static void CustomSubclassRelease(id self, SEL _cmd)
{
Class superclass = GetRealSuperclass(self);
IMP superRelease = class_getMethodImplementation(superclass, @selector(release));
WhileLocked(^{
((void (*)(id, SEL))superRelease)(self, _cmd);
});
}
static void CustomSubclassDealloc(id self, SEL _cmd)
{
ClearWeakRefsForObject(self);
Class superclass = GetRealSuperclass(self);
IMP superDealloc = class_getMethodImplementation(superclass, @selector(dealloc));
((void (*)(id, SEL))superDealloc)(self, _cmd);
}

Examples: Basic usage of MAZeroingWeakRef is simple:

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSObject *obj = [[NSObject alloc] init];
MAZeroingWeakRef *ref = [[MAZeroingWeakRef alloc] initWithTarget: obj];
NSLog(@"%@", [ref target]);
[obj release];
[pool release];
NSLog(@"%@", [ref target]);

Using a cleanup block is similarly simple:

NSObject *obj = [[NSObject alloc] init];
MAZeroingWeakRef *ref = [[MAZeroingWeakRef alloc] initWithTarget: obj];
[ref setCleanupBlock: ^(id target) { NSLog(@"Cleaned object %p!", target); }];
[obj release];

A simple way to turn a regular instance variable into a zeroing weak reference is to use MAZeroingWeakRef in your getter and setter, and then make sure to always use your getter in other code:

// ivar
MAZeroingWeakRef *_somethingWeakRef;
// accessors
- (void)setSomething: (Something *)newSomething
{
[_somethingWeakRef release];
_somethingWeakRef = [[MAZeroingWeakRef alloc] initWithTarget: newSomething];
}
- (Something *)something
{
return [_somethingWeakRef target];
}
// use
- (void)doThing
{
[[self something] doThingWithObject: self];
}

For a more advanced use, here’s an addition to NSNotificationCenter that eliminates the need to manually remove an observer in dealloc:

@implementation NSNotificationCenter (MAZeroingWeakRefAdditions)
- (void)addWeakObserver: (id)observer selector: (SEL)selector name: (NSString *)name object: (NSString *)object
{
[self addObserver: observer selector: selector name: name object: object];
MAZeroingWeakRef *ref = [[MAZeroingWeakRef alloc] initWithTarget: observer];
[ref setCleanupBlock: ^(id target) {
[self removeObserver: target name: name object: object];
[ref autorelease];
}];
}
@end

Similarly, if you’re tired of mysterious crashes caused by NSTableView data sources being deallocated before the views themselves, you can easily fix it:

@implementation NSTableView (MAZeroingWeakRefAdditions)
- (void)setWeakDataSource: (id <NSTableViewDataSource>)source
{
[self setDataSource: source];
MAZeroingWeakRef *ref = [[MAZeroingWeakRef alloc] initWithTarget: observer];
[ref setCleanupBlock: ^(id target) {
if([self dataSource] == target) // double check for safety
[self setDataSource: nil];
[ref autorelease];
}];
}
@end

Essentially, any time you have a weak reference (an object reference that you don’t retain or copy), you should use a MAZeroingWeakRef instead of a raw unretained pointer. It will save you trouble and pain and is extremely easy to use.

ZeroingCollections The repository includes MAWeakArray and MAWeakDictionary, subclasses of NSMutableArray and NSMutableDictionary which use zeroing weak references to their contents. MAWeakDictionary uses strong keys to weak objects, which would be useful for many caching scenarios. I won’t go through their code here, but they’re simple, and you can look at the code in the repository if you’re curious.

Although I didn’t write them, it would be possible to creat a weak version of NSMutableSet and NSMutableDictionary which uses weak keys instead of, or in addition to, weak objects. These would be trickier due to hashing/equality issues with the weak references, but could certainly be done.

Conclusion Zeroing weak references are an extremely useful construct present in many languages. Even Objective-C has them when running under garbage collection, but without GC, Objective-C code has been stuck using non-zeroing weak references, which are tricky and dangerous.

MAZeroingWeakRef brings zeroing weak references to manual memory managed Objective-C. Although it uses some trickery on the inside, the API is extremely simple to use. By automatically zeroing weak references, you avoid many potential crashers and data corruption. Zeroing weak references can also be used for things like object caches where non-zeroing weak references aren’t very practical at all.

The code is made available under a BSD license.

For the next Friday Q&A in two weeks, I will discuss how MAZeroingWeakRef works around the problems with CoreFoundation objects. Until then, enjoy!