TL.DR

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

从代码开始

没有isa

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

可以看到
NSNumbe

n并没有isa,它确实不是一个OC的对象。

NSPlaceholderNumber

进一步来看

int main(int argc, const char * argv[])
{
    // 1
    NSNumber *n = [NSNumber alloc];
    
    // 2
    n = [n initWithInt:3];
    
    return 0;
}

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

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

一个直观的猜测是,多次alloc返回的NSPlaceholderNumber是同一个实例。

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_cpv和_NSPlaceholderValueOrNumber_cpn来做到始终返回一个NSPlaceholderNumber实例的。

既然有两个全局变量,说明会有两个实例喽?
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;
}

的确如此
确实不一样

_NSCFNumber,最终的Tagged Pointer

NSPlaceholderNumber本身乏善可陈[[NSPlaceholderNumber initWithInt:]](https://github.com/NSFish/Pri...

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

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

How Tagged Pointers Work

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

OC中的[NSObject alloc]最终调用的是C标准库中的malloc,它所返回的地址通是16的整数

16在二进制下最后4位都是0,用最后一位是否为1来识别Tagged Pointer非常合理。

实际试验一下,打开Xcode的GuardMalloc,可以看到Console的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。

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类簇下的一个私有子类。只不过runtime选择将它作为NSNumber的Tagged Pointer在lldb下的“画皮”。

可以这样来测试它的存在

Class class = NSClassFromString(@"__NSCFNumber");

参考链接

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


NSFish
716 声望23 粉丝

只要去做,事情就会一件一件地被完成


引用和评论

0 条评论