• 0

  • 453

iOS底层小记一:关于Tagged Pointer

机器猫

机器学习

2星期前

背景

  • 64bits开始, iOS引入了Tagged Pointer技术,以此优化NSNumber、NSString、NSDate等小对象的存储。

好处

  • 在引入Tagged Pointer技术之前、NSNumber等对象的存储需要动态分配内存、维护引用计数等,NSNumber的指针存储的是堆中NSNumber对象的地址值。

    也就是说,仅仅只是为了存储一个1的整数值,就需要去堆区动态开辟内存,以及引用计数来管理这块内存,总共花费24个字节不觉得这很滑稽、很浪费么?

于是乎,苹果爸爸一拍脑袋,把NSNumber(NSString、NSDate)的数据直接放进了指针里!以此避免了没必要的内存开销,以及内存管理的花销。当指针不够存储数据的时候、才会动态分配内存去存储数据。

Tagged Pointer = Tag + Data(地址 + 数据)

举个🌰

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSNumber *number1 = @1;
        NSNumber *number2 = @2;
        NSNumber *number3 = @999999999999999999;
        
        NSLog(@"number1 = %p", number1);
        NSLog(@"number2 = %p", number2);
        NSLog(@"number3 = %p", number3);
    }

    return 0;
}

2020-09-29 17:58:57.252080+0800 MemoryTest[95240:2528802] number1 = 0xdfc933f566855f8e
2020-09-29 17:58:57.252517+0800 MemoryTest[95240:2528802] number2 = 0xdfc933f566855fbe
2020-09-29 17:58:57.252578+0800 MemoryTest[95240:2528802] number3 = 0x6000039f0080
复制代码

0xdfc933f566855f8e0xdfc933f566855fbe可见是一个栈区的地址(因为地址比较大), 0x6000039f0080是一个堆区地址,number3 = 999999999999999999超出了8个字节承载的范围,故而成了一个普通的指针。

  • WWDC2013 的《Session 404 Advanced in Objective-C》视频中,看到苹果对于Tagged Pointer特点的介绍:

  • Tagged Pointer专门用来存储小的对象,例如NSNumber和NSDate Tagged Pointer指针的值不再是地址了,而是真正的值。

  • 实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要 malloc 和 free。

  • 在内存读取上有着 3 倍的效率,创建时比以前快 106 倍。

由此可见,苹果引入Tagged Pointer,不但减少了 64 位机器下程序的内存占用,还提高了运行效率。完美地解决了小内存对象在存储和访问效率上的问题。

窥探源码

  • 如何判断一个指针是不是Tagged Pointer

runtime的源码objc-internal.h

objc_object::isTaggedPointer() 
{
    return _objc_isTaggedPointer(this);
}

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

我们注意到这个掩码_OBJC_TAG_MASK,找到它的定义:

#if (TARGET_OS_OSX || TARGET_OS_IOSMAC) && __x86_64__ // iOS系统下
   // 64-bit Mac - tag bit is LSB
#   define OBJC_MSB_TAGGED_POINTERS 0
#else // macOS系统下
   // Everything else - tag bit is MSB
#   define OBJC_MSB_TAGGED_POINTERS 1
#endif

#if OBJC_MSB_TAGGED_POINTERS
#   define _OBJC_TAG_MASK (1UL<<63)
#   define _OBJC_TAG_INDEX_SHIFT 60
#   define _OBJC_TAG_SLOT_SHIFT 60
#   define _OBJC_TAG_PAYLOAD_LSHIFT 4
#   define _OBJC_TAG_PAYLOAD_RSHIFT 4
#   define _OBJC_TAG_EXT_MASK (0xfUL<<60)
#   define _OBJC_TAG_EXT_INDEX_SHIFT 52
#   define _OBJC_TAG_EXT_SLOT_SHIFT 52
#   define _OBJC_TAG_EXT_PAYLOAD_LSHIFT 12
#   define _OBJC_TAG_EXT_PAYLOAD_RSHIFT 12
#else
#   define _OBJC_TAG_MASK 1UL
#   define _OBJC_TAG_INDEX_SHIFT 1
#   define _OBJC_TAG_SLOT_SHIFT 0
#   define _OBJC_TAG_PAYLOAD_LSHIFT 0
#   define _OBJC_TAG_PAYLOAD_RSHIFT 4
#   define _OBJC_TAG_EXT_MASK 0xfUL
#   define _OBJC_TAG_EXT_INDEX_SHIFT 4
#   define _OBJC_TAG_EXT_SLOT_SHIFT 4
#   define _OBJC_TAG_EXT_PAYLOAD_LSHIFT 0
#   define _OBJC_TAG_EXT_PAYLOAD_RSHIFT 12
#endif
复制代码

我们注意到在不同的系统下,掩码_OBJC_TAG_MASK的定义有所不同:

在iOS系统下(__x86_64__),_OBJC_TAG_MASK1UL<<63,等于是8字节地址的最高有效位1;

在macOS系统下,_OBJC_TAG_MASK1UL,等于是8字节地址的最低有效位1;

((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK; 做一次位运算,取到对应位上的数据,是否等于掩码,以此判断是否为Tagged Pointer`。

  • 无处不在的Tagged Pointer 随意摘几处runtime的源码:

判断是否是类对象:

// objc-object.h
objc_object::isClass()
{
    if (isTaggedPointer()) return false;
    return ISA()->isMetaClass();
}
复制代码

: 所以Tagged Pointer不再是个对象

inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // fixme necessary?

    if (fastpath(isa.nonpointer  &&  
                 !isa.weakly_referenced  &&  
                 !isa.has_assoc  &&  
                 !isa.has_cxx_dtor  &&  
                 !isa.has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        object_dispose((id)this);
    }
}
复制代码

: 当一个对象释放的时候,先判断是否为Tagged Pointer,因为Tagged Pointer不需要程序员去管理内存,系统帮你回收(栈区内存归系统管理,不多说)。

inline id 
objc_object::retain()
{
    ASSERT(!isTaggedPointer());

    if (fastpath(!ISA()->hasCustomRR())) {
        return rootRetain();
    }

    return ((id(*)(objc_object *, SEL))objc_msgSend)(this, @selector(retain));
}

objc_release(id obj)
{
    if (!obj) return;
    if (obj->isTaggedPointer()) return;
    return obj->release();
}
复制代码

: Tagged Pointer无法被强引用,也没有引用计数的说法。

参考链接

www.mikeash.com/pyblog/frid…

免责声明:文章版权归原作者所有,其内容与观点不代表Unitimes立场,亦不构成任何投资意见或建议。

机器学习

453

相关文章推荐

未登录头像

暂无评论