iOS开发者对引用计数
这个名词肯定不陌生,引用计数是苹果为了方便开发者管理内存而引入的一个概念,当引用计数为0时,对象就会被释放。但是,真的是所有对象都是这样吗?
内存分配 iOS将虚拟内存按照地址由低到高划分为如下五个区:
在程序运行时,代码区,常量区以及全局静态区的大小是固定的,会变化的只有栈和堆的大小。而栈的内存是有操作系统自动释放的,我们平常说所的iOS内存引用计数,其实是就堆上的对象来说的。
如何引入tagged pointer 自2013年苹果推出iphone5s之后,iOS的寻址空间扩大到了64位。我们可以用63位来表示一个数字(一位做符号位)。那么这个数字的范围是2^63 ,很明显我们一般不会用到这么大的数字,那么在我们定义一个数字时NSNumber *num = @100
,实际上内存中浪费了很多的内存空间。
当然苹果肯定也认识到了这个问题,于是就引入了tagged pointer
,tagged pointer
是一种特殊的“指针”,其特殊在于,其实它存储的并不是地址,而是真实的数据和一些附加的信息。
我们可以在WWDC2013的《Session 404 Advanced in Objective-C》视频中,看到苹果对于Tagged Pointer特点的介绍:
Tagged Pointer专门用来存储小的对象,例如NSNumber, NSDate, NSString。
Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free。
在内存读取上有着3倍的效率,创建时比以前快106倍。
NSTaggedPointer 我们先看下下面这段代码:
1 2 3 4 5 6 7 8 9 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);
运行结果:
1 2 3 4 5 6 7 8 9 10 2020-08-08 14:15:54.480862+0800 TaggedPointerDemo[55468:2078125] 0xdc5050684e86e57c a NSTaggedPointerString 2020-08-08 14:15:54.481719+0800 TaggedPointerDemo[55468:2078125] 0xdc5050684e80c57f ab NSTaggedPointerString 2020-08-08 14:15:54.482480+0800 TaggedPointerDemo[55468:2078125] 0xdc50506848b0c57e abc NSTaggedPointerString 2020-08-08 14:15:54.483342+0800 TaggedPointerDemo[55468:2078125] 0xdc50506e08b0c579 abcd NSTaggedPointerString 2020-08-08 14:15:54.483950+0800 TaggedPointerDemo[55468:2078125] 0xdc50563e08b0c578 abcde NSTaggedPointerString 2020-08-08 14:15:54.484246+0800 TaggedPointerDemo[55468:2078125] 0xdc56363e08b0c57b abcdef NSTaggedPointerString 2020-08-08 14:15:54.484800+0800 TaggedPointerDemo[55468:2078125] 0xda26363e08b0c57a abcdefg NSTaggedPointerString 2020-08-08 14:15:54.485200+0800 TaggedPointerDemo[55468:2078125] 0xdc527050ee978a35 abcdefgh NSTaggedPointerString 2020-08-08 14:15:54.485644+0800 TaggedPointerDemo[55468:2078125] 0xdcd85e404adcb774 abcdefghi NSTaggedPointerString 2020-08-08 14:15:54.486003+0800 TaggedPointerDemo[55468:2078125] 0x28334c2c0 abcdefghij __NSCFString
上图我们可以看到,当字符串的长度为10个以内时,字符串的类型都是NSTaggedPointerString
类型,当超过10个时,字符串的类型才是__NSCFString
打印结果分析:
NSTaggedPointer标志位 1 2 3 4 5 static inline bool _objc_isTaggedPointer(const void * _Nullable ptr) { return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK; }
上面这个方法我们看到,判断一个对象类型是否为NSTaggedPointerString
类型实际上是讲对象的地址与_OBJC_TAG_MASK
进行按位与操作,结果在跟_OBJC_TAG_MASK
进行对比,我们在看下_OBJC_TAG_MASK
的定义:
1 2 3 4 5 #if OBJC_MSB_TAGGED_POINTERS # define _OBJC_TAG_MASK (1UL<<63) #else # define _OBJC_TAG_MASK 1UL #endif
我们都知道一个对象地址为64位二进制,它表明如果64位数据中,最高位是1的话,则表明当前是一个tagged pointer
类型。
那么我们在看下上面打印出的地址,所有NSTaggedPointerString
地址都是0xd
开头,d转换为二进制1110
,根据上面的结论,我们看到首位为1表示为NSTaggedPointerString
类型。在这里得到验证。
注意
:TaggedPointer
类型在iOS和MacOS中标志位是不同的iOS为最高位而MacOS为最低位
对象类型 正常情况下一个对象的类型,是通过这个对象的ISA指针来判断的,那么对于NSTaggedPointer
类型我们如何通过地址判断对应数据是什么类型的呢?
objc4-723之前 在objc4-723之前,我们可以通过与判断TaggedPointer
标志位一样根据地址来判断,而类型的标志位就是对象地址的61-63位,比如对象地址为0xa
开头,那么转换成二进制位1010
,那么去掉最高位标志位后,剩余为010
,即10进制中的2。
接着我们看下runtime源码objc-internal.h中有关于标志位的定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 #if __has_feature(objc_fixed_enum) || __cplusplus >= 201103L enum objc_tag_index_t : uint16_t#else typedef uint16_t objc_tag_index_t;enum #endif { 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 , OBJC_TAG_RESERVED_7 = 7 , 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_First60BitPayload = 0 , OBJC_TAG_Last60BitPayload = 6 , OBJC_TAG_First52BitPayload = 8 , OBJC_TAG_Last52BitPayload = 263 , OBJC_TAG_RESERVED_264 = 264 };#if __has_feature(objc_fixed_enum) && !defined(__cplusplus) typedef enum objc_tag_index_t objc_tag_index_t;#endif
那么我们知道2表示的OBJC_TAG_NSString
即字符串类型。因为目前已经无法验证这种情况了 所以我们不做其他类型验证。
objc4-750之后 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 static Class * classSlotForBasicTagIndex(objc_tag_index_t tag) { uintptr_t tagObfuscator = ((objc_debug_taggedpointer_obfuscator >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK); uintptr_t obfuscatedTag = tag ^ tagObfuscator; #if SUPPORT_MSB_TAGGED_POINTERS return &objc_tag_classes[0x8 | obfuscatedTag];#else return &objc_tag_classes[(obfuscatedTag << 1 ) | 1 ];#endif }
classSlotForBasicTagIndex() 函数的主要功能就是根据指定索引 tag 从数组objc_tag_classes中获取类指针,而下标的计算方法发是根据外部传递的索引tag。比如字符串 tag = 2。当然这并不是简单的从数组中获取某条数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 uint16_t NSString_Tag = 2 ; uint16_t NSNumber_Tag = 3 ; uintptr_t string_tagObfuscator = ((objc_debug_taggedpointer_obfuscator >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK); uintptr_t number_tagObfuscator = ((objc_debug_taggedpointer_obfuscator >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK); uintptr_t string_obfuscatedTag = NSString_Tag ^ string_tagObfuscator; uintptr_t number_obfuscatedTag = NSNumber_Tag ^ number_tagObfuscator; NSLog (@"%@" , objc_tag_classes[0x8 | string_obfuscatedTag]); NSLog (@"%@" , objc_tag_classes[0x8 | number_obfuscatedTag]);
控制台输出为:
1 2 TaggedPointer[89420:3027642] NSTaggedPointerString TaggedPointer[89420:3027642] __NSCFNumber
当我们多次运行时,我们发现实际上每次获取到的string_tagObfuscator
和number_obfuscatedTag
都不一样,但是每次从objc_tag_classes
中取出的类型均是一致的,因此实际上每次运行objc_tag_classes中的内容也是不断变化的。
如果你想进一步的了解可以参考Objective-C中伪指针Tagged Pointer
NSCFNumber 下面我们在看下NSNumber类型
1 2 3 4 5 6 7 8 9 10 NSNumber *number1 = @(0x1 );NSNumber *number2 = @(0x20 );NSNumber *number3 = @(0x3F );NSNumber *numberFFFF = @(0xFFFFFFFFFFEFE );NSNumber *maxNum = @(MAXFLOAT);NSLog (@"number1 pointer is %p class is %@" , number1, number1.class);NSLog (@"number2 pointer is %p class is %@" , number2, number2.class);NSLog (@"number3 pointer is %p class is %@" , number3, number3.class);NSLog (@"numberffff pointer is %p class is %@" , numberFFFF, numberFFFF.class);NSLog (@"maxNum pointer is %p class is %@" , maxNum, maxNum.class);
我们在看下打印结果:
1 2 3 4 5 TaggedPointerDemo[59218:2167895] number1 pointer is 0xf7cb914ffb51479a class is __NSCFNumber TaggedPointerDemo[59218:2167895] number2 pointer is 0xf7cb914ffb51458a class is __NSCFNumber TaggedPointerDemo[59218:2167895] number3 pointer is 0xf7cb914ffb51447a class is __NSCFNumber TaggedPointerDemo[59218:2167895] numberffff pointer is 0xf7346eb004aea86b class is __NSCFNumber TaggedPointerDemo[59218:2167895] maxNum pointer is 0x28172a0c0 class is __NSCFNumber
我们发现对于NSNumber,我们打印出来的数据类型均为__NSCFNumber
,但是我们发现对于MAXFLOAT
打印出的地址显然与其他几项不符,上面几个NSNumber的地址以0xf
开头,根据字符串地址的经验我们可以看出f = 1111
,首位标记位为1,表示这个数据类型属于TaggedPointer
。而MAXFLOAT不是。
获取TaggedPointer的值 objc4-723之前 字符串:
从上图的地址中我们就可以看出,从低位到高位分别表示的就是字符串的值(在ASCII码表中的值)
数字:
对于数字来说从地址中也是直接读出存储的值,如上图。
objc4-750之后 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 static inline uintptr_t _objc_decodeTaggedPointer(const void * _Nullable ptr) { return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator; }static inline uintptr_t _objc_getTaggedPointerValue(const void * _Nullable ptr) { uintptr_t value = _objc_decodeTaggedPointer(ptr); uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK; if (basicTag == _OBJC_TAG_INDEX_MASK) { return (value << _OBJC_TAG_EXT_PAYLOAD_LSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_RSHIFT; } else { return (value << _OBJC_TAG_PAYLOAD_LSHIFT) >> _OBJC_TAG_PAYLOAD_RSHIFT; } }static inline intptr_t _objc_getTaggedPointerSignedValue(const void * _Nullable ptr) { uintptr_t value = _objc_decodeTaggedPointer(ptr); uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK; if (basicTag == _OBJC_TAG_INDEX_MASK) { return ((intptr_t)value << _OBJC_TAG_EXT_PAYLOAD_LSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_RSHIFT; } else { return ((intptr_t)value << _OBJC_TAG_PAYLOAD_LSHIFT) >> _OBJC_TAG_PAYLOAD_RSHIFT; } }
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 NSString *str1 = [NSString stringWithFormat:@"1" ]; NSString *str11 = [NSString stringWithFormat:@"11" ]; NSString *str2 = [NSString stringWithFormat:@"2" ]; NSString *str22 = [NSString stringWithFormat:@"22" ]; uintptr_t value1 = objc_getTaggedPointerValue((__bridge void *)str1); uintptr_t value2 = objc_getTaggedPointerValue((__bridge void *)str2); uintptr_t value11 = objc_getTaggedPointerValue((__bridge void *)str11); uintptr_t value22 = objc_getTaggedPointerValue((__bridge void *)str22); NSLog (@"%lx" , value1); NSLog (@"%lx" , value11); NSLog (@"%lx" , value2); NSLog (@"%lx" , value22);
控制台输出:
1 2 3 4 TaggedPointer[89535:3033433] 311 TaggedPointer[89535:3033433] 31312 TaggedPointer[89535:3033433] 321 TaggedPointer[89535:3033433] 32322
即 “1” = 0x31 1,最后一位表示长度,在ASCII码表中31表示的就是字符1。而且从字符串“11”的结果我们也可以验证上面的说法。
isa 指针(NONPOINTER_ISA) 上面我们说了,对于一个对象的存储,苹果做了优化,那么对于ISA指针呢?
对象的isa指针,用来表明对象所属的类类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 union isa_t { isa_t() { } isa_t(uintptr_t value) : bits(value) { } Class cls; uintptr_t bits;#if defined(ISA_BITFIELD) struct { ISA_BITFIELD; };#endif };
同时结合下图,我们可以更清晰的了解isa指针的作用以及类对象的概念。
从图中可以看出,我们所谓的isa指针,最后实际上落脚于isa_t的联合类型。那么何为联合类型呢?联合类型是C语言中的一种类型,是一种n选1的关系,联合的作用在于,用更少的空间,表示了更多的可能的类型,虽然这些类型是不能够共存的
。比如isa_t 中包含有cls
,bits
, struct
三个变量,它们的内存空间是重叠的。在实际使用时,仅能够使用它们中的一种,你把它当做cls,就不能当bits访问,你把它当bits,就不能用cls来访问。
对于isa_t
联合类型,主要包含了两个构造函数isa_t()
,isa_t(uintptr_t value)
和三个变量cls
,bits
,struct
,而uintptr_t
的定义为typedef unsigned long
。
当isa_t作为Class cls使用时,这符合了我们之前一贯的认知:isa是一个指向对象所属Class类型的指针。然而,仅让一个64位的指针表示一个类型,显然不划算。
因此,绝大多数情况下,苹果采用了优化的isa策略
,即,isa_t
类型并不等同而Class cls
, 而是struct
。
struct 下面我们先来看下struct的结构体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # if __arm64__ # define ISA_MASK 0x0000000ffffffff8ULL # define ISA_MAGIC_MASK 0x000003f000000001ULL # define ISA_MAGIC_VALUE 0x000001a000000001ULL # define ISA_BITFIELD \ uintptr_t nonpointer : 1 ; \ uintptr_t has_assoc : 1 ; \ uintptr_t has_cxx_dtor : 1 ; \ uintptr_t shiftcls : 33 ; \ uintptr_t magic : 6 ; \ uintptr_t weakly_referenced : 1 ; \ uintptr_t deallocating : 1 ; \ uintptr_t has_sidetable_rc : 1 ; \ uintptr_t extra_rc : 19 # define RC_ONE (1ULL<<45) # define RC_HALF (1ULL<<18)
注意:成员后面的:表明了该成员占用几个bit
而每个成员的意义如下表
标志位说明
成员
bit位
说明
nonpointer
1bit
标志位。1(奇数)表示开启了isa优化,0(偶数)表示没有启用isa优化。所以,我们可以通过判断isa是否为奇数来判断对象是否启用了isa优化
has_assoc
1bit
标志位。表明对象是否有关联对象。没有关联对象的对象释放的更快。
has_cxx_dtor
1bit
标志位。表明对象是否有C++或ARC析构函数。没有析构函数的对象释放的更快
shiftcls
33bit
类指针的非零位。
magic
6bit
固定为0x1a,用于在调试时区分对象是否已经初始化。
weakly_referenced
1bit
标志位。用于表示该对象是否被别的对象弱引用。没有被弱引用的对象释放的更快。
deallocating
1bit
标志位。用于表示该对象是否正在被释放。
has_sidetable_rc
1bit
标志位。用于标识是否当前的引用计数过大,无法在isa中存储,而需要借用sidetable来存储。(这种情况大多不会发生)
extra_rc
19bit
对象的引用计数减1。比如,一个object对象的引用计数为7,则此时extra_rc的值为6。
从上表我们发现,extra_rc
和has_sidetable_rc
是和引用计数相关的标志位,当extra_rc 不够用时,还会借助sidetable来存储计数值,这时,has_sidetable_rc会被标志为1。
接下来我们来验证下,这些标志位是否真的如表中介绍那样。
引用计数 我们先来看下面这段代码
1 2 3 4 - (void )testisa { NSObject *obj = [[NSObject alloc] init]; NSLog (@"1. obj isa_t = %p" , *(void **)(__bridge void *)obj); }
控制台输出结果
1 TaggedPointerDemo[59983:2185591] 1. obj isa_t = 0x1a1f335beb1
我们将地址0x1a1f335beb1
转换过后:
我们看到这时候 对象是nonpointer
开启了isa优化,且当前的引用计数器为 extra_rc = 0 + 1 = 1;
下面我们接着测试
1 2 3 4 5 NSObject *obj = [[NSObject alloc] init];NSLog (@"1. obj isa_t = %p" , *(void **)(__bridge void *)obj); _obj1 = obj;NSObject *tmpObj = obj;NSLog (@"2. obj isa_t = %p" , *(void **)(__bridge void *)obj);
控制台输出为
1 2 TaggedPointerDemo[63235:2266690] 1. obj isa_t = 0x1a1f335beb1 TaggedPointerDemo[63235:2266690] 2. obj isa_t = 0x41a1f335beb1
我们将地址0x41a1f335beb1
转换过后:
我们看到这时候,我们将obj强引用之后,又实用了一个局部变量对其进行引用,所以这时的引用计数应该为2,当然从图中我们也可以验证这一点。
weakly_referenced 我们这次添加一个弱引用来验证
1 2 _weakRefObj = _obj1;NSLog (@"3. obj isa_t = %p" , *(void **)(__bridge void *)_obj1);
控制台输出为
1 TaggedPointerDemo[63235 :2266690 ] 3. obj isa_t = 0x45a1f335beb1
这时候我们仅仅通过地址进行判断 当添加了_obj2 = _obj1
后,地址变为0x61a1f335beb1
与之前地址0x41a1f335beb1
对比
上图我们可以看到weakly_referenced
标志位被置为1.表示这个对象有被弱引用。
has_assoc 然后我们在添加一个关联属性
1 2 3 NSObject *attachObj = [[NSObject alloc] init]; objc_setAssociatedObject(_obj1, "attachKey" , attachObj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);NSLog (@"4. obj isa_t = %p" , *(void **)(__bridge void *)_obj1);
控制台输出为:
1 TaggedPointerDemo[63235 :2266690 ] 4. obj isa_t = 0x45a1f335beb3
从上图中我们看到has_assoc
标志位被置为1.
总结 截止到这里,我们通过观察NSTaggedPointer
,相关标志位我们基本了解了NSTaggedPointer是如何存储数据以及标志位的作用。
参考文章 Objective-C中伪指针Tagged Pointer
Friday Q&A 2015-07-31: Tagged Pointer Strings