(原文出处:Objective-C Internals | Always Processing)

Objective-C 中的未完成类能够指的是未来的类或类存根。未来类(一种私有的运行时特性)促进了与 CoreFoundation 的无缝桥接。而类存根则是 Swift 编译器生成的,用于支撑安稳的 Swift ABI 和 Objective-C 之间的互操作性。

在从前的一篇探讨了 Objective-C 类完成的文章中,忽略了一个有趣的细节:用于识别元类的函数的一个概念,即未完成的类。

// 类似于 isMetaClass,但也能够在未完成的类上有效
bool isMetaClassMaybeUnrealized() { /* ... */ }

未完成的类是部分初始化的元类,只要类名是已知的。有两种类型的未完成的元类:未来类和类存根。

未来类

Mac OS X 10.5 引入了未来类作为一个私有的运行时特性,以整理无缝桥接的完成。令人惊奇的是,这实际上在 objc_getFutureClass 中有文档说明:

用于 CoreFoundation 的无缝桥接。请不要自己调用此函数。

无缝桥接(Toll-Free Bridging)

首要,让咱们简要地看一下无缝桥接的完成。Mac OS X Developer Preview 1 引入了 CoreFoundation 结构和“无缝桥接”。简而言之,无缝桥接答应开发人员在桥接的 CoreFoundation 和 Foundation 类型之间自由转换(例如,将 CFArrayRef 转换为 NSArray * 或反之亦然),以便将一个类型传递为另一个类型,而无需承担任何运行时开销。此外,对于桥接的 Foundation 类的用户界说子类也得到了充沛支撑。

全面探讨无缝桥接超出了本文的范围,但您能够在这里阅读更多内容。咱们只需求突出两个要害的完成细节,以了解它怎么运用 Objective-C 运行时。

首要,一切 CoreFoundation 目标都有一个 Objective-C 兼容的 isa 字段作为其第一个成员。从 CFRuntime.h:

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

其次,每个带有 Foundation 等效物的 CoreFoundation 函数(例如,CFArrayGetCount() 和 -[NSArray count])将在 isa 不匹配 bridged 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”

无缝桥接的原始完成非常复杂。Foundation 界说了公共的 Objective-C 类(例如 NSObject、NSArray、NSMutableArray)、类群的私有完成(例如 NSCFArray)以及桥接占位符(例如 NSCFArray__),后者是桥接完成的一个微乎其微的子类。

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

调用 __CFInitialize()。最初的初始化进程之一是为 __CFRuntimeObjCClassTable 中的每个条目分配内存,该表将 CFTypeIDs 映射到它们的桥接 Objective-C Class。在这里分配的 objc_class(回想一下,Class 是 objc_class * 的 typedef)将成为鄙人一步中在后续的初始化进程中分配给 CoreFoundation 目标的内存中可用的桥接类实例。在初始化进程的早期,保存了指针值,以确保在分配后续初始化进程中分配的 CoreFoundation 目标时,这些目标能够正确地进行桥接。

对于每个桥接类型,查找占位符子类,并在存在时调用 _CFRuntimeSetupBridging()。

_CFRuntimeSetupBridging() 对占位符的 objc_class 结构进行位拷贝,将其拷贝到前面一步分配的内存中。

然后,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;
}

替身是 Objective-C 运行时的一项已弃用功用(在 Objective-C 2 中已移除),其实质是答应子类替代其父类的身份。替身类运用原始类的称号,为了保存称号的唯一性,运行时随后在原始类的称号前加上 %,在原始元类的称号前加上 %。因而,继续以数组为例,NSCFArray_ 变为 NSCFArray,而原始的 NSCFArray 变为 %NSCFArray。

替身类实例会接纳发送给原始类实例的一切音讯。在上述桥接完成后,发送到 NSCFArray 类(例如,alloc)的任何音讯都会由作为 NSCFArray(即 NSCFArray__)进行替身的类接纳。因而,此重定向导致 Foundation 运用替身类类型创建新目标。然后,当将 Foundation 目标作为 CFTypeRef 传递给 CoreFoundation 时,CF_IS_OBJC 回来 false,因为目标具有将其标识为私有桥接类型的 isa(因而不需求 Objective-C 分配到用户界说的子类)。由于它是一个私有类,CoreFoundation 能够直接拜访其内部。

Mac OS X 10.5 “Leopard” 及之后以及 iOS

未来类极大地简化了无缝桥接的完成。在 Mac OS X 10.5 及更高版别中,桥接配置仍然发生在 __CFInitialize() 中,但有一些要害的区别:

__CFInitialize() 在动态链接器加载结构时调用,消除了对下游代码的初始化依赖。

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

假如类已加载,运行时将回来其实例指针。

否则,运行时将分配一个带有给定称号的 objc_class 实例,并回来指向此“未来”类的指针。然后,当进程稍后加载具有该称号的类时,运行时将类界说拷贝到从前分配的内存中(类似于前版别中的 _CFRuntimeSetupBridging()),并将从二进制图像加载的类界说重新映射到从前分配的类界说(类似于替身)。虽然存在相似之处,但这种策略简化了 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);
}

未来类不要求在进程的生命周期内完成。但是,假如未来类接纳任何音讯,进程将崩溃。我对运用 Objective-C 运行时函数的未来类进行了检查,我测试过的函数在功用上都是正确的,即使是偶尔的。

类存根

macOS 10.15 和 iOS 13 中的 Objective-C 运行时引入了类存根以支撑安稳的 Swift ABI。Swift 编译器在以下状况下会生成类存根类:

通过 @objc 特点声明的 Swift 类型可在 Objective-C 中表明,包含任何直接或间接承继自 NSObject 的类。

Swift 类被编译成启用了库演化(运用 -enable-library-evolution 构建标志)的动态库(包含结构)。库演化也称为耐性或 ABI 安稳性。

另一个模块中的 Swift 类导入了(2)中的模块,并将(1)中界说的类作为其子类。当编译此衍生的 Swift 类时,编译器将生成 Objective-C 类元数据作为类存根。

我对为此状况需求存根的原因进行了初步查询,但没有得出任何结论。我想答案将不得不等到 Swift 内部探秘系列文章中 。但是,咱们能够看看 Objective-C 运行时是怎么处理类存根的。

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(),则运行时将根据需求初始化进程中加载的一切存根类。否则,在需求类目标时,将按需初始化存根类。

Swift 编译器生成的 Objective-C 头文件在类接口中添加了一个 attribute((objc_class_stub)),这会指示 Clang 通过调用 objc_loadClassref() 获取类目标,而不是直接引证类符号。运行时函数在初次拜访时调用 Swift 初始化器来生成 Class 目标,并将结果存储在存根中以供将来拜访。