iOS---内存管理(3)

数据结构

*Spinlock_t
*RefcountMap
*weak_table_t

Spinlock_t:

*Spinlock_t是“忙等”的锁。(忙等:如果当前锁已被其他线程获取,那么当前线程会不断的探测锁是否被释放,如果释放,自己第一时间获取这个锁)
*适用于轻量访问。

RefcountMap

引用计数表是一张Hash表,通过指针可以找到对应对象的引用计数,实际查找过程也是Hash查找。
这个Hash查找的Hash算法,实际上是对传入对象的指针做一个伪装操作,然后去获取对应的引用计数
之所以使用Hash查找,就是为了提高查找效率。查找效率的提高,源于存储一个对象的引用计数是通过这个函数来计算存储位置的,而获取的对象所代表的引用计数的值的时候,也是通过这个函数来计算我们应该获取的索引位置。
所以插入和获取都是通过同一个函数来计算位置的,这就避免了循环遍历操作
所以说使用Hash查找可以提高查找效率
Q:引用计数表是通过什么实现的
A:使用Hash表实现
Q: 为什么引用计数表要使用Hash查找?
A:提高查找效率,插入和获取都是通过同一个函数来计算位置的,这就避免了循环遍历操作


c9640bebe36f056b8941b3e548fec12.png
9058da1a793e947f94abff7313d502d.png

引用计数是用64位比特位来表示的,
第一个二进制位代表是否有弱引用。
第二个二进制位代表当前对象是否正在进行 deallocating
剩余位数存储的就是实际的引用计数值,计算对象实际的引用计数值,需要向右偏移2位,因为后面这两位要把它去掉,才能取到真实的引用计数值

weak_table_t

弱引用表, 也是一张Hash表, 在Runtime 源码中,系统是通过weak_table_t来定义的
对象指针作为Key,通过Hash函数,就可以计算出对应的弱引用对象的存储位置,或者说查找的位置
weak_entry_t实际也是个结构体数组,这个结构体数组中存储的每一个对象实际就是弱引用指针,也就是代码中定义的__weak id obj, obj的内存地址,或者说指针,就存储到weak_entry_t的结构体数组当中。


image.png

MRC

手动引用计数
alloc: 分配对象的内存空间
retain:使对象的引用计数+1
release:使对象的引用计数-1
retainCount:可以获取当前对象的引用计数值
autorelease:如果调用了一个对象的autorelease方法, 那么当前这个对象会在autorelease pool结束的时候调用它的release操作,进行引用计数-1
dealloc: 内存管理方法,在MRC的时候,需要调用 [super dealloc] 来释放父类的成员变量


1646232366(1).png

谨记:在MRC中特有的方法:retain,release, retainCount, autorelease,如果在ARC中调用会引起编译报错。

ARC

Q:什么是ARC
A: ARC实际上是编译器自动为我们插入retain,release操作之外,还需要runtime功能进行支持,然后由编译器和runtime共同协作,才能组成ARC的全部功能


1646233864(1).png

Q: MRC和ARC有什么区别?
A: MRC是手动管理内存,ARC是由编译器和runtime协作来进行自动引用计数的内存管理,同时MRC中可以调用一些引用计数相关的方法,而ARC中是不能调用的

Q:weak变量为何在对象释放的时候自动置为nil ?

引用计数管理

实现原理分析:
alloc:经过一系列调用,最终调用了C函数的calloc, 此时并没有引用计数+1
retain:
objc_680源码截取片段
1.SideTable &table = SideTables()[this];
SideTables可以看成是多个SideTable组成的Hash表,经过Hash函数的计算,可以快速的在SideTables中找到对应的SideTable
2.size_t & refcntStorage = table.refcnts[This];
在SideTable的结构当中,去获取引用计数map这样的一个成员变量,通过当前对象的指针,在这个SideTable的引用计数表中去获取当前对象的引用计数值。
refcnts就是SideTable的一个成员变量。也是引用计数表
这个查找过程又是一次Hash查找。所以相当于在进行retain操作的时候,是经历了2次Hash查找,最终查找到的结果是size_t
3.refcntStorage += SIDE_TABLE_RC_ONE;

Q: 在进行retain操作的时候,系统是怎样查找对应的引用计数的。
A: 经过2次Hash查找来找到的对应的引用计数值,然后进行相应的+1操作

release:
1.SideTable &table = SideTables()[this];
2.RefCountMap::iterator it = table.refcnts.find(this);
根据当前对象指针访问table当中的引用计数表,去查找对应的引用计数表
3.it->second -= SIDE_TABLE_RC_ONE
查找到后,进行-1操作

retainCount:
1.SideTable &table = SideTables()[this];
2.size_t refcnt_result = 1
声明一个局部变量,指定值是1
3.RefCountMap::iterator it = table.refcnts.find(this);
通过当前对象,到引用计数表中去查找
4.refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
把结果做一个向右偏移的操作,再结合局部变量的it,进行+的操作,返回给调用方

刚 alloc出来的对象,在引用计数表中,是没有这个对象相关联的映射的,那么这个值 it读出来的就是0,另外由于局部变量refcnt_result是1,那么只经过alloc调用的产生的对象,去调用retainCount, 就可以获取到它的值为1

dealloc

image.png

1.首先调用 _objc_rootDealloc()这样的私有函数

  1. _objc_rootDealloc()又会调用rootDealloc(), 在函数内部判断,当前对象是否能直接释放,直接释放的判断条件,依据于上图右侧的列表
    1). nonpointer_isa,判断当前对象是是否使用了非指针型的isa
    2). 当前对象是否有weak指针指向它
    3). 当前对象是否有关联对象
    4). 当前对象的内部实现,是否有涉及到一些C++相关的内容,以及当前对象是否使用ARC来管理内存,如果使用ARC管理内存或者说当前对象涉及到了一些C++的内容,这个判断标志都是YES
    5). 表示当前对象的引用计数是否通过sideTable当中的引用计数表来维护的
    3.只有1) && 2) && 3) && 4) && 5)都不包含,才可以采用C函数释放,否则要调用 object_dispose()清除的函数

object_dispose()实现

1646666526(1).png

objc_destructInstance()实现

image.png

1). 首先判断当前对象当中是否有C++相关的内容,或者当前对象采用的是ARC,如果有,会调用object_cxxDestruct(), 如果没有的话,会判断当前对象是否有关联对象,如果有关联对象,会在dealloc内部实现调用_object_remove_associations(), 对象相关关联的移除。
2 ). 关联对象移除后,会调用clearDeallocating(), 结束dealloc调用流程
Q:通过关联对象的技术,为一个类添加的一些实例变量,那么在对象的dealloc方法中,是否有必要对它的关联对象进行移除操作呢?
A: 在系统的dealloc内部实现当中,会自动判断当前对象是否有关联对象,如果有的话呢,系统就帮助我们把相关的关联对象一并移除掉

clearDeallocating()实现

image.png

1).调用sidetable_clearDeallocating()的函数
2).调用weak_clear_no_lock()函数,将指向该对象的弱引用指针置为nil
3).table.refcnts.erase(), 将当前对象在引用计数表当中的存储数据清除掉
4).结束调用流程
Q: 当对象dealloc或者废弃后,它的weak指针为何会自动置为nil
A: 就是因为在dealloc内部实现当中,有做相关对象的弱引用指针自动置为nil的操作

弱引用管理

image.png

Q: 一个weak变量,是怎样被添加到弱引用表当中的
A: 一个被声明为__weak的对象指针,通过编译器的编译后,会调用相应的objc_initWeak()方法,经过一系列的调用栈,最终在weak_register_no_lock()中进行弱引用的添加,具体添加的位置是通过Hash算法来进行位置查找的。
如果查找对应位置当中,已经有当前对象的弱引用数组,就把新的弱引用变量添加到数组当中,如果没有,重新创建一个弱引用数组,第0个地址添加上最新的weak指针,后面的都初始化为0或者nil
添加weak变量


image.png

Q: 当一个对象被释放或废弃之后, weak变量是怎样处理的?
A: 当一个对象被dealloc后,在dealloc内部实现中,会调用弱引用清除的相关函数,在函数内部实现当中,会根据当前对象指针查找弱引用表,把当前对象相对应的弱引用都拿出来,是一个数组,然后遍历数组当中所有的弱引用指针,分别置为nil


image.png

自动释放池

以下代码,array是在什么时候释放的?


image.png

Q: AutoreleasePool的实现原理是什么?
A:

Q: AutoreleasePool为什么可以嵌套使用
A:

Q: 什么是自动释放池/自动释放池的实现结构是怎样的?
A: 是以栈为结点通过双向链表的形式组合而成的

image.png
image.png
image.png

批量pop操作的意思是,在autoreleasePool花括号中的对象,都会被发送一次release消息。

image.png
image.png
image.png
image.png

从图上可知, AutoreleasePool是跟线程一一对应的


image.png

image.png
image.png
image.png

比如 next 指针指向一个位置,如果此位置产生了新的对象(因为调用了 autorelease),添加对象后, next指针就会移动到新的位置,再次添加对象,就可以添加到新的 next上。

image.png
image.png
image.png

循环引用

image.png
image.png
image.png
image.png
image.png
image.png
image.png

__weak和__unsafe_unretained(引用计数没加一) 一样


image.png
image.png
image.png

循环引用示例:

NSTimer的循环引用问题。


image.png

NSTimer被分派后,会被当前线程的Runloop强引用,或者说NSTimer是在主线程当中创建的,就由主线程的Runloop持有NSTimer, 即使是使用weak来修饰,但是由于Runloop是常驻线程,会对NSTimer强引用,再通过NSTimer对对象的强引用,仍然对这个对象进行了强引用,因此,即使VC页面退出,VC与对象的指向移除了,滚动栏由于被Runloop间接的引用持有了,这个对象也不会被释放,此时就出现内存泄漏.

NSTimer有重复定时器和非重复定时器的区分。
一般会对NSTimer调用invalid方法,另外将NSTimer置为nil。把循环引用破除掉。

Q:如果重复多次回调的计时器,不能对NSTimer做invalid和置nil的操作,此时应如何破除循环引用呢?
A:引入中间对象,VC退出后,就释放了对广告栏的强引用,当下次定时器的回调回来的时候,中间对象去查看对象是否释放掉了,就判断中间对象持有的对象是否为nil,这种方案也是利用了对象被释放了,weak指针会自动置为nil的特点来解决这个问题,如果中间对象持有的广告栏对象被释放了,就可以在中间对象的回调方法中,对NSTimer无效,并且置为nil, 就可以实现Runloop对Runloop的强引用,以及NSTimer对中间对象的强引用。


image.png
image.png
image.png
image.png

总结

Q:什么是ARC?
A:ARC是有LLVM编译器和Runtime共同协作来实现自动引用计数的管理。

Q:为什么weak指针指向的对象在废弃后会被自动置为nil?
A: 当对象被废弃后,dealloc的内部实现当中,会调用弱引用的方法,在清除弱引用的方法当中会通过Hash算法来查找被废弃对象在弱引用表中的位置,来提取它对应的弱引用指针的一个列表数组,进行for循环遍历,把每一个weak指针都置为nil。

Q:苹果是如何实现AutoreleasePool的?
A:AutoreleasePool是以栈为节点,由双向链表来合成的一个数据结构。

Q:什么是循环引用? 你遇到过哪些循环引用,是怎么解决的?
A:

你可能感兴趣的:(iOS---内存管理(3))