背景
- 从
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
0xdfc933f566855f8e和0xdfc933f566855fbe可见是一个栈区的地址(因为地址比较大),
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_MASK是1UL<<63,等于是8字节地址的最高有效位1;
在macOS系统下,
_OBJC_TAG_MASK是1UL,等于是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无法被强引用,也没有引用计数的说法。