[翻译]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 对象,并将结果存储在存根中以供将来访问。

相关推荐
Swift社区5 小时前
在 Swift 中实现字符串分割问题:以字典中的单词构造句子
开发语言·ios·swift
#摩斯先生5 小时前
Swift从0开始学习 对象和类 day3
ios·xcode·swift
没头脑的ht5 小时前
Swift内存访问冲突
开发语言·ios·swift
#摩斯先生5 小时前
Swift从0开始学习 并发性 day4
ios·xcode·swift
没头脑的ht5 小时前
Swift闭包的本质
开发语言·ios·swift
Jinkey11 小时前
FlutterBasic - GetBuilder、Obx、GetX<Controller>、GetxController 有啥区别
android·flutter·ios
程序猿看视界17 小时前
如何在 UniApp 中实现 iOS 版本更新检测
ios·uniapp·版本更新
dr李四维21 小时前
iOS构建版本以及Hbuilder打iOS的ipa包全流程
前端·笔记·ios·产品运营·产品经理·xcode
️ 邪神21 小时前
【Android、IOS、Flutter、鸿蒙、ReactNative 】自定义View
flutter·ios·鸿蒙·reactnative·anroid
比格丽巴格丽抱1 天前
flutter项目苹果编译运行打包上线
flutter·ios