美文网首页
OC中的Tagged Pointer

OC中的Tagged Pointer

作者: 苍眸之宝宝 | 来源:发表于2022-03-10 17:04 被阅读0次

    简介:

      在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

          本文链接:https://www.haomeiwen.com/subject/lhkkrrtx.html