Objective-C 内部机制:未实现类与 Toll-Free Bridging

Always Processing 中文译文:Objective-C 内部机制:未实现类与 Toll-Free Bridging(原文:Objective-C Internals: Unrealized Classes (and Toll-Free Bridging))

作者 TommyWu
封面圖片: Objective-C 内部机制:未实现类与 Toll-Free Bridging

译文 · 原文: Objective-C Internals: Unrealized Classes (and Toll-Free Bridging) · 作者 Brian T. Kelley

原文:https://alwaysprocessing.blog/2023/02/16/objc-unrealized-classes 发布:2023-02-16 作者:Brian T. Kelley 译者:MiMo;代码块保留英文原样 | 仅供个人学习与交流


Objective-C 中的 “未实现的类” 可能指代未来类或类存根。未来类(一种私有运行时特性)用于支持与 CoreFoundation 的无缝桥接(Toll-Free Bridging)。而类存根则由 Swift 编译器生成,以支持稳定的 Swift ABI 与 Objective-C 之间的互操作性。

一篇早期探讨 Objective-C 类实现的文章忽略了 [1] 用于识别元类(Metaclass)的函数中一个有趣的细节:未实现的类这一概念。

// Like isMetaClass, but also valid on un-realized classes
bool isMetaClassMaybeUnrealized() { /* ... */ }

一个未实现类(unrealized class)是部分初始化的元类(metaclass),其中仅类名已知。未实现的元类有两种类型:未来类(future classes)和类存根(class stubs)。

未来类

Mac OS X 10.5 中的 Objective-C 运行时引入了未来类(future classes)作为一种私有运行时功能,用于清理无缝桥接(toll-free bridging)的实现。令人意外的是,这实际上在 objc_getFutureClass 函数的文档中有记载:

供 CoreFoundation 的无缝桥接使用。请勿自行调用此函数。

无缝桥接

首先,我们简要了解一下无缝桥接的实现。Mac OS X Developer Preview 1 引入了 CoreFoundation 框架和 “无缝桥接”(toll-free bridging)。简而言之,无缝桥接允许开发人员在已桥接的 CoreFoundation 类型和 Foundation 类型之间自由进行强制类型转换(例如,将 CFArrayRef 转换为 / 自 NSArray *),从而像使用另一种类型一样传递一种类型,而不会产生任何运行时开销。此外,无缝桥接的 Foundation 类的用户自定义子类也得到了完全支持。

对无缝桥接(toll-free bridging)的全面探讨超出了本文范围,但您可在此处阅读更多内容。我们只需强调两个关键实现细节,以便理解其对 Objective-C 运行时(Objective-C runtime)的运用。

首先,所有 CoreFoundation 对象均以其首成员包含一个兼容 Objective-C 的 isa 字段(isa field)。来自 CFRuntime.h:

typedef struct __CFRuntimeBase {
void *_isa;
// ...
} CFRuntimeBase;

其次,每个具有 Foundation 等价物的 CoreFoundation 函数(例如 CFArrayGetCount()-[NSArray count]),如果其 isa 与桥接的 CoreFoundation 类型的 isa 不匹配,就会调用 Objective-C 实现。这为用户定义的 Foundation 子类提供了桥接支持。(CFInternal.h 中定义了 Objective-C 分派宏。)

CFIndex CFArrayGetCount(CFArrayRef array) {
CF_OBJC_FUNCDISPATCH0(__kCFArrayTypeID, CFIndex, array, "count");
__CFGenericValidateType(array, __kCFArrayTypeID);
return __CFArrayGetCount(array);
}
#define CF_OBJC_FUNCDISPATCH0(typeID, rettype, obj, sel) \
if (__builtin_expect(CF_IS_OBJC(typeID, obj), 0)) \
{rettype (*func)(const void *, SEL) = (void *)__CFSendObjCMsg; \
static SEL s = NULL; if (!s) s = sel_registerName(sel); \
return func((const void *)obj, s);}
CF_INLINE int CF_IS_OBJC(CFTypeID typeID, const void *obj) {
return (((CFRuntimeBase *)obj)->_isa != __CFISAForTypeID(typeID) && ((CFRuntimeBase *)obj)->_isa > (void *)0xFFF);
}

CoreFoundation 如何获取与 Foundation 共享的 isa 指针是本节剩余部分的主题。

Mac OS X 10.0 - Mac OS X 10.4 “Tiger”

免费桥接(toll-free bridging)的原始实现相当复杂。Foundation 定义了公共的 Objective-C 类(例如 NSObjectNSArrayNSMutableArray)、类簇(class clusters)的私有实现(例如 NSCFArray),以及一个桥接占位符(bridging placeholder)(例如 NSCFArray__),它是桥接实现的一个简单子类。

当 Foundation 被加载时,它会调用 CoreFoundation 的 __CFSetupFoundationBridging() 函数,该函数执行两项操作:

调用 __CFInitialize()。最初的初始化步骤之一是为 __CFRuntimeObjCClassTable 中的每个条目分配内存,该表将 CFTypeIDs 映射到其桥接的 Objective-C Class。这里分配的 objc_class(回想一下,Classobjc_class * 的 typedef)将在下一步成为 CoreFoundation 和 Foundation 之间共享的免费桥接类实例。该指针值在初始化过程早期就被预留,以便在后续初始化步骤中分配的 CoreFoundation 对象可以使用,从而保证这些对象能正确桥接。

对于每个桥接类型,首先查找其占位子类,若存在则调用 _CFRuntimeSetupBridging()

_CFRuntimeSetupBridging() 会将占位子类的 objc_class 结构体(译注:此为 Objective-C 运行时数据结构)进行按位拷贝,复制到上一步分配的内存空间中。

随后,CoreFoundation 会将其按位拷贝得到的占位类呈现为该桥接类型的最终实现。

void __CFSetupFoundationBridging(void *, void *, void *, void *) {
// ...
__CFInitialize();
// ...
Class aClass = objc_lookUpClass("NSCFArray__");
if (arrayClass != Nil) {
_CFRuntimeSetupBridging(CFArrayGetTypeID(), aClass->super_class, aClass);
}
aClass = objc_lookUpClass("NSCFDictionary__");
if (aClass != Nil) {
_CFRuntimeSetupBridging(CFDictionaryGetTypeID(), aClass->super_class, aClass);
}
// ...
}
void __CFInitialize(void) {
// ...
__CFRuntimeObjCClassTable[CFDictionaryGetTypeID()] = calloc(sizeof(struct objc_class), 1);
__CFRuntimeObjCClassTable[CFArrayGetTypeID()] = calloc(sizeof(struct objc_class), 1);
// ...
}
Boolean _CFRuntimeSetupBridging(CFTypeID typeID, struct objc_class *mainClass, struct objc_class *subClass) {
void *isa = __CFISAForTypeID(typeID);
memmove(isa, subClass, sizeof(struct objc_class));
class_poseAs(isa, mainClass);
return true;
}

Mac OS X 10.5 “Leopard” 及以后版本,以及 iOS

Posing 是 Objective-C 运行时(Runtime)中的一个已弃用特性(在 Objective-C 2 中已被移除),它允许一个子类有效地取代其父类的身份。执行 posing 的类会采用原始类的名称,为了保持名称的唯一性,运行时随后会在原始类的名称前添加一个 %,并在原始元类(metaclass)的名称前添加 _%。因此,继续以数组为例,NSCFArray__ 就变成了 NSCFArray,而原始的 NSCFArray 则变成了 %NSCFArray

被 pose 的类实例会接收到所有发送给原始类实例的消息。在上述桥接完成后,任何发送给 NSCFArray 类的消息(例如 alloc),都会被正在 pose 为 NSCFArray 的类(即 NSCFArray__)所接收。因此,这种重定向会导致 Foundation 使用被 pose 的类类型来创建新对象。随后,当将一个 Foundation 对象作为 CFTypeRef 传递给 Core Foundation 时,CF_IS_OBJC 会返回 false,因为该对象的 isa 将其标识为私有的桥接类型(因此不需要 Objective-C 分派到用户定义的子类)。由于它是一个私有类,Core Foundation 可以直接访问其内部实现。

未来类(future classes)大幅简化了无缝桥接(toll-free bridging)的实现。在 Mac OS X 10.5 及更高版本中,桥接配置仍然在 __CFInitialize() 中完成,但存在几个关键差异:

__CFInitialize() 在动态链接器加载框架时被调用,从而消除了对下游代码的任何初始化依赖。

对于每个桥接类型,CoreFoundation 会使用类名调用 objc_getFutureClass(),并用运行时返回的 Class 指针填充 __CFRuntimeObjCClassTable

如果该类已被加载,运行时会返回其实例指针。

否则,运行时会分配一个具有给定名称的 objc_class 实例,并返回指向这个” 未来类” 的指针。然后,当进程随后加载具有该名称的类时,运行时会将类定义复制到先前分配的内存中(类似于早期版本中的 _CFRuntimeSetupBridging()),并从二进制映像加载的类定义重映射到先前分配的类定义(类似于 posing)。尽管存在相似之处,这种策略同时简化了 Objective-C 运行时和 CoreFoundation 的实现。

以下是 Leopard 系统中新版 Core Foundation 实现的近似描述。

static void __CFInitialize(void) __attribute__ ((constructor));
static void __CFInitialize(void) {
// ...
_CFRuntimeBridgeClasses(0x10/*CFDictionaryGetTypeID()*/, "NSCFDictionary");
_CFRuntimeBridgeClasses(0x11/*CFArrayGetTypeID()*/, "NSCFArray");
// ...
}
void _CFRuntimeBridgeClasses(CFTypeID cf_typeID, const char *objc_classname) {
__CFRuntimeObjCClassTable[cf_typeID] = objc_getFutureClass(objc_classname);
}

未来类(future class)无需在进程生命周期内实例化。但如果未来类接收到任何消息,进程将会崩溃。我抽查了使用 Objective-C 运行时函数处理未来类的情况,测试的函数均能正常工作 —— 尽管这纯属偶然。

存根类(Stub Classes)

macOS 10.15 和 iOS 13 的 Objective-C 运行时引入了存根类以支持稳定的 Swift ABI(应用程序二进制接口)。当满足以下条件时,Swift 编译器会生成存根类:

  1. Swift 类型通过 @objc 特性声明为可在 Objective-C 中表示(包括所有直接或间接继承自 NSObject 的类)。
  2. 上述第 (1) 条中的 Swift 类被编译进启用了库演进(Library Evolution)的动态库(包括框架)中(需使用 -enable-library-evolution 构建标志)。库演进也被称为弹性(resilience)或 ABI 稳定性。
  3. 另一个模块中的 Swift 类导入了第 (2) 条中的模块,并继承了第 (1) 条中定义的类。当这个派生 Swift 类被编译时,编译器会将 Objective-C 类元数据(metadata)生成为类存根(class stub)。

我对这个场景为何需要存根进行了粗略调查,但未能得出结论。我想答案只能留给 Swift 内部原理系列文章了🙃。不过,我们可以看看 Objective-C 运行时(Runtime)如何处理类存根(stub class)。

一个 isa 值为 1 到 15(含)的值会将一个objc_class实例标识为存根类。目前,除 1 以外的其他 isa 值均被保留。

bool isStubClass() const {
uintptr_t isa = (uintptr_t)isaBits();
return 1 <= isa && isa < 16;
}

若调用 objc_getClassList()objc_copyClassList(),运行时(runtime)将按需初始化所有已加载到进程中的桩类(stub classes)。否则,桩类会在需要该类对象时按需初始化。

Swift 编译器生成的 Objective-C 头文件会为类接口添加 __attribute__((objc_class_stub)) 属性,这指示 Clang 通过调用 objc_loadClassref() 来获取类对象,而不是直接引用类符号。该运行时函数会在首次访问时调用 Swift 初始化器来生成 Class 对象,并将结果存储在桩类中供后续访问使用。

我期待着有朝一日能撰写一篇文章,解释究竟是哪个 Swift 特性使得这种额外的间接寻址成为必要!