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

相关推荐
玫瑰花开一片一片9 小时前
Flutter IOS 真机 Widget 错误。Widget 安装后系统中没有
flutter·ios·widget·ios widget
烎就是我11 小时前
100行代码swift从零实现一个iOS日历
ios·swift
鸿蒙布道师1 天前
鸿蒙NEXT开发通知工具类(ArkTs)
android·ios·华为·harmonyos·arkts·鸿蒙系统·huawei
鸿蒙布道师1 天前
鸿蒙NEXT开发网络相关工具类(ArkTs)
android·ios·华为·harmonyos·arkts·鸿蒙系统·huawei
余生大大1 天前
关于Safari浏览器在ios<16.3版本不支持正则表达式零宽断言的解决办法
ios·正则表达式·safari
爱分享的程序员1 天前
前端跨端框架的开发以及IOS和安卓的开发流程和打包上架的详细流程
android·前端·ios
Macle_Chen1 天前
ios开发中xxx.debug.dylib not found
ios·bug·debug.dylib
WDeLiang2 天前
Flutter 环境搭建
flutter·ios·visual studio code
lilili啊啊啊2 天前
iOS 应用性能测试工具对比:Xcode Instruments、克魔助手与性能狗
测试工具·ios·iphone·xcode·克魔