(原文出处:Objective-C Internals | Always Processing)
Objective-C 运行时通过在对象的 isa 指针中打包额外的信息来优化性能。本文比较了不同的打包机制,并讨论了存储在指针值中的各种字段值。
本系列的第一篇文章介绍了 isa 指针:它是每个 Objective-C 对象中的一个实例变量,指向其类对象,用于标识对象实例的类型。上一篇文章引用了这个字段的内部定义(char isa_storage[sizeof(isa_t)]),并提到 isa 字段已经被弃用。现在,我们将详细探讨运行时如何使用这个字段以及它的弃用情况。
背景
在 Apple 的 32 位 Objective-C 运行时(iOS、macOS 和 tvOS 上),isa 字段只是指向对象的类对象的指针。下面的代码来自 objc-private.h、objc-object.h 和 objc-class.mm,显示了从对象实例获取 isa 类指针值的 object_getClass() 运行时函数的有效实现(适用于这些平台)。
// objc-private.h
union isa_t {
private:
Class cls;
public:
Class getClass(bool authenticated);
};
// objc-object.h
Class objc_object::getIsa() {
return ISA(); // 布尔类型参数,默认值是false
}
Class isa_t::getClass(bool) {
return cls;
}
Class objc_object::ISA(bool) {
return isa().getClass(false);
}
// objc-class.mm
Class object_getClass(id obj) {
if (obj) return obj->getIsa();
else return Nil;
}
非指针 isa
Apple 的 64 位 Objective-C 运行时(64 位 Intel 处理器上的模拟器除外)以及 Apple Watch 上的 Objective-C 运行时使用“非指针 isa”,它在未使用的指针位中打包了额外的信息。
在指针值中设置额外的位会改变其引用的地址并使其失效,新值可能不是进程地址空间中的地址,如果解引用可能会导致未对齐的内存访问,等等。因此,称为“非指针 isa”。
由于 isa 字段不再仅存储类指针值,所以在 objc.h 公共头文件中将其标记为已弃用,以防止直接使用可能导致未定义行为。(可用性宏定义位于 objc-api.h 中。)object_getClass() 和 object_setClass() 函数是直接使用 isa 字段的官方替代方法。
// objc-api.h
#if !defined(OBJC_ISA_AVAILABILITY)
# define OBJC_ISA_AVAILABILITY __attribute__((deprecated))
#endif
// objc.h
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
非指针 isa 变体
在撰写本文时,非指针 isa 有三种实现方式:
- Packed isa(Apple Silicon arm64-not-e 变体,64 位 Intel)
- Packed isa with Pointer Authentication(Apple Silicon arm64e 变体)
- Indexed isa(Apple Watch)
下表显示了每个非指针 isa 变体中打包到未使用类指针位的额外字段:
Packed isa | Packed isa with Ptr Auth | Indexed isa |
---|---|---|
has_assoc | has_assoc | has_assoc |
has_cxx_dtor | - | has_cxx_dtor |
shiftcls | - | - |
shiftcls_and_sig | shiftcls_and_sig | - |
indexcls | - | indexcls |
magic | magic | magic |
weakly_referenced | weakly_referenced | weakly_referenced |
has_sidetable_rc | has_sidetable_rc | has_sidetable_rc |
extra_rc | extra_rc | extra_rc |
符号含义:
- ✓ 表示该变体具有此字段。
- − 表示该变体不适用此字段。
- x 表示该变体没有此字段。
现在,让我们探讨运行时中每个字段的使用方式,以及它们如何用于提高 Objective-C 运行时的性能。
nonpointer
这是指针载荷的最低有效位(因此对于指针值始终为零),如果设置,表示 isa 值是非指针变体。此字段使运行时能够在运行时为兼容性目的在每个类上基于每个类使用遗留的 isa-value-is-a-class-pointer 行为或非指针 isa 优化。
在 macOS 上,对于任何链接到 OS X 10.10 或更早版本的应用程序,都会禁用非指针 isa,因为直接的 isa 使用在 OS X 10.11 之前没有被弃用。
if (dyld_get_active_platform() == PLATFORM_MACOS && !dyld_program_sdk_at_least(dyld_platform_version_macOS_10_11)) {
DisableNonpointerIsa = true;
}
如果主应用程序可执行文件有一个 __DATA,__objc_rawisa 节,运行时会禁用非指针 isa 特性。在直接 isa 使用被弃用之前,可能会加载插件的应用程序使用此节来启用二进制兼容性。(这仅适用于 macOS。)
for (EACH_HEADER) {
if (hi->mhdr()->filetype != MH_EXECUTE) continue;
unsigned long size;
if (getsectiondata(hi->mhdr(), "__DATA", "__objc_rawisa", &size)) {
DisableNonpointerIsa = true;
}
break; // assume only one MH_EXECUTE image
}
对于所有平台,Objective-C 运行时对 OS_object 类及其派生类禁用非指针 isa 特性,因为 libdispatch 也使用 isa 指针作为虚表。
cCopy codeelse if (!hackedDispatch && 0 == strcmp(ro->getName(), "OS_object")) {
// hack for libdispatch et al - isa also acts as vtable pointer
hackedDispatch = true;
instancesRequireRawIsa = true;
}
has_assoc、has_cxx_dtor、weakly_referenced
和 has_sidetable_rc 这四个字段的主要目的是确定一个对象是否可以使用快速释放路径,该路径只是简单地使用 free() 释放对象的内存。否则,在释放内存之前,运行时必须首先进行额外的记账。来自 objc-object.h:
cCopy codevoid objc_object::rootDealloc() {
if (isTaggedPointer()) return;
if (isa().nonpointer &&
!isa().weakly_referenced &&
!isa().has_assoc &&
#if ISA_HAS_CXX_DTOR_BIT
!isa().has_cxx_dtor &&
#else
!isa().getClass(false)->hasCxxDtor() &&
#endif
!isa().has_sidetable_rc)
{
free(this);
} else {
object_dispose((id)this);
}
}
has_assoc:如果对象通过使用 objc_setAssociatedObject() 运行时 API 创建了一个关联对象,则设置为 true。如果一个对象有一个或多个关联对象,运行时必须在释放对象的内存之前从其侧表中删除条目。
has_cxx_dtor:如果类或超类有一个 .cxx_destruct 方法,则设置为 true。如果一个 Objective-C 对象有一个或多个带有 C++ 类型的实例变量[1],运行时会在对象分配时(在任何 init 方法之前)调用类的 .cxx_construct 实例方法来运行任何非平凡的构造函数。在 dealloc 方法链完成后,运行时会调用类的 .cxx_destruct 实例方法,在释放对象的内存之前运行任何非平凡的析构函数。
当启用自动引用计数(ARC)时,编译器会在类的 .cxx_destruct 方法中实现实例变量的释放,从而阻止优化直接调用 free()。object_dispose() 代码路径调用 objc_destructInstance(),如果可用,会使用非指针 isa 位来省略不必要的清理操作。
请注意,当启用指针认证时,此位不可用,但类对象上可以获取此信息,代价是需要额外的内存加载。
weakly_referenced:每当创建对象的弱引用[2]时,设置为 true。与关联对象类似,运行时在释放对象的内存之前必须从其侧表中删除条目。
has_sidetable_rc:如果保留计数已经溢出了 extra_rc,一个侧表会存储额外的保留计数,此时运行时必须在释放对象的内存之前删除条目。
shiftcls 和 shiftcls_and_sig
这些字段存储了 packed isa 变体的类指针。类对象始终是 8 字节对齐的(无论是通过二进制映像中的布局还是运行时的标准分配器),因此最低的 3 位始终是 0。因此,“shift class” 字段将类指针存储为将最低的 3 位移出的值。该字段还依赖于虚拟内存系统允许的最大指针值,因为将该值存储在位域中会截断值的高位。
在 Apple Silicon 上使用指针认证对类指针进行签名(并将其存储在 shiftcls_and_sig 字段中)。除了有效指针值的下界和上界之外,此字段还依赖于指针认证使用的位。
indexcls
Apple Watch 上的 Objective-C 运行时将类指针存储在数组中,并将类的数组索引存储在 isa 的 indexcls 字段中。索引在运行时延迟分配,如果数组的容量(32,767 个条目)用尽,则运行时会回退到使用指针 isa。
Apple Watch ABI 使用 32 位指针[3],这些指针没有足够的未使用位来存储指针值和打包位。使用数组存储类指针减少了类标识所需的位数,以便以一些间接性的代价获得非指针 isa 的性能优势。
magic
此字段在运行时中未使用。运行时导出了用于魔术掩码和魔术值的常量,调试器使用这些常量来识别具有非指针 isa 的对象实例,以启用 Objective-C 调试功能。
extra_rc
对象实例的保留计数在非指针 isa 特性在平台或类层次结构不可用时存储在侧表中。然而,对于许多并发的保留或释放操作,使用侧表可能成为性能瓶颈,因为每个操作都必须获取锁。非指针 isa 特性通过在这个字段中存储实例的保留计数来减少这种竞争。
如果保留计数溢出了字段的值,一半的保留计数被移动到侧表中,一半保留在这个字段中。仅移动一半的计数使得未来的释放操作可以在获取锁以访问侧表中的计数之前减少字段值。
这个字段的大小因平台而异。下表列出了每个平台的字段大小和最大的内联保留计数值。
extra_rc 位数 | 最大值 |
---|---|
Packed isa on 64-bit Intel | 8 |
Packed isa on Apple Silicon arm64-not-e variant | 19 |
Packed isa on Apple Silicon with Pointer Authentication (arm64e variant) | 8 |
Indexed isa (Apple Watch) | 7 |
- 之后的文章将更详细地探讨 Objective-C++。
- 之后的文章将更详细地探讨弱引用。
- 之后的文章将更详细地探讨 Apple 的 arm64_32 ABI。