OC中的Tagged Pointer

简介:

  在objc4源码中,我们经常会在函数中看到Tagged PointerTagged Pointer究竟是何方神圣?有什么作用呢?本篇文章用于记录我对Tagged Pointer的理解。

Tagged Pointer主要为了解决两个问题:

  1. 内存资源浪费,堆区需要额外的开辟空间:
      例如NSNumber对象,其值是一个整数。正常情况下,如果这个整数只是一个NSInteger的普通变量,那么它所占用的内存是与CPU的位数有关,在32位CPU下占4个字节,在64位CPU下是占8个字节的。而指针类型的大小通常也是与CPU位数相关,一个指针所占用的内存在32位CPU下为 4 个字节,在64位CPU下也是8个字节。
      所以一个普通的 iOS 程序,如果没有Tagged Pointer对象,从 32 位机器迁移到64位机器中后,虽然逻辑没有任何变化,但这种 NSNumberNSDate 一类的对象所占用的内存会翻倍。

  2. 访问效率,每次set/get都需要访问堆区,浪费时间,而且需要管理堆区对象的声明周期,降低效率:
      为了改进上述提到的内存占用和效率问题,所以苹果提出了Tagged Pointer对象。对于某些占用内存很小的数据对象,不再单独开辟空间去存储,而是将实际的实例值存储在对象的指针中,同时对该指针进行标记,用于区分正常的指针指向!
      由于NSNumberNSDate类的变量本身的值需要占用的内存大小常常不需要8个字节,拿整数来说,4个字节所能表示的有符号整数就可以达到20多亿(注:2^31=2147483648,另外1位作为符号位),对于绝大多数情况都是可以处理的。所以苹果将一个对象的指针拆成两部分,一部分直接保存数据(即下面所说的Data部分),另一部分作为特殊标记(即下面所说的Tag部分),表示这是一个特别的指针,不指向任何一个对象的地址。

Tagged Pointer内存管理:

普通类内存管理:
  总空间 = 栈指针空间(isa普通类指针) + 堆中分配的空间
  指针变量指向堆分配的内存空间,需要动态分配内存,进行引用计数机制,控制对象堆内存的管理。
Tagged Pointe内存管理:
  总空间 = 栈指针空间(Tagged Pointer)
  栈指针空间 = 对象的特殊标记(Tag) + 对象的值(Data)
  对象的特殊标记(Tag),总共占有四个二进制位;在ARM64架构中最高位的二进制四位用于区分Tagged Pointer和普通指针的区别,其它三位用于区分NSNumberNSDateNSString等对象类型
  Data为对象的值
  指针变量包含值,不用进行引用计数机制对内存管理,所以不需要retain,release操作。

Tagged Pointer总结:

  1. Tagged Pointer指向的并不是一个类,它是适用于64位处理器的一个内存优化机制,专门用来存储小对象,当存储不下时,则转为对象,例如NSNumberNSDateNSString
  2. 指针的值不再是地址了,而是包含真正值且经过特殊处理过的指针。所以,实际上它不再是一个对象了,它只是一个披着对象外衣的普通变量而已。它的内存并不存储在堆中,不需要动态分配内存、维护引用计数、管理它的生命周期等
    也不需要方法调用时执行objc_msgSend流程(消息发送、动态方法解析、消息转发);
  3. 在内存读取上有着3倍的效率,创建时比以前快106倍;
  4. Tagged Pointer指针指向的不在是一个类,所以无法直接访问isa指针。

Tagged Pointer原理和底层实现:

1.Xcode设置环境变量

  Xcode默认情况下对Tagged Pointer进行了混淆;设置环境变量OBJC_DISABLE_TAG_OBFUSCATIONYES, 可以关闭 Tagged Pointer的数据混淆。
设置环境变量的步骤:
Edit Scheme -> Run Debug -> Arguments -> Environment Variables -> + -> OBJC_DISABLE_TAG_OBFUSCATION -> YES

代码如下:

- (void)taggedPointerNumber {
    NSNumber *number1 = @(0x1);
    NSNumber *number2 = @(0x20);
    NSNumber *number3 = @(0x3F);
    NSNumber *number4 = @(0xFFFFFFFFFFEFE);
    NSNumber *maxNum = @(MAXFLOAT);
    
    // 使用了Tagged Pointer,NSNumber对象的值直接存储在了指针上,不会在堆上申请内存。则使用一个NSNumber对象只需要指针的 8 个字节内存就够了,大大的节省了内存占用。
//    NSLog(@"%zd", malloc_size(number1);
    NSInteger number1Size = malloc_size((__bridge const void *)(number1));
    NSLog(@"占用堆内存=%zd", number1Size);
    NSLog(@"指针内存=%zd", sizeof(number1));
    // NSNumber普通对象,会在堆上申请内存
    NSInteger maxNumSize = malloc_size((__bridge const void *)(maxNum));
    NSLog(@"占用堆内存=%zd", maxNumSize);
    NSLog(@"指针内存=%zd", sizeof(maxNum));
    
    NSLog(@"%p %@ %@", number1, number1, number1.class);
    NSLog(@"%p %@ %@", number2, number2, number2.class);
    NSLog(@"%p %@ %@", number3, number3, number3.class);
    NSLog(@"%p %@ %@", number4, number4, number4.class);
    NSLog(@"%p %@ %@", maxNum, maxNum, maxNum.class);
}
/// ARM64开启混淆打印:
占用堆内存=0
指针内存=8
占用堆内存=32
指针内存=8
0x8b33564c2d3526b5 1 __NSCFNumber
0x8b33564c2d3524a5 32 __NSCFNumber
0x8b33564c2d352555 63 __NSCFNumber
0x8bcca9b3d2cac944 4503599627370238 __NSCFNumber
0x282dc8760 3.402823e+38 __NSCFNumber

/// ARM64关闭混淆打印:
占用堆内存=0
指针内存=8
占用堆内存=32
指针内存=8
0xb000000000000012 1 __NSCFNumber
0xb000000000000202 32 __NSCFNumber
0xb0000000000003f2 63 __NSCFNumber
0xb0ffffffffffefe3 4503599627370238 __NSCFNumber
0x281a2afe0 3.402823e+38 __NSCFNumber

1.内存
  number1只有栈上的指针内存;而maxNum不仅有指针内存,在堆中还分配了32字节的内存用于存储该变量的值。
2.指针

变量 指针值 10进制数值
number1 0xb000000000000012 1
number2 0xb000000000000202 32
number3 0xb0000000000003f2 63
number4 0xb0ffffffffffefe3 4503599627370238
maxNum 0x281a2afe0 3.402823e+38

  通过观察发现,对象的number1number2number3number4都存储在了对应的指针中;而maxNum不同由于数据过大,导致无法 1 个指针 8 个字节的内存根本存不下,而申请了32字节堆内存。
3.Tagged Pointerisa

isa和Tagged Pointer.png

  打断点,从上图可以看出,number1number2number3number4的isa指向了0x0(即nil),是Tagged Pointer指针;maxNum指向了NSNumber类的isa指针。
4.Tagged Pointer位解析
  以number1Tagged Pointer指针为例:

高位    <--     低位
0xb000000000000012

4.1最高位解析:
  0x为16进制标识符,在16进制中,一位数字代表二进制中的四位;
ARM64架构下,Tagged Pointer的标识为二进制的最高位的四位,也就是16进制表示的从左向右的第一位:

/// 16进制的第一位,也是最高位
b
/// 16进制的b转换为二进制的四位,也是指针二进制最高四位
1011

  其中二进制中最高位是Tagged Pointer标识位,如例子中的的1,表示该指针是Tagged Pointer的指针;其它三位表示支持Tagged Pointer的类标识位,如例子中011,转化为10进制就是3,3在支持Tagged Pointer的系统类数组中代表NSNumber类。
runtime源码objc-internal.h中有关支持Tagged Pointer类的标志定义如下:

         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,

4.2末尾解析:
  Tagged Pointer16进制的末尾也就是二进制的低位的四位,表示的Tagged Pointer存储数据的类型标识符,例如number1末尾的2表示Tagged Pointer存储的是int的数据。

数据类型 标识符
char 0
short 1
int 2
long int 3
float 4
double 5

  除去高位和低位的标识位,中间这一部分才是真正存储值的区域。
注意:

  1. NSString类型的Tagged Pointer指针与基本类型的指针是不一样的,末尾的数字为字符串的长度;
  2. NSString类型的Tagged Pointer指针存储char类型,返回的是ASCII码(该值为16进制的,需要进行十进制转换)
- (void)taggedPointerString {
    NSMutableString *mutableStr = [NSMutableString string];
        NSString *immutable = nil;
        #define _OBJC_TAG_MASK (1UL<<63)
        char c = 'a';
        do {
            [mutableStr appendFormat:@"%c", c++];
            immutable = [mutableStr copy];
            NSLog(@"%p %@ %@", immutable, immutable, immutable.class);
        }while(((uintptr_t)immutable & _OBJC_TAG_MASK) == _OBJC_TAG_MASK);
}

打印信息:

0xa000000000000611 a NSTaggedPointerString
0xa000000000062612 ab NSTaggedPointerString
0xa000000006362613 abc NSTaggedPointerString
0xa000000646362614 abcd NSTaggedPointerString
0xa000065646362615 abcde NSTaggedPointerString
0xa006665646362616 abcdef NSTaggedPointerString
0xa676665646362617 abcdefg NSTaggedPointerString
0xa0022038a0116958 abcdefgh NSTaggedPointerString
0xa0880e28045a5419 abcdefghi NSTaggedPointerString
0x280e3cb40 abcdefghij __NSCFString

Tagged Pointer标识位、类标识、数据类型做代码验证

1.Tagged Pointer标识位

  Tagged Pointer标识位即判断是否是Tagged Pointer指针的表示方法。该篇文章源码版本为objc4-818.2。
  在源码objc_internal.h中可以找到判断Tagged Pointer标识位的方法,如下代码:

static inline bool 
_objc_isTaggedPointer(const void * _Nullable ptr)
{
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

  上面的代码将指针ptr_OBJC_TAG_MASK掩码进行位与操作。这个掩码_OBJC_TAG_MASK的源码同样在objc_internal.h中可以找到:

#if (TARGET_OS_OSX || TARGET_OS_MACCATALYST) && __x86_64__
    // 64-bit Mac - tag bit is LSB
#   define OBJC_MSB_TAGGED_POINTERS 0
#else
    // Everything else - tag bit is MSB
#   define OBJC_MSB_TAGGED_POINTERS 1
#endif

#if OBJC_SPLIT_TAGGED_POINTERS
#   define _OBJC_TAG_MASK (1UL<<63)
#elif OBJC_MSB_TAGGED_POINTERS
#   define _OBJC_TAG_MASK (1UL<<63)
#else
#   define _OBJC_TAG_MASK 1UL

根据源码得知:
MacOS(x86_64和ARM64 M芯片)下采用 LSB(Least Significant Bit,即最低有效位)为Tagged Pointer标识位;(define _OBJC_TAG_MASK 1UL)
iOS(ARM64 A芯片)下则采用 MSB(Most Significant Bit,即最高有效位)为Tagged Pointer标识位。(define _OBJC_TAG_MASK (1UL<<63))

2. Tagged Pointer类标识

在源码objc_internal.h中可以查看到NSNumber、NSDate、NSString等类的标识位,如下:

    // 60-bit payloads
    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,

    // 60-bit reserved
    OBJC_TAG_RESERVED_7        = 7, 
3. Tagged Pointer数据类型

Tagged Pointer16进制的最后一位(即2进制的最后四位)表示数据类型,例子如上述代码。见4.2末尾解析

参考链接:

iOS内存管理之Tagged Pointer

你可能感兴趣的:(OC中的Tagged Pointer)