美文网首页
[iOS] 内存管理-Tagged Pointer

[iOS] 内存管理-Tagged Pointer

作者: 沉江小鱼 | 来源:发表于2021-04-10 19:16 被阅读0次

    前言:在之前分析 isa结构,还有引用计数的管理时,会遇到 isTaggedPointer的判断,这里就来学习一下Tagged Pointer到底是个啥?

    1. Tagged Pointer介绍

    1.1 为什么要使用 Tagged Pointer

    2013 年,苹果推出了iPhone 5s,配备了首个采用64 位架构A7 双核处理器,指针变量的内存占用由32 位(4字节)变成了 64 位(8字节)OC 对象都是存储在上面的,并且在栈上有一个指针指向了堆的内存地址,对于一些变量,比如@(1)@"abc",如果同时在栈和堆上都取分配内存空间的话,就很浪费了,另外还要维护它的引用计数,管理它的生命周期,这些都给程序增加了额外的逻辑,影响运行效率。

    为了改进上面上面所提出的问题,苹果提出了Tagged Pointer对象。由于 NSNumber、NSDate、NSString一类的变量本身的值占用的内存大小多数情况下不需要 8 个字节,就比如整数,4 个字节所能表示的有符号整数就可以达到2147483648,对于绝大多数情况都是可以处理的。

    1.2 Tagged Pointer到底是是什么?

    所以苹果将一个对象的指针拆成了两部分:本身数据 + 特殊标记,表示这是一个特别的指针,不指向任何一个地址,这个特别的指针就是 Tagged Pointer,如下图所示:

    image.jpeg

    由于Tagged Pointer 不指向任何一个地址,所以在对象进行retain或者release操作时,都会判断isTaggedPointer,如果为true,则不进行操作,也就是说我们并需要去管理它的生命周期,如下:

    __attribute__((aligned(16), flatten, noinline))
    id 
    objc_retain(id obj)
    {
        if (!obj) return obj;
        if (obj->isTaggedPointer()) return obj;
        return obj->retain();
    }
    
    
    __attribute__((aligned(16), flatten, noinline))
    void 
    objc_release(id obj)
    {
        if (!obj) return;
        if (obj->isTaggedPointer()) return;
        return obj->release();
    }
    

    2. 判断是否为Tagged Pointer

    我们运行下面的代码,去看下 Tagged Pointer 指针地址的含义:

    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
        
        NSNumber *number1 = @1;
        NSLog(@"number1 %p %@", number1, [number1 class]);
        NSNumber *number2 = @2;
        NSLog(@"number2 %p %@", number2, [number2 class]);
    
        NSString *a = [[@"a" mutableCopy] copy];
        NSLog(@"a %p %@", a, [a class]);
        NSString *ab = [[@"ab" mutableCopy] copy];
        NSLog(@"ab %p %@", ab, [ab class]);
    }
    

    如果在之前,输出的结果会如下所示:

    number1 0xb000000000000012 __NSCFNumber
    number2 0xb000000000000022 __NSCFNumber
    a 0xa000000000000611 NSTaggedPointerString
    ab 0xa000000000062612 NSTaggedPointerString
    

    去除前后的标识,剩下的中间值就是我们设置的变量的值。

    但是实际上,输出如下:

    number1 0xe629750fa0a06e3c __NSCFNumber
    number2 0xe629750fa0a06e0c __NSCFNumber
    a 0xf629750fa0a0683f NSTaggedPointerString
    ab 0xf629750fa0a6483c NSTaggedPointerString
    

    我们可以看到字符串的类型仍然为NSTaggedPointerString,但是对象地址看起来跟正常的一样了,这是因为在某个时间结点(不太清楚啥时候,但不重要)之后,对于 TaggedPointer 地址进行了混淆操作,那我们可以通过查看isTaggedPointer方法实现,去判断一个针是否为TaggedPointer,源码如下:

    #if (TARGET_OS_OSX || TARGET_OS_IOSMAC) && __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_MSB_TAGGED_POINTERS
    #   define _OBJC_TAG_MASK (1UL<<63)
    #else
    #   define _OBJC_TAG_MASK 1UL
    #endif
    
    static inline bool 
    _objc_isTaggedPointer(const void * _Nullable ptr)
    {
        return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
    }
    

    从上面可以看到判断是否Tagged Pointer,仅仅是将地址和_OBJC_TAG_MASK进行一个与运算,将其结果和_OBJC_TAG_MASK进行比对。_OBJC_TAG_MASK在Mac OS下为1UL,其他平台下为1UL<<63

    这个方法其实相当于判断地址最高位是否为 1,为 1 则说明是Tagged Pointer,为啥是判断地址最高位是否为 1?需要去学习一下&与操作了。

    我们将上面打印出的第一个字符串的地址:0xf629750fa0a0683f,转成二进制:

    image.png

    可以看到最高位为 1,说明确实是Tagged Pointer

    我们将 NSNumber 对象的地址转成 2 进制之后,首位也是 1,说明也是Tagged Pointer,但是打印出的类型不像字符串那样NSTaggedPointerString直接能看出来是Tagged Pointer,可能是苹果并没有为 NSNumber 类设计一个单独的 class 来表示Tagged Pointer

    3. Tagged Pointer类型区分

    上面我们知道了如何判断一个对象指针是否为Tagged Pointer,但是NSNumber 、NSString 都有可能是Tagged Pointer,怎么去区分其类型呢?我们需要获取到混淆之前的地址,可以在源码中通过objc_debug_taggedpointer_obfuscator查找TaggedPointer的编码和解码,来查看底层是如何混淆处理的:

    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;
    }
    

    我们看到在编码时是将指针地址和objc_debug_taggedpointer_obfuscator进行了一次异或,objc_debug_taggedpointer_obfuscator的赋值方法,如下:

    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));
            // #   define _OBJC_TAG_MASK (1UL<<63) 就是判断时 & 的 _OBJC_TAG_MASK
            objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
        }
    }
    

    为了获取小对象的真实地址,我们可以将解码的源码拷贝到外面,将混淆之后的地址进行解码,如下所示:


    image.jpeg

    观察解码后的小对象地址,其中的62表示bASCII码,再以NSNumber为例,同样可以看出,1就是我们实际的值:

    image.png

    到这里,我们验证了小对象指针地址中确实存储了值,那么小对象地址高位其中的0xa、0xb又是什么含义呢?

    //NSString
    0xa000000000000621
    
    //NSNumber
    0xb000000000000012
    0xb000000000000025
    

    0xa转换成二进制为 1 010(64为为1,63~61后三位表示 tagType类型 - 2),表示NSString类型

    0xb 转换为二进制为 1 011(64为为1,63~61后三位表示 tagType类型 - 3),表示NSNumber类型,这里需要注意一点,如果NSNumber的值是-1,其地址中的值是用补码表示的。

    这里可以通过_objc_makeTaggedPointer方法的参数tag类型objc_tag_index_t进入其枚举,其中 2表示NSString,3表示NSNumber

    image.png

    4. 总结

    • Tagged Pointer 其实是地址 + 值,并不是真正的对象,只是一个披着对象皮的普通变量。可以直接进行读取,占用空间小,节省内存;
    • Tagged Pointerretainrelease时,都直接返回了,不需要管理其生命周期;
    • Tagged Pointer 并不会存储在堆中,而是在常量区,也不需要malloc和free,所以可以直接读取,相比存储在堆区的数据读取,效率上快了3倍左右,创建的效率相比堆区快了近100倍左右;
    • Tagged Pointer的64位地址中,前4位代表类型,后4位主要适用于系统做一些处理,中间56位用于存储值
    • 对于NSString来说,当字符串较小时,建议直接通过@""初始化,因为存储在常量区,可以直接进行读取,会比WithFormat初始化方式更加快速。

    相关文章

      网友评论

          本文标题:[iOS] 内存管理-Tagged Pointer

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