iOS内存管理 —— TaggedPointer 和 retain、release 流程分析
1. 内存布局
内存五大区
栈 : 存放局部变量,参数,指针,函数,地址一般以0x7开头。栈区的内存通过sp寄存器来定位的。堆 :存放alloc、new开辟内存的对象,地址一般以0x6开头。全局区(bss) :未初始化的全局变量和静态变量,地址一般以0x1开头。常量区(data) :已初始化的全局变量和静态变量,地址一般以0x1开头。代码区(text) :主要用于存放程序运行时的代码,代码会被编译成二进制存进内存的
除了内存五大区,还有内核区和保留区,内核区是系统用来进行内核处理操作的区域,而保留区则是预留给系统处理nil等。oxc0000000只代表了3个G,也就是说4个G的内存里面,分给了内核区一个G。
2. 内存管理方案
内存管理方案除了MRC和ARC,还有以下三种
TaggedPointer : 用来储存小对象的指针例如NSNumber,NSDate等,指针上还存了tag和内容,优化了内存和访问速度。WWDC关于TaggedPointer的介绍.NONPOINTER_ISA : 非指针型isa散列表 : 引用计数表,弱引用表
2.1 模拟器调试TaggedPointer
这里看到NSString 的类型有三个:
NSCFConstantString :字符串常量,是一种编译时常量 ,retainCount值很大,对其操作,不会引起引用计数变化,存储在字符串常量区 NSCFString :是在运行时 创建的NSString子类 ,创建后引用计数会加1,存储在堆上 NSTaggedPointerString :标签指针,是苹果在64位环境下对NSString、NSNumber等对象做的优化。对于NSString对象来说
- 当字符串是由
数字、英文字母 组合且长度小于等于9 时,会自动成为NSTaggedPointerString 类型,存储在常量区 - 当有
中文或者其他特殊符号 时,会直接成为__NSCFString 类型,存储在堆区
参考
接下来去源码搜索TaggedPointer 。看到这里有关于playload的位置运算,说明这里有加密和解密的过程。那么接下来就去搜索decode。 这里看到了_objc_decodeTaggedPointer 这个方法,_objc_decodeTaggedPointer 还调用了_objc_decodeTaggedPointer_noPermute 对pointer进行了异或objc_debug_taggedpointer_obfuscator 的操作。 搜索一下objc_debug_taggedpointer_obfuscator ,看到其在initializeTaggedPointerObfuscator 进行了初始化。这里如果开启了混淆,那么就会得到一个随机的objc_debug_taggedpointer_obfuscator 值,否则就为0。 在看到这里encode的时候也是异或objc_debug_taggedpointer_obfuscator ,这样两次异或就能得到原来的值。 而之前的左移右移操作,则是在_objc_makeTaggedPointer 里面。这里可以看出来先进行移动,在进行加密。 接下来打印NSTaggedPointerString 的地址,向右移三位 去掉tag 。由于是小端模式 ,所以从后往前读,读出107 和99 ,分别对应k 和 c 。 NSTaggedPointerString 的地址第一位的1 代表的是这个指针是target pointer ,而后面的010 也就是2 ,则代表的是NSString 类型。 那么如果是NSNumber类型的,则是011 ,也就是3 。
2.2 真机调试TaggedPointer
这里看到前面只是标记这个为TaggedPointer ,类型的标记跑到最后去了。而前面还有一个0010 代表着2,这个是什么呢?是不是代表字符长度 呢? 看到这里asd 和 asdf 分别打印出来的是0011 和0100 ,代表 3 和 4 。这里说明确实是代表着字符长度 。 那么在NSNumber里面这个代表着啥呢?这里看到6的值是2,6.0的值是5。那么是不是代表着类型呢?经过一番实验,得出char 为0 ,short 为1 ,int 为2 ,long 为3 , float 为4 ,double 为5 。
3. NONPOINTER_ISA
之前的文章中介绍过 NONPOINTER_ISA,知道对象的引用计数是存在extra_rc里面的。如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数大于 10, 则需要使用到下面的 has_sidetable_rc。
3.1 retain 流程分析
- 判断是否为Tagged Pointer,如果是则不进行操作直接返回,否则就往下走
- 判断是否为 NONPOINTER_ISA,如果不是则直接通过散列表(sidetable)进行retain操作,这里进行+2是因为散列表将引用计数存在63号位置上,+2相当于63号位上的+1操作。是的话就往下走。
- 判断是否正在析构,如果是则进行相关操作后返回
- 执行extra_rc+1,即引用计数+1操作,并给一个引用计数的状态标识carry,用于表示extra_rc是否满了(最大值为256)
- 如果满了,那么就将一半的引用计数存在散列表,一半存在extra_rc。这里分一半是因为通过extra_rc操作引用计数更加的方便,而散列表则需要先去寻找表,再去找到引用计数存储的地方 进行操作,并且散列表还有开锁解锁的操作。
3.2 release 流程分析
- 判断是否为Tagged Pointer,如果是则不进行操作直接返回,否则就往下走
- 判断是否为 NONPOINTER_ISA,如果不是则直接通过散列表(sidetable)进行release操作。
- 判断是否正在析构,如果是则进行相关操作后返回false
- 对extra_rc中的引用计数值进行-1操作,并存储此时的extra_rc状态到carry中
- 如果此时的状态carray为0,则走到underflow流程
- 判断是否有散列表,如果没有则进行deallocate,发送dealloc消息给对象。如果有则从散列表取一半出来进行–操作后赋值给extra_rc,散列表里面的引用计数也变为原来的一半。
4. 面试题
下面代码是否会崩溃呢?这里上面的代码不会崩溃,下面的代码会崩溃。崩溃的原因是一直进行多线程的读和写,写的过程中对新值的retain和对旧值的release。所以会产生一个瞬间旧值已经释放了但是还是继续release,所以崩溃了。那么为什么Tagged Pointer不会有这个问题呢? 看到rootRetain ,发现这里如果是TaggedPointer 就不做处理直接return。而rootRelease 也是一样的。
|