美文网首页
Objective-C 对象的内存管理-Tagged Point

Objective-C 对象的内存管理-Tagged Point

作者: _涼城 | 来源:发表于2022-04-02 14:43 被阅读0次

    前言

        在 2013 年 9 月,苹果推出了 iPhone5s,与此同时,iPhone5s 配备了首个采用 64 位架构的 A7 双核处理器,在这之前通常创建对象,对象存储在堆中,对象的指针存储在栈中。要找到这个对象,就需要先在栈中,找到指针,然后通过指针找到堆中的对象。一个普通的 iOS 程序,从 32 位机器迁移到 64 位机器中后,虽然逻辑没有任何变化,但这种 NSNumberNSDate 一类的对象所占用的内存会翻倍。如下图所示:

    tagged_pointer_before.jpg

        为了存储和访问一个 NSNumber 对象,需要在堆上为其分配内存,另外还要维护它的引用计数,管理它的生命期。这些都给程序增加了额外的逻辑,造成运行效率上的损失。

    什么是 Tagged Pointer

        为了改进上面提到的内存占用和效率问题,苹果提出了 Tagged Pointer 对象。由于 NSNumberNSDate 一类的变量本身的值需要占用的内存大小常常不需要 8 个字节,拿整数来说,4 个字节所能表示的有符号整数就可以达到 20 多亿(注:2^{31}=2147483648,另外 1 位作为符号位),对于绝大多数情况都是可以处理的。

        所以 Tagged Pointer 就是将一个对象的指针拆成两部分,一部分直接保存数据,另一部分作为特殊标记,表示这是一个特别的指针,不指向任何一个地址。所以,引入了 Tagged Pointer 对象之后,64 位 CPUNSNumber 的内存图变成了以下这样:

    tagged_pointer_after.jpg

    Tagged Pointer 代码

    对此,可以通过实验代码验证:

        NSNumber *number1 = @1;
        NSNumber *number2 = @2;
        NSNumber *number3 = @3;
        NSNumber *numberFFFF = @(0xFFFF);
        NSString *string1 = [NSString stringWithFormat:@"1"];
    
        NSLog(@"number1 pointer is %p", number1);
        NSLog(@"number2 pointer is %p", number2);
        NSLog(@"number3 pointer is %p", number3);
        NSLog(@"numberffff pointer is %p", numberFFFF);
        NSLog(@"string1 pointer is %p", string1);
    
        NSNumber *bigNumber = @(0xEFFFFFFFFFFFFFFF);
        NSString *bigString = [NSString stringWithFormat:@"12345678901"] ;
        NSLog(@"bigNumber pointer is %p", bigNumber);
        NSLog(@"bigString pointer is %p", bigString);
    

    输出结果如下:

    number1 pointer is 0xf49bc6f4ee1ca35e
    number2 pointer is 0xf49bc6f4ee1ca36e
    number3 pointer is 0xf49bc6f4ee1ca37e
    numberffff pointer is 0xf49bc6f4ee135cbe
    string1 pointer is 0xe49bc6f4ee1ca05d
    bigNumber pointer is 0x6000000bb140
    bigString pointer is 0x6000000bb160
    

    可以发现,Tagged Pointer 的指针地址与大容量数据的指针地址不同。

    Tagged Pointer 混淆

    objc 源码中可以找到下面这样一段代码在 iOS 12.0 以后,用于混淆 TaggedPointer:

    static void
    initializeTaggedPointerObfuscator(void)
    {
        if (sdkIsOlderThan(10_14, 12_0, 12_0, 5_0, 3_0) ||
            // Set the obfuscator to zero for apps linked against older SDKs,
            // in case they're relying on the tagged pointer representation.
            DisableTaggedPointerObfuscation) {
            objc_debug_taggedpointer_obfuscator = 0;
        } else {
            // Pull random data into the variable, then shift away all non-payload bits.
            arc4random_buf(&objc_debug_taggedpointer_obfuscator,
                           sizeof(objc_debug_taggedpointer_obfuscator));
            objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
        }
    }
    

    通过 objc_debug_taggedpointer_obfuscator 查看底层是如何混淆处理的

    • 编码

      static inline void *_Nonnull
      _objc_encodeTaggedPointer(uintptr_t ptr)
      {
      return (void*)(objc_debug_taggedpointer_obfuscator ^ ptr);
      }
      
    • 解码

      static inline uintptr_t
      _objc_decodeTaggedPointer(const void *_Nullable ptr)
      {
          return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
      }
      

    Tagged Pointer 指针信息

        在 TaggedPointer 指针中,**最高位为标志位,61 - 63 位为类型信息,中间存储着实际信息,最后一位为标志位。

    NSTaggedPointerString 类型中最后 4 位存储字符串长度**,字符串存储的是 ascii

    通过 _objc_decodeTaggedPointer 函数对 TaggedPointer 进行解码,可以得到实际的指针地址进行查看:

    extern uintptr_t objc_debug_taggedpointer_obfuscator;
    
    uintptr_t
    _objc_decodeTaggedPointer_(id ptr)
    {
        return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
    }
    int main(){
         
         NSString *string1 = [NSString stringWithFormat:@"1"];
         NSNumber *number1 = @1;
         NSLog(@"really string1 pointer is 0x%lx", _objc_decodeTaggedPointer_(string1));
         NSLog(@"really number1 pointer is 0x%lx", _objc_decodeTaggedPointer_(number1));
    }
    
    

    输出结构如下:

    really string1 pointer is 0xa000000000000311
    really number1 pointer is 0xb000000000000012
    

    通过上面的输出结果,我们会发现,字符串与数字的指针二进制前四位不一致,16 进制分别为 0xa0xb,转换为二进制分别为 10101011

    TaggedPointer 标志位

    objc 源码中搜索 TaggedPointer,可以找到下面这样一段代码用于判断是否为 TaggedPointer:

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

    判断一个对象类型是否为 TaggedPointer 类型实际上是将指针地址与 _OBJC_TAG_MASK 进行按位与操作,结果在跟 _OBJC_TAG_MASK 进行对比,在看下_OBJC_TAG_MASK 的定义:

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

    _OBJC_TAG_MASK 表明如果 64位数据中,最高位是 1 的话,则表明当前是一个 tagged pointer 类型。

    注意:TaggedPointer类型在iOS和MacOS中标志位是不同的iOS为最高位而MacOS为最低位

    Tagged Pointer 类型

    可以在 objc 源码中找到类型枚举 objc_tag_index_t如下:

    {
        // 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, 
    
        // 52-bit payloads
        OBJC_TAG_Photos_1          = 8,
        OBJC_TAG_Photos_2          = 9,
        OBJC_TAG_Photos_3          = 10,
        OBJC_TAG_Photos_4          = 11,
        OBJC_TAG_XPC_1             = 12,
        OBJC_TAG_XPC_2             = 13,
        OBJC_TAG_XPC_3             = 14,
        OBJC_TAG_XPC_4             = 15,
        OBJC_TAG_NSColor           = 16,
        OBJC_TAG_UIColor           = 17,
        OBJC_TAG_CGColor           = 18,
        OBJC_TAG_NSIndexSet        = 19,
        OBJC_TAG_NSMethodSignature = 20,
        OBJC_TAG_UTTypeRecord      = 21,
    
        OBJC_TAG_First60BitPayload = 0, 
        OBJC_TAG_Last60BitPayload  = 6, 
        OBJC_TAG_First52BitPayload = 8, 
        OBJC_TAG_Last52BitPayload  = 263, 
    
        OBJC_TAG_RESERVED_264      = 264
    };
    

    Tagged Pointer 特点

        苹果对于 TaggedPointer 特点的介绍:

    • TaggedPointer 专门用来存储小的对象,例如 NSNumberNSDate
    • TaggedPointer 指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要 malloc 和 free,也就不需要引用计数。
    • 在内存读取上有着 3 倍的效率,创建时比以前快 106 倍。
      由此可见,苹果引入 TaggedPointer,不但减少了 64 位机器下程序的内存占用,还提高了运行效率。完美地解决了小内存对象在存储和访问效率上的问题。

    补充: TaggedPointer 不是真正的对象,因此没有 isa

    相关文章

      网友评论

          本文标题:Objective-C 对象的内存管理-Tagged Point

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