[翻译]Objective-C内部探秘4:未实现的类(和桥接)

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

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

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

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:

C 复制代码
typedef struct __CFRuntimeBase {
    void *_isa;
    // ...
} CFRuntimeBase;

其次,每个带有 Foundation 等效物的 CoreFoundation 函数(例如,CFArrayGetCount() 和 -[NSArray count])将在 isa 不匹配 bridged CoreFoundation 类型的 isa 的情况下调用 Objective-C 实现,这就是为用户定义的 Foundation 子类提供桥接支持的地方。(CFInternal.h 定义了 Objective-C 分派宏。)

scss 复制代码
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 将其位拷贝的占位符类呈现为桥接实现。

C 复制代码
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 实现的近似。

C 复制代码
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 值都是保留的。

C 复制代码
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 对象,并将结果存储在存根中以供将来访问。

相关推荐
用户092 天前
SwiftUI Charts 函数绘图完全指南
ios·swiftui·swift
YungFan2 天前
iOS26适配指南之UIColor
ios·swift
权咚2 天前
阿权的开发经验小集
git·ios·xcode
用户092 天前
TipKit与CloudKit同步完全指南
ios·swift
法的空间3 天前
Flutter JsonToDart 支持 JsonSchema
android·flutter·ios
2501_915918413 天前
iOS 上架全流程指南 iOS 应用发布步骤、App Store 上架流程、uni-app 打包上传 ipa 与审核实战经验分享
android·ios·小程序·uni-app·cocoa·iphone·webview
00后程序员张3 天前
iOS App 混淆与加固对比 源码混淆与ipa文件混淆的区别、iOS代码保护与应用安全场景最佳实践
android·安全·ios·小程序·uni-app·iphone·webview
Magnetic_h3 天前
【iOS】设计模式复习
笔记·学习·ios·设计模式·objective-c·cocoa
00后程序员张3 天前
详细解析苹果iOS应用上架到App Store的完整步骤与指南
android·ios·小程序·https·uni-app·iphone·webview
前端小超超3 天前
capacitor配置ios应用图标不同尺寸
ios·蓝桥杯·cocoa