NSNumber 做为 Tagged Pointer 是如何被构造出来的?

从 SF 慢慢把文章搬过来。。。html

segmentfault.com/a/119000001…git

TL.DR

本文以 NSNumber 为例,说明一个 Tagged Pointer 是怎样被建立出来的。github

没有 isa

int main(int argc, const char * argv[])
{
    NSNumber *n = @3;
    
    return 0;
}
复制代码

能够看到 n 并无 isa,它确实不是一个 OC 的对象。segmentfault

NSNumber

NSPlaceholderNumber

int main(int argc, const char * argv[])
{
    // 将 alloc 和 init 拆开来看
    NSNumber *n = [NSNumber alloc];
    n = [n initWithInt:3];
    
    return 0;
}
复制代码

架构

NSNumber alloc
[NSNumber alloc] 返回的是NSPlaceholderNumber,有 isa,这是个正常的 OC 对象。

不对呀,若是每次 alloc 都会生成一个 NSPlaceholderNumber 对象,那还不如直接生成一个 NSNumber 对象呢,这哪有内存优化?app

一个直观的猜想是,屡次 alloc 返回的 NSPlaceholderNumber 是同一个实例。ide

int main(int argc, const char * argv[])
{
    NSNumber *a = [NSNumber alloc];
    NSNumber *b = [NSNumber alloc];
    NSNumber *c = [NSNumber alloc];
    NSNumber *d = [NSNumber alloc];
    NSNumber *e = [NSNumber alloc];
    NSNumber *f = [NSNumber alloc];
    NSNumber *h = [NSNumber alloc];
    
    return 0;
}
复制代码

结果 函数

它们都同样
果真如此。

NSPlaceholderNumber 是哪来的

[NSNumber alloc] 最终调用了 [NSNumber allocWithZone:]测试

NSNumber *__cdecl +[NSNumber allocWithZone:](NSNumber_meta *self, SEL a2, _NSZone *a3)
{
  NSNumber *result; // rax

  if ( (NSNumber_meta *)__NSNumberClass == self )
    result = (NSNumber *)_NSPlaceholderValueOrNumber((__int64)&__NSNumberClass, 1);
  else
    result = (NSNumber *)NSAllocateObject(self, 0LL, a3);
  return result;
}
复制代码

这里值得关注的是 _NSPlaceholderValueOrNumber(,)优化

__int64 __usercall _NSPlaceholderValueOrNumber@<rax>(__int64 a1@<rax>, char a2@<dil>)
{
  __int64 result; // rax
  void *v3; // rax
  __int64 v4; // rcx
  void *v5; // rax
  __int64 v6; // rax
  __int64 v7; // [rsp-8h] [rbp-10h]

  // cpv,我猜是 const pointer value
  v7 = a1;
  result = _NSPlaceholderValueOrNumber_cpv;
  if ( !_NSPlaceholderValueOrNumber_cpv )
  {
    v3 = _objc_msgSend(&OBJC_CLASS___NSPlaceholderValue, "self", v7);
    result = NSAllocateObject(v3, 0LL, 0LL);

    // _NSPlaceholderValueOrNumber_cpv 应该是个全局变量
    // 也就是说 NSPlaceholderValue 实例只会有一个
    _NSPlaceholderValueOrNumber_cpv = result;
  }

  //相应地 cpn 应该是 const pointer number..
  v4 = _NSPlaceholderValueOrNumber_cpn;
  if ( !_NSPlaceholderValueOrNumber_cpn )
  {
    v5 = _objc_msgSend(&OBJC_CLASS___NSPlaceholderNumber, "self", v7);
    v6 = NSAllocateObject(v5, 0LL, 0LL);
    v4 = v6;

    // 同理 NSPlaceholderNumber 实例只会有一个
    _NSPlaceholderValueOrNumber_cpn = v6;

    result = _NSPlaceholderValueOrNumber_cpv;
  }

  if ( a2 ) //a2 为 true 则使用cpn,即返回 NSPlaceholderNumber 实例
  {
    result = v4;
  }

  // btw,这几兄弟的继承关系是这样的:NSPlaceholderNumber -> NSPlaceholderValue -> NSNumber -> NSValue
  return result;
}
复制代码

原来是经过全局变量 _NSPlaceholderValueOrNumber_cpn 来作到始终返回一个 NSPlaceholderNumber 实例的。 代码中还有一个相似的全局变量 _NSPlaceholderValueOrNumber_cpv,用来返回 NSPlaceholderValue 实例。

int main(int argc, const char * argv[])
{
    NSValue *v1 = [NSValue alloc];
    NSValue *v2 = [NSValue alloc];
    NSValue *v3 = [NSValue alloc];

    NSNumber *n1 = [NSNumber alloc];
    NSNumber *n2 = [NSNumber alloc];
    NSNumber *n3 = [NSNumber alloc];

    return 0;
}
复制代码

的确如此

确实不同

那么 NSPlaceholderNumber 是如何最终返回一个 Tagged Pointer 的呢?

_NSCFNumber,最终的 Tagged Pointer

NSPlaceholderNumber 自己乏善可陈,[NSPlaceholderNumber initWithInt:]只是一层皮

NSPlaceholderNumber *__cdecl -[NSPlaceholderNumber initWithInt:](NSPlaceholderNumber *self, SEL a2, int a3)
{
  int v4; // [rsp+Ch] [rbp-4h]

  v4 = a3;

  // 其它的 initWithXXX: 等方法也都是调用 CFNumberCreate,惟一的区别是指定的数据长度不一样
  return (NSPlaceholderNumber *)CFNumberCreate(kCFAllocatorDefault, 3LL, &v4);
}
复制代码

真正的执行者是 CFNumberCreate,它的代码很长,这里只取生成 Tagged Pointer 的部分

// a1: 分配器类型,从NSPlaceholderNumber那传过来的是kCFAllocatorDefault
// a2: 数值长度
// a3: 数值
// a1: 分配器类型,从NSPlaceholderNumber那传过来的是kCFAllocatorDefault
// a2: 数值长度
// a3: 数值
unsigned __int64 __fastcall CFNumberCreate(__objc2_class **a1, __int64 a2, unsigned int *a3) {
  // ...
  
  if ( __CFTaggedNumberClass
    && (kCFAllocatorSystemDefault == v4
     || (!v4 || (__objc2_class **)kCFAllocatorDefault == v4)
     && kCFAllocatorSystemDefault == (__objc2_class **)CFAllocatorGetDefault())
    && __CFNumberCaching != 2 )
  {
    switch ( __CFNumberTypeTable[a2] & 0x1F )
    {
      case 1:
        *(_QWORD *)&v5 = *(char *)v3;
        return objc_debug_taggedpointer_obfuscator ^ (__CFNumberCanonicalTypeIndex[__CFNumberTypeTable[a2] & 7] | 16LL * *(_QWORD *)&v5 & 0xFFFFFFFFFFFFFF0LL | 0xB000000000000000LL);
      case 2:
        *(_QWORD *)&v5 = *(signed __int16 *)v3;
        return objc_debug_taggedpointer_obfuscator ^ (__CFNumberCanonicalTypeIndex[__CFNumberTypeTable[a2] & 7] | 16LL * *(_QWORD *)&v5 & 0xFFFFFFFFFFFFFF0LL | 0xB000000000000000LL);
      case 3:
        *(_QWORD *)&v5 = (signed int)*v3;
        return objc_debug_taggedpointer_obfuscator ^ (__CFNumberCanonicalTypeIndex[__CFNumberTypeTable[a2] & 7] | 16LL * *(_QWORD *)&v5 & 0xFFFFFFFFFFFFFF0LL | 0xB000000000000000LL);
      case 4:
        v5 = *(double *)v3;
        goto LABEL_21;
      case 5:
        v6 = _mm_cvtsi32_si128(*v3);
        *(_QWORD *)&v5 = (unsigned int)(signed int)*(float *)v6.m128i_i32;
        if ( *(float *)v6.m128i_i32 != (float)SLODWORD(v5) )
          goto LABEL_23;
        v7 = *(_QWORD *)&v5 == 0LL;
        v8 = _mm_cvtsi128_si32(v6) < 0;
        break;
      case 6:
        *(_QWORD *)&v5 = (unsigned int)(signed int)*(double *)v3;
        if ( *(double *)v3 != (double)SLODWORD(v5) )
          goto LABEL_23;
        v7 = *(_QWORD *)&v5 == 0LL;
        v8 = *(_QWORD *)v3 < 0;
        break;
      default:
        goto LABEL_23;
    }

    // ...
}
复制代码

这里最重要的调用无疑是 objc_debug_taggedpointer_obfuscator。 遗憾的是,在目前开源的 objc4-723 中的实现和 macOS 10.13 上的 /usr/lib/libobjc.A.dylib 中均没有找到这个函数。

做为代替,取 objc4-723 中构造 Tagged Pointer 的部分

// Create a tagged pointer object with the given tag and value.
// Assumes the tag is valid.
// Assumes tagged pointers are enabled.
// The value will be silently truncated to fit.
static inline void * _Nonnull
_objc_makeTaggedPointer(objc_tag_index_t tag, uintptr_t value)
{
    // PAYLOAD_LSHIFT and PAYLOAD_RSHIFT are the payload extraction shifts.
    // They are reversed here for payload insertion.

    // assert(_objc_taggedPointersEnabled());
    if (tag <= OBJC_TAG_Last60BitPayload) {
        // assert(((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT) == value);
        return (void *)
            (_OBJC_TAG_MASK | 
             ((uintptr_t)tag << _OBJC_TAG_INDEX_SHIFT) | 
             ((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT));
    } else {
        // assert(tag >= OBJC_TAG_First52BitPayload);
        // assert(tag <= OBJC_TAG_Last52BitPayload);
        // assert(((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT) == value);
        return (void *)
            (_OBJC_TAG_EXT_MASK |
             ((uintptr_t)(tag - OBJC_TAG_First52BitPayload) << _OBJC_TAG_EXT_INDEX_SHIFT) |
             ((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT));
    }
}
复制代码

经过位操做把 value 和标志位拼起来,最终返回的就是 _NSCFNumber。很直观,不过

这个 OBJC_TAG_Last60BitPayload 和 OBJC_TAG_First52BitPayload 是什么?

LSB

Apple 在 Advances in Objective-C 的说明中,是将地址的最后一位置为 1,来与正常的指针区分开的。

How Tagged Pointer Works

这里要说明一下,为何正常的指针,最后一位会都是 0 呢?

OC 中的 [NSObject alloc] 最终调用的是 C 标准库中的 malloc,它所返回的地址是 16 的整数。16 在二进制下最后 4 位都是 0,用最后一位是否为 1 来识别 Tagged Pointer 很是合理。

实际试验一下,打开 Xcode 的 GuardMalloc,能够看到 log

GuardMalloc[debug-objc-38327]: Allocations will be placed on 16 byte boundaries.
复制代码

剩下的 3 位,能够用于区分不一样类型的 Tagged Pointer,好比 NSTaggedPointerNumber、NSTaggedPointerDate 等等。在 objc4-723 中,有

#if __has_feature(objc_fixed_enum) || __cplusplus >= 201103L
enum objc_tag_index_t : uint16_t
#else
typedef uint16_t objc_tag_index_t;
enum
#endif
{
    OBJC_TAG_NSAtom            = 0, 
    OBJC_TAG_1                 = 1, 
    OBJC_TAG_NSString          = 2, 
    OBJC_TAG_NSNumber          = 3, 
    OBJC_TAG_NSIndexPath       = 4, 
    OBJC_TAG_NSManagedObjectID = 5, 
    OBJC_TAG_NSDate            = 6, 
    OBJC_TAG_RESERVED_7        = 7, 

    // ...
};
复制代码

恰好 8 种。

MSB

目前,不论 是x86_64 仍是 ARM 64,都没有充分使用 64 位。就 ARM 64 而言,目前仅仅使用了后 48 位。 因此,Tagged Pointer 的标志位也是能够放在最前面的,即 MSB。

OC Runtime 实际上支持 4 种放置方式

#if __has_feature(objc_fixed_enum) || __cplusplus >= 201103L
enum objc_tag_index_t : uint16_t
#else
typedef uint16_t objc_tag_index_t;
enum
#endif
{
    // ...

    OBJC_TAG_First60BitPayload = 0, 
    OBJC_TAG_Last60BitPayload  = 6, 
    OBJC_TAG_First52BitPayload = 8, 
    OBJC_TAG_Last52BitPayload  = 263, 

    // ...
};
#if __has_feature(objc_fixed_enum) && !defined(__cplusplus)
typedef enum objc_tag_index_t objc_tag_index_t;
#endif
复制代码

相应地,mask 也不同

#if TARGET_OS_OSX && __x86_64__
    // 64-bit Mac - tag bit is LSB
    // macOS下,始终用最小位做为标志位
# define _OBJC_TAG_MASK 1UL
#else
    // Everything else - tag bit is MSB
    // 剩下的只有ARM64了。ARM64只用到64位中的48位,高位全是0,此时mask就要反过来。
    // 这种状况下,理论上可以支持的Tagged Pointer的类就多了不少。
# define _OBJC_TAG_MASK (1UL<<63)
#endif
复制代码

结论

  1. [NSNumber alloc] 返回 NSPlaceholderNumber
  2. [NSPlaceholderNumber initWithXXX:] 调用了 CFNumberCreate
  3. CFNumberCreate调用了 objc_debug_taggedpointer_obfuscator(或 _objc_makeTaggedPointer)
  4. _objc_makeTaggedPointer 根据运行设备的架构,拼接出一个地址并返回

顺带一提,__NSCFNumber 并非为 NSNumber 的Tagged Pointer 捏造出来的,而是实际存在的 NSNumber 类簇下的一个私有子类。只不过 OC Runtime 选择将它做为 NSNumber 的 Tagged Pointer 在 lldb 下的“画皮”。

能够这样来测试它的存在

Class class = NSClassFromString(@"__NSCFNumber");
复制代码

参考连接

objc explain: Non-pointer isa Let's Build Tagged Pointers

相关文章
相关标签/搜索