iOS引用计数管理之揭秘计数存储
前言
最近偶尔出去面试了解一下现在iOS行情和面试会问的问题。其中有这样的一个问题被问到很多次:引用计数原理。回去查资料发现当时回答的很糟糕,于是就在这里单独写一篇文章记录下来。这篇文章只讲一个问题:引用计数的数量存哪里的,文末提到的其他问题后面会单独再写。
预备知识
要说清楚这个问题,我们需要先来了解下面的三个知识点。
调试环境如下。
macOS:10.13.4; XCode:9.4; 调试设备:My Mac。
Tagged Pointer
这个玩意的详细解释在这里,简单的说64位系统下,对于值小(多小?后面有讲解)的对象指针本身已经存了值的内容了,而不用去指向一个地址再去取这个地址所存对象的值;相信你也知道了,如果是Tagged Pointer的话就少了创建对象的操作。
我们也可以在WWDC2013的《Session 404 Advanced in Objective-C》视频中,看到苹果对于Tagged Pointer特点的介绍: 1:Tagged Pointer专门用来存储小的对象,例如NSNumber和NSDate 2:Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free。 3:在内存读取上有着3倍的效率,创建时比以前快106倍。
我们来测试一下。
NSLog(@"%p",@(@(1).intValue));//0x127 NSLog(@"%p",@(@(2).intValue));//0x227 由此可知int类型的tag为27,因为去掉27后0x1 = 1,0x2 = 2,正好是值。 NSLog(@"%p",@(@(1).doubleValue));//0x157 NSLog(@"%p",@(@(2).doubleValue));//0x257 由此可知double类型的tag为57,因为去掉27后0x1 = 1,0x2 = 2,正好是值。 明显0x127、0x257不是一个地址,所以@(1)、@(2)也不是一个对象,只是一个普通变量。
既然是Tagged Pointer那肯定得有一个tag,经过测试发现值类型不一样所具有的tag也会相对不一样。
为什么说相对,因为测试发现unsigned long和long long具有相同的tag值37。 当然其他类型也有一样的。
什么时候NSNumber对象Tagged Pointer失效呢?那就是当值和tag加起来占用的字节数要超过地址长度(8字节64位)时会失效:
为什么说要超过,而不是超过,这个我也比较纠结,具体的看看下面的例子。
这里针对double类型来举个例子,其他类型的结果可能稍有不同,因为上面说到tag有不同的值,所占用二进制位长度会不一样。
int 17:10001,5位; long long 37:100101,6位; double 57:111001,6位。 ...
这样64减去已占用的tag位,剩下的位来表示值,所能表示的范围也不一样。
double pow(double, double)返回的是double类型的值。 NSLog(@"%p",@(pow(2, 55) - 3));//0x7ffffffffffffc57 57是double类型的tag,0x7ffffffffffffc57去掉tag剩下的是0x7ffffffffffffc = pow(2, 55) - 3 = 36028797018963964;二进制表示为0...0(9个)1...1(53个)00(2个)。 关于这里为什么要-3这就是我比较纠结的原因,因为二进制表示后面还有2个0啊,还可以多表示3啊; 系统这么做肯定有自己的考虑,也许是我理解错了,希望你来指正。 NSLog(@"%p",@(pow(2, 55) - 2));//0x6030002c50c0 这个单纯就是一个地址了,没有57这个tag了,里面并没有存值的内容,所以Tagged Pointer失效了。 从这个例子可以知道tag占用8位,64 - 55 = 9,9 - 1 = 8,因为第一位是来做符号位表示正负数的; 上面我们测试出来57占用6个二进制位,为什么这里值最长占用56二进制长度呢,我也不知道。 关于Tagged Pointer是否启用,你也可以通过下面的语句来打印,这个语句是runtime源码中的。 NSNumber *number = @(pow(2, 55) - 3); NSLog(@"%d",((uintptr_t)number & 1UL) == 1UL);//true number = @(pow(2, 55) - 2); NSLog(@"%d",((uintptr_t)number & 1UL) == 1UL);//false
目前我所知的系统中可以启用Tagged Pointer的类对象有:NSDate、NSNumber、NSString,上面我们只举例了NSNumber,你可以自己下来试试另外的。
当然了你可以在环境变量中设置OBJC_DISABLE_TAGGED_POINTERS=YES强制不启用Tagged Pointer,环境变量我们可以添加很多东西的,具体的你可以看看runtime源码的objc-env.h文件。
不启用Tagged Pointer
这样runtime就会做相应的处理了。
不启用后上面的例子就会得到这样的结果,也就表示关闭成功了。
NSLog(@"%p",@(pow(2, 55) - 3));//0x6030002ccbc0 NSLog(@"%p",@(pow(2, 55) - 2));//0x6030002ccc50 NSNumber *number = @(pow(2, 55) - 3); NSLog(@"%d",((uintptr_t)number & 1UL) == 1UL);//false number = @(pow(2, 55) - 2); NSLog(@"%d",((uintptr_t)number & 1UL) == 1UL);//false
Non-pointer isa
我们一直认为实例对象的isa都指向类对象,甚至还看到这样的源码。
typedef struct objc_object *id struct objc_object { Class _Nonnull isa; }
其实这是之前版本的代码了,现在版本的代码早就变了。
struct objc_object { private: isa_t isa; ... }
所以实例对象的isa都指向类对象这样的说法不对。
现在实例对象的isa是一个isa_t联合体,里面存了很多其他的东西,相信你也猜到了引用计数也在其中;如果该实例对象启用了Non-pointer,那么会对isa的其他成员赋值,否则只会对cls赋值。
我们也可以在WWDC2013的《Session 404 Advanced in Objective-C》视频中,看到苹果对于Tagged Pointer特点的介绍: 1:Tagged Pointer专门用来存储小的对象,例如NSNumber和NSDate 2:Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free。 3:在内存读取上有着3倍的效率,创建时比以前快106倍。
0
对象是否不启用Non-pointer目前有这么几个判断条件,这些都可以在runtime源码objc-runtime-new.m中找到逻辑。
我们也可以在WWDC2013的《Session 404 Advanced in Objective-C》视频中,看到苹果对于Tagged Pointer特点的介绍: 1:Tagged Pointer专门用来存储小的对象,例如NSNumber和NSDate 2:Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free。 3:在内存读取上有着3倍的效率,创建时比以前快106倍。
1
我们自己新建一个Person类,通过OBJC_DISABLE_NONPOINTER_ISA=YES/NO来看看isa结构体的具体内容,设置方法上面有截图。
我们也可以在WWDC2013的《Session 404 Advanced in Objective-C》视频中,看到苹果对于Tagged Pointer特点的介绍: 1:Tagged Pointer专门用来存储小的对象,例如NSNumber和NSDate 2:Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free。 3:在内存读取上有着3倍的效率,创建时比以前快106倍。
2
不使用Non-pointer的isa
我们也可以在WWDC2013的《Session 404 Advanced in Objective-C》视频中,看到苹果对于Tagged Pointer特点的介绍: 1:Tagged Pointer专门用来存储小的对象,例如NSNumber和NSDate 2:Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free。 3:在内存读取上有着3倍的效率,创建时比以前快106倍。
3
使用Non-pointer的isa
我们也可以在WWDC2013的《Session 404 Advanced in Objective-C》视频中,看到苹果对于Tagged Pointer特点的介绍: 1:Tagged Pointer专门用来存储小的对象,例如NSNumber和NSDate 2:Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free。 3:在内存读取上有着3倍的效率,创建时比以前快106倍。
4
isa的赋值是在alloc方法调用时,内部会进入initIsa()方法,你可以进去看一看有啥不同之处。
我们也可以在WWDC2013的《Session 404 Advanced in Objective-C》视频中,看到苹果对于Tagged Pointer特点的介绍: 1:Tagged Pointer专门用来存储小的对象,例如NSNumber和NSDate 2:Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free。 3:在内存读取上有着3倍的效率,创建时比以前快106倍。
5
SideTable
散列表,这是一个比较重要的数据结构,相信你也猜到了这个和对象引用计数有关;如果该对象不是Tagged Pointer且关闭了Non-pointer,那该对象的引用计数就使用SideTable来存。我们先来看一下SideTable结构体定义,至于怎么被使用的且听我慢慢道来。
我们也可以在WWDC2013的《Session 404 Advanced in Objective-C》视频中,看到苹果对于Tagged Pointer特点的介绍: 1:Tagged Pointer专门用来存储小的对象,例如NSNumber和NSDate 2:Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free。 3:在内存读取上有着3倍的效率,创建时比以前快106倍。
6
启动应用后,我们第一次看到SideTable其实是在runtime读取image的时候。
我们也可以在WWDC2013的《Session 404 Advanced in Objective-C》视频中,看到苹果对于Tagged Pointer特点的介绍: 1:Tagged Pointer专门用来存储小的对象,例如NSNumber和NSDate 2:Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free。 3:在内存读取上有着3倍的效率,创建时比以前快106倍。
7
我们也可以在WWDC2013的《Session 404 Advanced in Objective-C》视频中,看到苹果对于Tagged Pointer特点的介绍: 1:Tagged Pointer专门用来存储小的对象,例如NSNumber和NSDate 2:Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free。 3:在内存读取上有着3倍的效率,创建时比以前快106倍。
8
本文由 投稿者 创作,文章地址:https://blog.isoyu.com/archives/iosyinyongjishuguanlizhijiemijishucunchu.html
采用知识共享署名4.0 国际许可协议进行许可。除注明转载/出处外,均为本站原创或翻译,转载前请务必署名。最后编辑时间为:7月 11, 2018 at 07:50 下午