《iOS底层原理文章汇总》
上一篇文章iOS-底层原理32-启动优化二进制重排介绍了启动优化二进制重排,本文介绍内存管理
1.内存布局
通过汇编去找指针,汇编通过sp的寄存器去定位,栈中内存通过汇编寄存器去定位的,每一个内存都包含地址的内存空间,能直接定位到。0-0x00400000作为预留给系统处理,null以及底层发送的指令
2.内存管理
通过nonpointer_isa(非指针型isa)来优化整个64位地址,为什么self.nameStr为cooci的时候程序不会发生多条线程对同一个对象retain release操作?而self.nameStr为cooci_和谐学习不急不躁时程序在多线程会发生retain release同时对一个对象操作而崩溃?
查看self.nameStr=@"cooci"时,self.nameStr的isa指向NSTaggedPointerString
image.png查看self.nameStr=@"cooci_和谐学习不急不躁"时,self.nameStr的isa指向NSCFString
image.png
self.nameStr在进行赋值操作执行setter或getter方法时,会对新值newValue进行retain,对旧值oldValue进行release,查看源码,obj的isa为 isTaggedPointer时,setter和getter方法执行时不会进行新值的retain和旧值的release,故不会发生过度释放而崩溃。
taggedPointer类型在修饰小对象时有了比较特殊的处理,指针是什么?String有值+指针地址,通过指针去操作引用计数等等其他处理taggerPointer类型修饰的String和对象String是否是相同的处理呢?
image.png
image.png
3.taggedPointer
什么是小对象?NSNumber,NSDate,小于11位的小String,一个对象8字节,64位,NSNumber里面存入1,用不了64位,会浪费空间,为了节省空间,可以用小对象去接收
I.在方法查找流程中,第一次接触到taggedpointer指针是在readImages->initializeTaggedPointerObfuscator()
通过对指针地址编码进行异或运算混淆,两次异或就能解码计算出原始没有混淆前的地址
要向获取原始的真正属于taggedpointerstring的指针地址呢,对现在的地址进行一次异或运算,得到小对象地址0xa000000000000621,倒数两位62表示98,表示b,@1是NSNumber类型,倒数第二位为1,说明地址里面存储了值,不仅是简单的地址还包含了值,double和float类型底层经过特殊处理
II.小对象地址里面的0xa和0xb是哪里来的?
在判断是否是小对象的方法中指针地址与_OBJC_TAG_MASK进行与运算,相当于保留最高位,_OBJC_TAG_MASK的值为#define _OBJC_TAG_MASK (1UL<<63)
最高位是否为1代表是否是taggedpointer,2代表NSString,3代表NSNumber,小对象放在常量区,不需要ARC自动管理
TP表示是taggedpointer类型
4.retain
I.判断是否为nonpointer
II.操作引用计数
a.如果不是nonpointer -> 散列表
spinlock_t slock; 开解锁
RefcountMap refcnts; 引用计数表
weak_table_t weak_table; 弱引用表
散列表 在内存里面有多张 + 最多能够多少张 一张 对象开锁解锁会不安全且效率低
b.是否正在释放和析构
c.extra_rc+1 满了-散列表
d.carry 满了标记,extra_rc 满/2 -> extra_rc 满/2 -> 散列表(开锁关锁)
5.散列表
为什么有isa?retain会操作引用计数+1,retainCount在isa的bits中,会在操作引用计数时用到
A.objc_retain->obj->retain()->rootRetain(),判断是否为nonpointerisa,若不是nonpointerisa,无法进行存储。
B.操作引用计数:若不是nonpointerisa,直接操作散列表SideTables不止一张,和关联对象表原理一样
为什么散列表在内存中有多张,若放在一张表中操作引用计数,会暴露所有对象不安全,锁解锁消耗性能,真机下面是8张表,散列表是哈希表,哈希表结合链表和数组,增删改查都方便,拉链法,通过哈希函数定位相应下标。不用链表和数组,链表不便于查,数组不便于增删。
6.dealloc
对象要释放,需要做哪些事情?isa中有个cxx析构代码,关联对象表清空,弱引用表清空,散列表中引用计数表清空,最后free
7.release:retain的反向操作,将extra_rc中的引用计数减为0后,判断是否有carry,有继续减散列表中的引用计数,extra_rc和散列表中的引用计数都减为0后,开始析构dealloc,自动触发释放函数
alloc -> retain -> release -> dealloc构成一个闭环
8.retainCount
面试题:[NSObject alloc]引用计数,输出为1
alloc创建对象引用计数为0,默认+1,为1
9.强持有和循环引用的区别
self.timer添加到runloop中,控制器pop没有进入dealloc方法进行释放?
I.解决办法:在- (void)didMoveToParentViewController:(UIViewController *)parent
方法中对timer进行销毁,之后进入dealloc方法对self进行销毁
II.强持有和循环引用的区别
以上怎么造成循环引用,self无法释放,无法进入dealloc方法中的?
self -> timer -> self,self很明显持有timer,timer是怎么持有self的呢?查看官方文档,The object to which to send the message specified by aSelector when the timer fires. The timer maintains a strong reference to this object until it (the timer) is invalidated. timer对self进行了强持有
若用
__weak typeof(self) weakSelf = self
weakSelf代替self能解决循环引用嘛,结果是不能。[NSRunLoop currentRunLoop] 强持有 -> timer -> weakSelf -> self,runloop对timer进行了强持有,runloop的生命周期长,runloop对timer没有进行释放,weakSelf和self都释放不掉,和self->block->weakSelf-self的模型完全不一样weakSelf : 没有对内存加1
两个内存地址self和weakSelf指向了同一片内存空间,timer根据官方文档捕获的是内存地址<LGTimerViewController: 0x7f91a9006110>,Block捕获的是weakSelf, 和self没有关系,self -> block -> weakSelf (临时变量的指针地址) 地址->内存
block捕获的是地址指针,timer捕获的是对象本身,timer中无法通过weakSelf来打破, NSRunLoop -> timer -> weakSelf (<LGTimerViewController: 0x7fb8c9d11240>),RunLoop包含<LGTimerViewController: 0x7fb8c9d11240>,RunLoop不停,LGTimerViewController无法释放
这就是强持有和循环引用的区别
III.解决强持有的方法
解决思路:我们需要打破这一层强持有 - self
A.通过在- (void)didMoveToParentViewController:(UIViewController *)parent
方法中对timer进行销毁,之后进入dealloc方法对self进行销毁
B.中介者模式:引入中介者LGTimerWapper,添加timer,给VC中的selector发送消息,当vc pop时释放,判断vc是否为nil,是将timer释放,从而打破runloop强持有LGTimerWapper,皆释放,若timer中的方法过多,要不断添加到LGTimerWapper中
C.Proxy:虚基类的方式,强持有引用转移到消息转发,虚基类无法做事进行转发,不断的转发到vc,信息的传递,在vc的dealloc方法中对vc进行释放,proxy进而释放,timer释放,runloop也释放了proxy,皆释放