第6章:内存管理——一、内存管理包含的内容;二、内存布局;三、内存管理方案

一、内存管理包含的内容

     1、内存布局

     2、内存管理方案

     3、数据结构

     4、ARC&MRC

     5、引用计数

     6、弱引用

     7、自动释放池

     8、循环引用

 

二、内存布局

BSS段存储未初始化的全局变量和静态变量,一旦初始化就会从BSS段中回收掉,转存到数据段中;data段存储已经初始化的全局变量和静态变量,以及常亮数据,直到程序结束时会被立即回收。

 

三、内存管理方案

iOS通过引用计数器来管理内存,对象产生时引用计数为1;当一个新的引用指向对象,那么这个对象的引用计数值增加1;当对象被少一次引用时对象的引用记数值减1;当对象的引用记数值为0的时候,代表这个对象没有被使用,此时系统会自动回收掉此对象,回收这个对象的同时自动调用这个对象的dealloc方法。但是系统底层是如何操作/管理引用计数的呢?我们接下来说明。本节源码根据objc-runtime-680版本讲述。

     1、内存管理方案

     iOS操作系统中,不同场景会采用不同的内存管理方案。不同场景的内存管理方案说明如下:

1)、TaggedPointer:小对象采用这种内存管理方案,比如NSNumber等

2)、NONPOINTER_ISA:64位架构采用这种内存管理方案。在64位机中数据一般用40位就够了,苹果了提高内存利用率在剩余的比特位上存储了内存管理相关的内容。

3)、散列表:其中包括弱引用表和引用计数表

     2、NONPOINTER_ISA

1)、indexed:0代表该isa指针只是一个isa指针,指向当前对象的类对象;1代表该指针中不仅存储了类对象的指针,而且还存储了内存管理相关的数据,即NONPOINTER_ISA。

2)、has_assoc:代表该对象是否具有关联对象。

3)、has_cxx_dtor:代表当前对象是否有使用到c++相关代码。在ARC中也通过该位标识某些对象是通过ARC进行内存管理的。

4)、shiftcls:代表当前对象的类对象的地址

5)、weakly_referenced:标识该对象是否具有弱引用指针

6)、deallocating:标识该对象是否正在进行dealloc操作。

7)、has_sidetable_rc:标识该isa指针中存储的引用计数是否达到上限,如果达到上限就会将引用计数外挂一个sidetable哈希表,用于存储相关的引用计数。

8)、extra_rc:用于存储额外的引用计数。当引用计数很小时是存储在isa指针中的,当该isa指针中存储的引用计数达到上限时才使用sidetable哈希表来存储引用计数。

     3、散列表内存管理方式

1)、SideaTables()哈希表

2)、SideaTable结构

3)、为什么不是一个SideaTable?

因为如果所有对象都用一个SideaTable的话,更改一个对象的引用计数就要对整个结构枷锁,存在效率问题。系统为了提高效率就采用了分离锁方案,表示图如下:

4)、怎样实现快速分流,即如何根据对象快速找到对应的SideaTable结构?

     4、散列表内存管理方式设计的数据结构

主要是Spinlock_t自旋锁、RefcountMap引用计数表、weak_table_t弱引用表。

1)、Spinlock_t自旋锁

Spinlock_t是一种忙等的锁,即如果锁被其他线程获取,则该线程一直探测这个锁是否有被释放,如果释放掉则第一时间去获取这个锁。其他锁在线程获取不到锁时会阻塞休眠,然后等到其它线程释放锁时唤醒当前线程。===>这就是自旋锁的特点。由此可知自旋锁适用于轻量访问。

2)、RefcountMap引用计数表

该表是用hash表实现的。使用hash表是为了提高查找效率,hash的插入和查找都是通过hash函数实现的,这就避免了for循环遍历。

其中,ptr是对象的地址,size_t就是获取到的存储引用计数的值的变量。

其中,weakly_referenced:标识该对象是否具有弱引用指针;deallocating:标识该对象是否正在进行dealloc操作。

3)、weak_table_t弱引用表

weak_entry_t数组中存储的就是指向该对象的weak指针的地址。

总结:Runtime维护了一个weak表,用于存储指向对象的所有weak指针。weak表其实是一个hash(哈希)表,Key是weak指针所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象的地址)数组。在weak指针所指向对象销毁时,会通过weak表找到该对象对应的weak指针的地址数组,然后遍历数组将所有weak指针置为nil。参考:http://www.cocoachina.com/ios/20170328/18962.html

 

讲了这么多实际上我没有整个的串联起来,不能从整体上理解内存管理

我把自己理解的先说一下:64位架构采用NONPOINTER_ISA这种内存管理方式,当引用计数很大,超出了isa指针对引用计数的存储能力,则使用SideaTables()哈希表方式管理引用计数。

????我还不理解NONPOINTER_ISA和TaggedPointer如何切换????

http://yulingtianxia.com/blog/2015/12/06/The-Principle-of-Refenrence-Counting/

https://www.jianshu.com/p/f32ef640e687

http://www.cocoachina.com/ios/20160503/16060.html

https://www.jianshu.com/p/5b5a322f0837

https://www.jianshu.com/p/bb384657f65a

第一篇把引用计数讲明白了,只是什么情况下使用point方式管理内存没有说清楚;第二篇说的我有点明白point方式了,就是isa指针不指向地址了,直接用于存储引用计数;第三篇也很不错;第四篇介绍了一个博客来了解point方案;第五篇简要说明了point方案

     5、ARC&MRC

1)、MRC

MRC是手动引用计数,包含的关键字有alloc、retain、release、retainCount、autorelease、dealloc。在MRC中重写dealloc方法需要手动去调用父类的dealloc方法。

在ARC中,retain、release、retainCount、autorelease这四个关键字不能出现,否则报错。

2)、ARC

ARC是自动引用计数,具有如下特点:

(1)、ARC是LLVM和Runtime协作的结果,如weak关键字实现的内存管理就是由Runtime实现的。

(2)、ARC中禁止手动调用retain、release、retainCount、dealloc,且重写dealloc方法时不能手动调用父类的dealloc方法。

(3)、ARC中新增weak、strong属性关键字。

3)、MRC和ARC的区别

(1)、MRC是手动管理内存,ARC是LLVM和Runtime协作实现的自动管理内存

(2)、MRC中可以调用引用计数相关的方法,而ARC中不可以调用

     6、MRC中引用计数方法的源码实现

1)、alloc实现

经过一系列调用,最终调用了c函数calloc。此时并没有设置引用计数为1,但是retainCount确实为1,为什么?这一点讲retainCount实现时说明。

2)、retain实现

SIDE_TABLE_RC_ONE不是1,而是一个偏移量,为什么这么做呢?因为在64位系统中引用计数不是占了8字节的所有位,前两位不是用于计数的。

3)、release实现

4)、retainCount实现

对象初始化时引用计数表中的值为0,retainCount值为1是因为取值时从一取值。为什么这么做??????博客中有,总结一下。

5)、dealloc实现

很重要,因为绝大多数引用计数相关的面试问题都与dealloc相关。

为什么dealloc主流程中要进行右侧那么多的判断才能执行object_dispose()函数的调用呢?因为如果有以上情况的其中之一,则object_dispose()函数会对weak、关联、ARC、引用计数进行相关清理操作。

object_dispose()实现如下

objc_destructInstance()实现如下

其中,hasCxxDtor?是判断当前对象有没有使用c++相关代码、有没有使用ARC,如果使用了需要做相应的清除操作。

clearDeallocating()实现如下

     7、弱引用

1)、一个weak变量是怎样被添加到弱引用表中的

上面尖括号是c++中的模板,分别对应老对象、新对象、在dealloc过程中是否可以crash,为什么这么做还没想清楚,跟c++用法有关吧。

上面if判断不影响注册weak变量,可以不看,而直接往下看。

当一个上图通过weak_entry_for_referent函数(下面展示其代码)获取对象对应的弱引用数组,如果获取到数组,则将指向对象的弱引用指针加入数组;如果未找到则创建弱引用数组,将该弱引用指针放到数组,并将数组加入弱引用表中。

2)、当一个weak指向对象释放时,weak变量是怎么处理的?

释放对象的同时将weak变量指向设置为nil。具体实现如下:

其中referent_id是将要被dealloc的对象,entry是获取到的弱引用数组。

 

     8、自动释放池

一个线程只有一个AutoreleasePoolPage双向链表,代码中调用的@autoreleasepool{}只是在链表的某个节点中增加了一个哨兵对象。每个@autoreleasepool{}的释放过程参考第三条“释放时刻”。参考网址http://blog.sunnyxx.com/2014/10/15/behind-autorelease/

1)、AutoreleasePool的实现原理是怎样的?

ARC下,我们使用@autoreleasepool{}来使用一个AutoreleasePool,随后编译器将其改写成下面代码:

               void *context = objc_autoreleasePoolPush();//context就是哨兵对象

               // {}中的代码,这里面的对象都会调用- autorelease消息,从而使对象被加入page节点的栈中。

               objc_autoreleasePoolPop(context);

而这两个函数都是对AutoreleasePoolPage的简单封装,所以自动释放机制的核心就在于这个类。

AutoreleasePoolPage::push(void)函数实现如下,即向节点的栈中加入一个哨兵对象,并返回哨兵的地址:

AutoreleasePoolPage::pop(void* ctxt)函数内部过程如下:

(1)、根据传入的哨兵对象找到对应位置

(2)、给上次push操作之后添加的对象依次发送release消息

(3)、回退next指针到正确位置

2)、自动释放池的结构

自动释放池是以栈为结点通过双向链表的形式组合而成;是和线程一一对应的。自动释放池的节点结构如下:

AutoreleasePool并没有单独的结构,而是由若干个AutoreleasePoolPage以双向链表的形式组合而成(分别对应结构中的parent指针和child指针)

AutoreleasePool是按线程一一对应的(结构中的thread指针指向当前线程)

(1)、AutoreleasePoolPage每个对象会开辟4096字节内存(也就是虚拟内存一页的大小),除了上面的实例变量所占空间,剩下的空间全部用来储存autorelease对象的地址

(2)、一个AutoreleasePoolPage的空间被占满时,会新建一个AutoreleasePoolPage对象,连接链表,后来的autorelease对象指针在新的page加入栈中

(3)、上面的id *next指针作为游标指向栈顶最新add进来的autorelease对象的下一个位置

所以,若当前线程中只有一个AutoreleasePoolPage对象,并记录了很多autorelease对象地址时内存如下图:

其中,下面部分是节点本身占用的内存,上面是自动释放池节点对应的栈,栈由高地址向低地址增长。图中的情况,这一页再加入一个autorelease对象就要满了(也就是next指针马上指向栈顶),这时就要执行上面说的操作,建立下一页page对象,与这一页链表连接完成后,新page的next指针被初始化在栈底(begin的位置),然后继续向栈顶添加新对象==>这就是autorelease函数的作用。所以,向一个对象发送- autorelease消息,就是将这个对象加入到当前AutoreleasePoolPage的栈顶next指针指向的位置;- autorelease消息的原理如下图。

3)、释放时刻

每当进行一次objc_autoreleasePoolPush调用时,runtime向当前的AutoreleasePoolPage中add进一个哨兵对象,值为0(也就是个nil),那么这一个page就变成了下面的样子:

objc_autoreleasePoolPush的返回值正是这个哨兵对象的地址,被objc_autoreleasePoolPop(哨兵对象)作为入参,于是:

(1)、根据传入的哨兵对象地址找到哨兵对象所处的page

(2)、在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次- release消息,并向回移动next指针到正确位置

(3)、补充2:从最新加入的对象一直向前清理,可以向前跨越若干个page,直到哨兵所在的page

刚才的objc_autoreleasePoolPop执行后,最终变成了下面的样子:

4)、嵌套的AutoreleasePool

知道了上面的原理,嵌套的AutoreleasePool就非常简单了,pop的时候总会释放到上次push的位置为止,多层的pool就是多个哨兵对象而已,就像剥洋葱一样,每次一层,互不影响。

5)、其他Autorelease相关知识点

使用容器的block版本的枚举器时,内部会自动添加一个AutoreleasePool:

[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {

    // 这里被一个局部@autoreleasepool包围着

}];

当然,在普通for循环和for in循环中没有,所以,还是新版的block版本枚举器更加方便。for循环中遍历产生大量autorelease变量,或者alloc图片数据等内存消耗较大的场景时,需要手加局部AutoreleasePool==>这是AutoreleasePool的应用场景。

     9、循环引用

1)、循环引用的分类

(1)、自循环引用

(2)、相互循环引用

(3)、多循环引用

2)、破除循环引用有哪些方案?

(1)、__weak

(2)、__block:MRC下,__block修饰对象不会增加其引用计数,避免了循环引用;ARC下,__block修饰对象会被强引用,无法避免循环引用,需手动解环。

(3)、__unsafe_unretained:如果被修饰对象在某一时机被释放,会产生悬垂指针!

3)、你遇到过哪些循环引用问题,如何解决?

(1)、Block使用示例

        Block章节再整理?????????

(2)、NSTimer使用示例

        使用NSTimer时,不仅使用者会强引用NSTimer,而且RunLoop也会强引用NSTimer,此时通过让使用者会弱引用NSTimer来解除循环引用是不可取的,因为RunLoop常驻内存(特别是主线程的RunLoop),会造成使用者不能释放,如下图所示:

        如果定时器是单次使用的定时器,可以在回调方法中销毁NSTimer来破除RunLoop对NSTimer的强引用;如果定时器是重复使用的定时器,可以在使用者dealloc中销毁NSTimer来破除RunLoop对NSTimer的强引用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值