[翻译]Objective-C内部探秘6:标记指针对象

(原文出处: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:很感兴趣。这意味着另一个私有子类使用了标记指针对象优化,但我没有跟踪各种代码路径,以尝试识别它。

相关推荐
奇客软件1 天前
如何从相机的记忆棒(存储卡)中恢复丢失照片
深度学习·数码相机·ios·智能手机·电脑·笔记本电脑·iphone
GEEKVIP1 天前
如何修复变砖的手机并恢复丢失的数据
macos·ios·智能手机·word·手机·笔记本电脑·iphone
一丝晨光1 天前
继承、Lambda、Objective-C和Swift
开发语言·macos·ios·objective-c·swift·继承·lambda
GEEKVIP2 天前
iPhone/iPad技巧:如何解锁锁定的 iPhone 或 iPad
windows·macos·ios·智能手机·笔记本电脑·iphone·ipad
KWMax2 天前
RxSwift系列(二)操作符
ios·swift·rxswift
Mamong2 天前
Swift并发笔记
开发语言·ios·swift
GEEKVIP2 天前
手机使用指南:如何在没有备份的情况下从 Android 设备恢复已删除的联系人
android·macos·ios·智能手机·手机·笔记本电脑·iphone
奇客软件3 天前
如何使用工具删除 iPhone 上的图片背景
windows·ios·智能手机·excel·音视频·cocoa·iphone
安和昂3 天前
【iOS】计算器的仿写
ios
SchneeDuan3 天前
iOS--App启动过程及优化
ios