PLWeakCompatibility 源码导览(上)

Mike Ash Friday Q&A 中文译文:PLWeakCompatibility 源码导览(上)

作者 TommyWu
封面圖片: PLWeakCompatibility 源码导览(上)

译文 · 原文: Friday Q&A 2012-05-18: A Tour of PLWeakCompatibility: Part I · 作者 Mike Ash

原文:https://www.mikeash.com/pyblog/friday-qa-2012-05-18-a-tour-of-plweakcompatibility-part-i.html 发布:2012-05-18 作者:Mike Ash 译者:MiMo(mimo-v2.5-pro);代码块保留英文原样


几周前,我介绍了 PLWeakCompatibility。这是一个小型库,可以集成到应用程序中,以便在不支持 __weak 限定符的操作系统上使用它。ARC(自动引用计数)在 Mac OS X 10.6 和 iOS 4 上得到官方支持,但 __weak 只在 10.7 和 iOS 5 及更高版本上可用。PLWeakCompatibility 为这些旧操作系统在使用 ARC 时添加了对 __weak 的支持。今天,我将讨论 PLWeakCompatibility 内部是如何工作的。

动机

ARC 是一项非常棒的技术。虽然它不如真正的垃圾回收器那么好,但比手动内存管理要好用得多。然而,如果没有 __weak 关键字,使用 ARC 会很烦人。替代方案是 __unsafe_unretained,它会给你一个不保留的引用(unretained reference),该引用在目标对象被释放时不会自动置零。如果你在目标对象销毁后访问这样一个变量,程序将会崩溃。相比之下,一个 __weak 变量在对象释放时会自动变为 nil,这使得访问过期指针(stale pointer)变得不可能(或至少困难得多)。

在 Plausible Labs,我们希望将一个大型项目迁移到 ARC(自动引用计数),但需要保持对 iOS 4 的兼容性。一种可行方案是使用类似 MAZeroingWeakRef 的显式代理对象来管理归零弱引用(zeroing weak reference)。另一种方案是使用 __unsafe_unretained,其实它并不比手动内存管理中的传统非保留引用更不安全。

如果可能,我们实在不想要任何折中方案。最终我们决定尝试能否通过一些技巧让编译器在旧系统上接受 __weak,并为其提供必要的运行时支持。经过一些研究,我们发现这不仅可行,而且相当直接,于是 PLWeakCompatibility 项目就此诞生。

编译器层面

从编译器的角度来看,__weak 实际上相当简单。所有复杂的部分都委托给了运行时,因此编译器只需在正确的时间发出正确的运行时调用即可。这些运行时函数在 clang 的 ARC 文档中列出,它们是:

void objc_copyWeak(id *dest, id *src);

此函数用于将一个弱指针从一个位置复制到另一个位置,前提是目标位置尚不包含弱指针。它可应用于类似以下代码的场景:

__weak id weakPtr1 = ...;
__weak id weakPtr2 = weakPtr1;

下一个函数是:

void objc_destroyWeak(id *object);

这会注销一个 __weak 指针。当一个局部 __weak 变量离开作用域时,或者在拥有 __weak 实例变量的类的 dealloc 实现中,都会使用到它。

id objc_initWeak(id *object, id value);

这个函数用于初始化一个 __weak 变量。它会被用于类似如下的代码:

id strongPtr = ...;
__weak id weakPtr = strongPtr;

接下来,我们来看:

id objc_loadWeak(id *object);

这个函数从弱指针(weak pointer)中加载出其值并返回,返回前会先对该值进行保留(retain)和自动释放(autorelease),以确保其存活时间足够让调用方使用。在任何需要在表达式中使用 __weak 变量的地方,都可以使用此函数。

id objc_loadWeakRetained(id *object);

这和前面的函数类似,只是它省略了自动释放(autorelease)操作。这可以让编译器生成更高效的代码。

void objc_moveWeak(id *dest, id *src);

这将弱指针从一个位置复制到另一个位置。它与 objc_copyWeak 非常相似,但可选择是否清空源位置。最后,我们有:

id objc_storeWeak(id *object, id value);

此函数用于将新值存入一个 __weak 变量。当 __weak 变量是赋值操作的目标时,就会用到它。

细心的读者会注意到,这里列出的函数数量远远超出实际需要。实际上,严格意义上只有两个函数是必需的:objc_loadWeakRetainedobjc_storeWeak。所有其他函数都可以基于这两个函数来实现。例如,objc_destroyWeak 可以简单地实现为 objc_storeWeak(location, nil);objc_initWeak 则是 *location = nil; objc_storeWeak(location, value);。事实上,Objective-C 运行时(Runtime)就是这样实现它们的。那么,为什么还需要所有这些额外的函数呢?

看来这主要是为了为优化留出余地。虽然所有其他函数都可以基于这两个原语来实现,但根据运行时的具体实现,可能存在更快的方法,例如,初始化一个已知此前未被使用过的 __weak 变量。尽管目前的运行时并未利用这一点,但通过让编译器生成更专门的调用,就为未来进行此类优化提供了可能性。

由于 PLWeakCompatibility 并不特别关注在旧平台上的执行速度,且在新平台上直接调用苹果的实现,因此我们仅用这两个基础操作(primitives)就实现了所有其他函数调用。

欺骗编译器
运行时函数(runtime functions)的生成机制与普通函数调用相同。这意味着,如果你的应用中存在名为 objc_storeWeak 的函数,编译器会欣然生成调用该函数的代码。它并不显式依赖于 Objective-C 运行时库。然而,默认情况下,当部署目标(deployment target)是未正式支持 __weak 的操作系统时,clang 会拒绝编译任何包含 __weak 的代码。幸运的是,通过添加一对编译器标志(compiler flags),可以告知 clang 当前目标确实支持 __weak

-Xclang -fobjc-runtime-has-weak

第二个标志告诉 clang 运行时确实支持 __weak,即使部署目标表明不支持。第一个标志是个小技巧,用于绕过不了解该标志并会忽略它的顶层编译器驱动(top-level compiler driver),从而传递第二个标志。

有了这两个标志,clang 就接受 __weak 并生成相应的函数调用。剩下要做的就是提供这些函数的我们自己的实现。

避免 ARC
运行时函数的官方原型都使用 id 类型。但这给 PLWeakCompatibility 带来了一个问题。目标是制作一个单一文件,可以放入 ARC 项目中,无需太多设置就能启用 __weak。这意味着这些函数将使用 ARC 编译,而在其中使用 id 会导致 ARC 生成各种不必要的 retain 和 release 调用。

我最终使用 void * 代替 id,并隐藏在一个方便的 typedef 背后。

typedef void *PLObjectPtr;

有了这个基础,运行时函数的原型看起来是这样的:

PLObjectPtr objc_loadWeakRetained(PLObjectPtr *location);
PLObjectPtr objc_initWeak(PLObjectPtr *addr, PLObjectPtr val);
void objc_destroyWeak(PLObjectPtr *addr);
void objc_copyWeak(PLObjectPtr *to, PLObjectPtr *from);
void objc_moveWeak(PLObjectPtr *to, PLObjectPtr *from);
PLObjectPtr objc_loadWeak(PLObjectPtr *location);
PLObjectPtr objc_storeWeak(PLObjectPtr *location, PLObjectPtr obj);

尽管这些函数原型已不再与官方原型匹配,但它们仍是二进制兼容的,而这才是关键所在。编译器在生成运行时调用时并不会参考这些原型,并且 id 类型可以毫无问题地被视为 void *

穿透处理
当存在原生 __weak 支持时,我们不应抢占其功能。这意味着我们所有函数首先需要检查是否已有原生支持可用,若有则直接调用原生实现。例如,objc_loadWeak 的实现大致如下:

PLObjectPtr objc_loadWeakRetained(PLObjectPtr *location) {
PLObjectPtr (*fptr)(PLObjectPtr *) = dlsym(RTLD_NEXT, "objc_loadWeakRetained");
if(fptr != NULL)
return fptr(location);
return PLLoadWeakRetained(location);
}

如果你不熟悉,dlsym 是一个能在运行时查找符号(symbol)的函数,而 RTLD_NEXT 是一个特殊参数,用于指示它去寻找某个特定符号的 “下一个” 实现(implementation)。换言之,如果调用方不在这个应用程序中,它会找到什么符号?这本质上就是告诉它去查找该函数的原始运行时实现(如果存在的话)。

我们不希望每次调用这个函数都调用 dlsym,因为那样会相当慢。通过使用 dispatch_once 只执行一次检查,我们可以很好地加快速度。此外,在声明函数指针时需要再写一遍函数类型有点烦人,这个问题很容易通过使用 __typeof__ 来解决。经过这些修改,代码看起来是这样的:

PLObjectPtr objc_loadWeakRetained(PLObjectPtr *location) {
static dispatch_once_t fptrOnce
static __typeof__(&objc_loadWeakRetained) fptr;
dispatch_once(&fptrOnce, ^{ fptr = dlsym(RTLD_NEXT, "objc_loadWeakRetained"); });
if(fptr != NULL)
return fptr(location);
return PLLoadWeakRetained(location);
}

现在这已经足够通用,可以将其放入一个宏中以避免重复。这个宏接受函数名和参数,如果可用的话会自动调用原始实现:

#define NEXT(name, ...) do { \
static dispatch_once_t fptrOnce; \
static __typeof__(&name) fptr; \
dispatch_once(&fptrOnce, ^{ fptr = dlsym(RTLD_NEXT, #name); });\
if (fallthroughEnabled && fptr != NULL) \
return fptr(__VA_ARGS__); \
} while(0)

注意这个额外的 fallthroughEnabled 标志,它的存在纯粹是为了测试。它允许禁用贯穿(fallthrough)行为,以便单元测试能覆盖两种执行情况。

有了这个宏之后,我们就可以快速实现所有的非原始函数(non-primitive functions)了:

PLObjectPtr objc_initWeak(PLObjectPtr *addr, PLObjectPtr val) {
NEXT(objc_initWeak, addr, val);
*addr = NULL;
return objc_storeWeak(addr, val);
}
void objc_destroyWeak(PLObjectPtr *addr) {
NEXT(objc_destroyWeak, addr);
objc_storeWeak(addr, NULL);
}
void objc_copyWeak(PLObjectPtr *to, PLObjectPtr *from) {
NEXT(objc_copyWeak, to, from);
objc_initWeak(to, objc_loadWeak(from));
}
void objc_moveWeak(PLObjectPtr *to, PLObjectPtr *from) {
NEXT(objc_moveWeak, to, from);
objc_copyWeak(to, from);
objc_destroyWeak(from);
}
PLObjectPtr objc_loadWeak(PLObjectPtr *location) {
NEXT(objc_loadWeak, location);
return objc_autorelease(objc_loadWeakRetained(location));
}

原生函数 objc_loadWeakRetained 仅是直接调用了另一个内部函数,这样设计的目的是为了在代码中更好地分离职责:

PLObjectPtr objc_loadWeakRetained(PLObjectPtr *location) {
NEXT(objc_loadWeakRetained, location);
return PLLoadWeakRetained(location);
}

objc_storeWeak 的实现稍微复杂一些。首先,与其他函数一样,它会先调用至运行时实现(如果有的话):

PLObjectPtr objc_storeWeak(PLObjectPtr *location, PLObjectPtr obj) {
NEXT(objc_storeWeak, location, obj);

在此之后,它会调用一个内部函数,以注销当前位于该位置的弱引用(weak reference)。

PLUnregisterWeak(location, obj);

接下来,它将新值存入该存储位置,并且若新值不为 nil,则注册该存储位置:

if (obj != nil)
PLRegisterWeak(location, obj);

最后,它简单地返回了存储的对象:

return obj;
}

因此我们将这个功能分解为三个内部原语函数。PLLoadWeakRetained 用于加载一个弱引用(weak reference)并返回其带引用计数的指针;PLRegisterWeak 为特定对象注册新的弱引用存储位置,并确保当该对象被销毁时该位置会被置零;PLUnregisterWeak 从对象的弱引用列表中移除该存储位置,以避免对象销毁时被触及。实现这三个函数后,PLWeakCompatibility 将完整可用。

实现方案

在 Cocoa 中实现零化弱引用(zeroing weak reference)系统存在两个主要挑战:其一是准确获知对象何时被销毁,并在销毁时将其所有引用置零。

第二个挑战在于加载弱引用时避免竞态条件(race conditions)。在 Cocoa 中,从最后一个 release 消息发送给即将销毁的对象到该对象的 dealloc 方法被调用之间存在时间间隔。在此间隔期间加载该对象的弱引用必须返回 nil,因为此时对象的销毁已不可避免 —— 即使进行保留操作也无法使其存活。(译注:此描述基于旧版运行时机制,现代系统的实现细节可能已变化)

这两个挑战均通过 MAZeroingWeakRef 得以解决,该库采用动态子类化(dynamic subclassing)与 isa 混写(isa-swizzling)技术。当 MAZeroingWeakRef 存在时,PLWeakCompatibility 会调用其实现。然而我们也希望有一套能直接集成于代码主体、完全独立运行的简化方案,因此 PLWeakCompatibility 需要自建解决体系。

PLWeakCompatibility 通过混写(swizzling)目标对象的 release 和 dealloc 方法来应对此类挑战:混写 dealloc 可侦测对象销毁时机;被混写的 release 方法会将对象加入当前正在释放的对象列表。任何尝试解析列表中对象弱引用(weak reference)的操作都将阻塞,直至释放完成 —— 此时对象若存活则可获取弱引用,若已消亡则弱引用归零。具体运作机制需待第二部分详解!

总结

PLWeakCompatibility 对在旧操作系统上使用 ARC 是一个强大的助力。通过向编译器传递几个标志,我们能够欺骗它发出对运行时函数(runtime functions)的调用,从而启用 __weak,即使该运行时并不原生支持这些函数。接着,通过我们自己提供具有相同语义的这些函数的实现,我们在那些不原生支持 __weak 的操作系统上实现了完整的 __weak 兼容性。

最后,我们将多个运行时函数分解为三个原始函数:一个用于加载弱引用(weak reference),一个用于注册,一个用于注销。下次,我将详细讨论这三个函数的实现以及它们是如何工作的。


#Original (English)

Source: https://www.mikeash.com/pyblog/friday-qa-2012-05-18-a-tour-of-plweakcompatibility-part-i.html

A few weeks ago, I introduced PLWeakCompatibility. This is a small library that can be dropped into an app to enable use of the __weak qualifier on OSes that don’t support it. ARC is officially supported on Mac OS X 10.6 and iOS 4, but __weak is only available on 10.7 and iOS 5. PLWeakCompatibility adds support for __weak on those older OSes when using ARC. Today I’m going to discuss how PLWeakCompatibility works on the inside.

MotivationARC is really nice technology to use. It’s not quite as nice as a real garbage collector, but it’s far nicer to use than manual memory management. However, it’s really annoying to use ARC without the __weak keyword. The alternative is __unsafe_unretained, which gives you a unretained reference which doesn’t zero out when the target object is deallocated. If you access such a variable after the target has been destroyed, you’ll crash. In contrast, a __weak variable automatically becomes nil when deallocated, making it impossible (or at least much, much harder) to access a stale pointer.

Over at Plausible Labs, we wanted to move a big project over to ARC but needed to maintain compatibility with iOS 4. One possibility for this was to use something like MAZeroingWeakRef, an explicit proxy object that manages the zeroing weak reference. Another was to use __unsafe_unretained, which is really no more unsafe than old-style unretained references with manual memory management.

We really didn’t want any half measures if we could avoid them. Finally we decided to see whether it was possible to trick the compiler into accepting __weak on older systems, and providing the necessary runtime code for it to actually work. After some investigation, it turned out that this was not only feasible but actually pretty straightforward, and thus PLWeakCompatibility was born.

The Compiler SideFrom the compiler’s point of view, __weak is actually fairly simple. All of the interesting bits are delegated to the runtime, so the compiler just has to emit the right runtime calls at the right time. These runtime functions are listed in the clang ARC documentation, and they are:

void objc_copyWeak(id *dest, id *src);

This function copies a weak pointer from one location to another, when the destination doesn’t already contain a weak pointer. It would be used for code like:

__weak id weakPtr1 = ...;
__weak id weakPtr2 = weakPtr1;

The next function is:

void objc_destroyWeak(id *object);

This unregisters a __weak pointer. This would be used when a local __weak variable goes out of scope, or in the dealloc implementation of a class with __weak instance variables.

id objc_initWeak(id *object, id value);

This function initializes a __weak variable. It would be used for code like:

id strongPtr = ...;
__weak id weakPtr = strongPtr;

Next, we have:

id objc_loadWeak(id *object);

This loads the value out of a weak pointer and returns it, after retaining and autoreleasing the value to ensure that it stays alive long enough for the caller to use it. This function would be used anywhere a __weak variable is used in an expression.

id objc_loadWeakRetained(id *object);

This is just like the previous function, except that it omits the autorelease. This can allow the compiler to emit more efficient code.

void objc_moveWeak(id *dest, id *src);

This copies the weak pointer from one location to another. It’s much like objc_copyWeak, except that it may optionally clear out the source location. Finally, we have:

id objc_storeWeak(id *object, id value);

This function stores a new value into a __weak variable. It would be used anywhere a __weak variable is the target of an assignment.

The astute reader will notice that there are far more functions here than there need to be. In fact, only two of these functions are strictly necessary: objc_loadWeakRetained and objc_storeWeak. All of the others can be implemented in terms of those two. For example, objc_destroyWeak can be implemented as simply objc_storeWeak(location, nil);. objc_initWeak is just *location = nil; objc_storeWeak(location, value);. And in fact the Objective-C runtime implements them like this. Why all the extra functions, then?

It appears to simply be to leave the door open for optimization. While all of these other functions can be implemented in terms of the two primitives, depending on the runtime implementation there may be faster ways to e.g. initialize a __weak variable that’s known not to have been previously used. Although the runtime isn’t taking advantage of this currently, by having the compiler generate more specialized calls, it allows for the possibility in the future.

Since PLWeakCompatibility isn’t particularly concerned about speed on older platforms, and simply calls through to Apple’s implementations on newer platforms, we simply implemented all of the other calls in terms of the two primitives.

Fooling the CompilerThe runtime functions are emitted just like any other function call. That means that if you have a function called objc_storeWeak somewhere in your app, the compiler will happily generate code that calls it. It’s not explicitly tied to the Objective-C runtime library. However, by default, clang refuses to compile any code with __weak in it when the deployment target is an OS that doesn’t officially support it. Fortunately, it’s possible to tell clang that the current target really does support __weak by adding a pair of compiler flags:

-Xclang -fobjc-runtime-has-weak

The second flag tells clang that the runtime really does support __weak, even when the deployment target indicates otherwise. The first flag is a little hack to sneak the second flag past the top-level compiler driver, which doesn’t know about that flag and will ignore it.

With those two flags in place, clang accepts __weak and emits the appropriate function calls. All that remains is to provide our own implementation of those functions.

Avoiding ARCThe official prototypes for the runtime functions all use id. However, this presents a problem for PLWeakCompatibility. The goal was to produce a single file which could be dropped into an ARC project to enable __weak without much setup. That means that these functions would be compiled using ARC, and using id in them would cause ARC to emit all sorts of unwanted retain and release calls.

I settled on using void * instead of id, hidden behind a convenient typedef:

typedef void *PLObjectPtr;

With that in place, the prototypes for the runtime functions look like this:

PLObjectPtr objc_loadWeakRetained(PLObjectPtr *location);
PLObjectPtr objc_initWeak(PLObjectPtr *addr, PLObjectPtr val);
void objc_destroyWeak(PLObjectPtr *addr);
void objc_copyWeak(PLObjectPtr *to, PLObjectPtr *from);
void objc_moveWeak(PLObjectPtr *to, PLObjectPtr *from);
PLObjectPtr objc_loadWeak(PLObjectPtr *location);
PLObjectPtr objc_storeWeak(PLObjectPtr *location, PLObjectPtr obj);

Although these prototypes no longer match the official ones, they are still binary compatible, which is all that matters. The compiler isn’t looking at these prototypes when it emits the runtime calls, and an id can be treated as a void * without any trouble.

Falling ThroughWhen native __weak support is available, we don’t want to preempt it. That means that the first thing all of our functions need to do is check to see whether native support is available, and call through to it instead. For example, the implementation of objc_loadWeak would look something like this:

PLObjectPtr objc_loadWeakRetained(PLObjectPtr *location) {
PLObjectPtr (*fptr)(PLObjectPtr *) = dlsym(RTLD_NEXT, "objc_loadWeakRetained");
if(fptr != NULL)
return fptr(location);
return PLLoadWeakRetained(location);
}

If you’re unfamiliar, dlsym is a function that can look up symbols at runtime, and RTLD_NEXT is a special parameter which tells it to look for the “next” implementation of a particular symbol. In other words, if the caller wasn’t present in the app, what symbol would it find then? This essentially tells it to go off and find the original runtime implementation of this function if it exists.

We don’t want to call dlsym for every single call to this function, since that would be fairly slow. We can speed it up nicely by using dispatch_once to only perform the check once. Furthermore, it’s a little annoying to have to write out the type of the function a second time when declaring the function pointer, and this is easily solved by using typeof. With those modifications, the code looks like this:

PLObjectPtr objc_loadWeakRetained(PLObjectPtr *location) {
static dispatch_once_t fptrOnce
static __typeof__(&objc_loadWeakRetained) fptr;
dispatch_once(&fptrOnce, ^{ fptr = dlsym(RTLD_NEXT, "objc_loadWeakRetained"); });
if(fptr != NULL)
return fptr(location);
return PLLoadWeakRetained(location);
}

This is now sufficiently generic to put in a macro to avoid repetition. This macro takes the name of the function and the arguments, and automatically calls through to the original implementation if available:

#define NEXT(name, ...) do { \
static dispatch_once_t fptrOnce; \
static __typeof__(&name) fptr; \
dispatch_once(&fptrOnce, ^{ fptr = dlsym(RTLD_NEXT, #name); });\
if (fallthroughEnabled && fptr != NULL) \
return fptr(__VA_ARGS__); \
} while(0)

Note the extra fallthroughEnabled flag, which is there simply for testing. It allows disabling the fallthrough so that unit tests can exercise both cases.

With this macro in place, we can then write quick implementations of all the non-primitive functions:

PLObjectPtr objc_initWeak(PLObjectPtr *addr, PLObjectPtr val) {
NEXT(objc_initWeak, addr, val);
*addr = NULL;
return objc_storeWeak(addr, val);
}
void objc_destroyWeak(PLObjectPtr *addr) {
NEXT(objc_destroyWeak, addr);
objc_storeWeak(addr, NULL);
}
void objc_copyWeak(PLObjectPtr *to, PLObjectPtr *from) {
NEXT(objc_copyWeak, to, from);
objc_initWeak(to, objc_loadWeak(from));
}
void objc_moveWeak(PLObjectPtr *to, PLObjectPtr *from) {
NEXT(objc_moveWeak, to, from);
objc_copyWeak(to, from);
objc_destroyWeak(from);
}
PLObjectPtr objc_loadWeak(PLObjectPtr *location) {
NEXT(objc_loadWeak, location);
return objc_autorelease(objc_loadWeakRetained(location));
}

The primitive function objc_loadWeakRetained simply calls through to another internal function, which exists simply to better separate things in the code:

PLObjectPtr objc_loadWeakRetained(PLObjectPtr *location) {
NEXT(objc_loadWeakRetained, location);
return PLLoadWeakRetained(location);
}

The implementation of objc_storeWeak is slightly more complex. First it calls through to the runtime implementation, if any, just like with the other functions:

PLObjectPtr objc_storeWeak(PLObjectPtr *location, PLObjectPtr obj) {
NEXT(objc_storeWeak, location, obj);

After this, it calls an internal function to unregister the weak reference currently at location:

PLUnregisterWeak(location, obj);

Next, it stores the new value into location and, if the new value isn’t nil, registers this location:

if (obj != nil)
PLRegisterWeak(location, obj);

Finally it simply returns the object that was stored:

return obj;
}

We’ve therefore decomposed this functionality into three internal primitive functions. PLLoadWeakRetained loads a weak reference and returns a retained pointer to it. PLRegisterWeak registers a new weak reference location for a particular object, and ensures that the location is zeroed out when the object is destroyed. PLUnregisterWeak removes the location from the object’s list of weak references so that it will no longer be touched when the object is destroyed. With these three functions implemented, PLWeakCompatibility will be complete.

The PlanThere are two main challenges for a zeroing weak reference system in Cocoa. One is finding out exactly when an object is being destroyed, and zeroing out all references to it when that happens.

The second challenge is avoiding race conditions when loading a weak reference. In Cocoa, there is an interval between the last release message being sent to a now-dead object and that object’s dealloc method being invoked. Loading a weak reference to that object in that interval must return nil, because the destruction of the object is at that point unavoidable. Retaining it at that point won’t keep it alive.

Both of these challenges are solved by MAZeroingWeakRef, which uses dynamic subclassing and isa-swizzling to solve them. PLWeakCompatibility will call through to MAZeroingWeakRef when it’s present. However, we also wanted a simpler implementation that we could include directly with the rest of the code, so that it could all be used completely standalone. Thus PLWeakCompatibility needs its own solutions as well.

PLWeakCompatibility addresses these challenges by swizzling out the release and dealloc methods of the target object. Swizzling dealloc solves the challenge of finding out when an object is destroyed. The swizzled release method adds the object to a list of objects that are currently being released. Any attempt to resolve a weak reference to an object on the list blocks until the release is complete, at which point the object is either alive, and a weak reference can be obtained, or dead, and the weak reference is zero. However, the details of how this all works will have to wait for part II!

ConclusionPLWeakCompatibility is a great aid to using ARC on older OSes. By passing a couple of flags to the compiler, we’re able to trick it into emitting calls to the runtime functions that enable __weak even though the runtime doesn’t support them. Then, by providing our own implementation of those functions with the same semantics, we enable full __weak compatibility on OSes that don’t support them natively.

Finally, we decomposed the multiple runtime functions into three primitive functions: one for loading a week reference, one for registering, and one for unregistering. Next time around, I’ll discuss in detail the implementation of those three functions and how they work.