(原文出处:Objective-C Internals | Always Processing)
标记指针对象(Tagged Pointer Objects)是Objective-C运行时的一个私有特性,苹果用它来优化一些核心Foundation类(基类的双关语)。
什么是标记指针?
大多数内存分配器保证每个分配的最小对齐。例如,在苹果的平台上,malloc()保证了一个对齐,可以用于任何数据类型,包括AltiVec和SSE相关类型。
在实践中,所有的分配都是16字节对齐的,因此任何指针的低4位始终为零。有时,使用这个事实并在这些位中存储附加信息是有利的(这需要更新指针值的所有使用方式,以正确处理额外的位)。当指针的未使用位存储附加信息时,通常称该指针为"标记"。
Objective-C对象中的isa指针可能是标记的,其详细信息在此系列早期的"The Many Uses of isa"帖子中的"Non-Pointer isa"部分中讨论过。
标记指针对象
在Objective-C中,标记指针对象是一种特殊类型的对象指针。如果对象指针被标记,由标记指针表示的类实例所持有的数据完全编码到指针值本身中。不会发生堆分配。
消除堆分配可以显著降低例如NSArray中的NSNumbers的成本。在堆上分配的NSNumber使用至少16字节(因为分配是16字节对齐的),加上8字节的指针。然而,编码为标记指针的NSNumber仅使用8字节的指针,通过不调用分配器来节省内存和时间。
标记身份
用于将指针标识为标记的位因平台而异。objc-internal.h为运行时实现提供了一个方便的函数,用于标识指针是否被标记。macOS和基于Intel处理器的Catalyst应用程序使用位0来标识标记的指针对象,其他一切都使用位63。
C
#if __arm64__
// ARM64 uses a new tagged pointer scheme...
# define _OBJC_TAG_MASK (1UL<<63)
#elif (TARGET_OS_OSX || TARGET_OS_MACCATALYST) && __x86_64__
// 64-bit Mac - tag bit is LSB
# define _OBJC_TAG_MASK 1UL
#else
// Everything else - tag bit is MSB
# define _OBJC_TAG_MASK (1UL<<63)
#endif
static inline bool
_objc_isTaggedPointer(const void *ptr) {
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
通过消除堆分配来存储对象的值也消除了isa指针的存储。因此,标记指针对象方案保留了一些位来标识对象的类。
在撰写本文时,Objective-C运行时有两种方案来保留类标识位:一种保留3位,用于带有60位有效负载的对象,另一种保留11位,用于带有52位有效负载的对象。
最多可以使用7种类类型来使用60位有效负载变体(第八种类型是用于标识52位有效负载变体的特殊情况)。最多可以使用256种类类型来使用52位有效负载变体。objc-internal.h有一个枚举为各种类标识位值提供了一些符号化标识。
当运行时需要对象的isa指针时,它调用objc_object::getIsa()[1](在objc-object.h中定义),该函数对于在堆上分配的对象返回isa实例变量,对于标记指针对象之一的标记类数组中存储的isa指针。
C
inline Class
objc_object::getIsa() {
if (fastpath(!isTaggedPointer())) return ISA(/*authenticated*/true);
extern objc_class OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
uintptr_t slot, ptr = (uintptr_t)this;
Class cls;
slot = (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
cls = objc_tag_classes[slot];
if (slowpath(cls == (Class)&OBJC_CLASS_$___NSUnrecognizedTaggedPointer)) {
slot = (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
cls = objc_tag_ext_classes[slot];
}
return cls;
}
当系统框架在进程开始时初始化时,它们调用_objc_registerTaggedPointerClass()来设置给定标记值的Class对象。我们可以通过为该函数添加一个符号断点,并在调用时打印其参数来观察这一点:
assembly
(lldb) reg re x0 x1
x0 = 0x0000000000000003
x1 = 0x0000000203a994f0 (void *)0x0000000203a99518: __NSCFNumber
消息发送的解剖
任何操作指针值的代码都必须特别处理标记指针,因为无条件地解引用指针几乎肯定会导致运行时崩溃。
objc_msgSend()中的第一步是检查标记指针,以确定如何加载对象的isa指针以查找其类的方法,类似于上面的objc_object::getIsa()实现。在加载了isa指针之后,指针是否被标记对于消息发送逻辑的其余部分来说都是无关紧要的。
当支持标记指针对象的Objective-C类接收到消息时,它必须检查其self指针是否被标记,并适当地处理该情况。下面是我的分析-[NSNumber integerValue]的一部分,以展示NSNumber标记指针的工作原理(根据我对macOS 12.6.2上arm64架构的这些函数反汇编的解释):
C
@implementation __NSCFNumber
- (NSInteger)integerValue {
return [self longValue];
}
- (long)longValue {
long longValue;
CFNumberGetValue((__bridge CFNumberRef)self, kCFNumberSInt64Type, &longValue);
return longValue;
}
@end
Boolean CFNumberGetValue(CFNumberRef number, CFNumberType theType, void *valuePtr) {
if (_objc_isTaggedPointer(number)) {
if (_objc_getTaggedPointerTag(number) == OBJC_TAG_NSNumber) {
long localMemory;
valuePtr = valuePtr ?: (void *)&localMemory;
uintptr_t value = _objc_getTaggedPointerValue(number);
uintptr_t shift = (value & 0x08) ? 0x4 : 0x6;
CFNumberType type = __CFNumberTypeTable[theType].canonicalType;
if (type <= kCFNumberFloat64Type) {
value = value >> shift;
switch (type) {
case kCFNumberSInt8Type:
*(uint8_t *)valuePtr = (uint8_t)value;
break;
case kCFNumberSInt16Type:
*(uint16_t *)valuePtr = (uint16_t)value;
break;
case kCFNumberSInt32Type:
*(uint32_t *)valuePtr = (uint32_t)value;
break;
case kCFNumberSInt64Type:
*(uint64_t *)valuePtr = (uint64_t)value;
break;
case kCFNumberFloat32Type:
*(float *)valuePtr = (float)value;
break;
case kCFNumberFloat64Type:
*(double *)valuePtr = (double)value;
break;
}
return true;
} else {
return __CFNumberGetValueCompat(number, theType, valuePtr);
}
} else {
theType = __CFNumberTypeTable[theType].canonicalType;
return [(__bridge id)number _getValue:valuePtr forType:theType];
}
} else {
// ...
}
}
关于上述手动反汇编代码,我想分享一些评论和观察:
这个私有的__NSCFNumber子类(在这段代码路径中)不操作self的值,因此不需要处理标记指针的情况。-integerValue方法作为-longValue方法的别名,后者调用CoreFoundation来完成繁重的工作。鉴于CFNumberRef和NSNumber *是toll-free bridged,看到一个类型是另一个类型的包装是有道理的。
Apple的CFNumber的实现几乎肯定包括了objc-internal.h,以便访问简化与标记指针对象一起使用的函数。
似乎4位或6位的有效负载作为其他CFNumber功能的位标志,但无论是哪个功能,这个函数都没有使用。
__CFNumberTypeTable将公共类型值映射到对应的固定大小的类型,简化了值提取逻辑。
Apple有一个私有类型kCFNumberSInt128Type,在switch语句中未处理,所以对__CFNumberGetValueCompat()的调用对于这种情况是必要的。
具有非零分数值的浮点数不使用标记指针对象优化,因为switch语句中的逻辑不处理该情况。
我对调用-_getValue:forType:很感兴趣。这意味着另一个私有子类使用了标记指针对象优化,但我没有跟踪各种代码路径,以尝试识别它。