从数据结构理解Autoreleasepool 原理

2,389 阅读5分钟

一: 原理

NSAutoreleasePool实际上是个对象引用计数自动处理器,使用引用计数的方法来管理对象的存活。当对象被创建时,引用计数被设成1。可以给对象发送retain消息,让对象对自己的引用计数加1。当对象接受到release消息时,对象就会对自己的引用计数进行减1,当引用计数到了0,对象就会调用自己的dealloc处理,对象就被销毁。

NSAutoreleasePool实际上是个对象引用计数自动处理器,使用引用计数的方法来管理对象的存活。当对象被创建时,引用计数被设成1。可以给对象发送retain消息,让对象对自己的引用计数加1。当对象接受到release消息时,对象就会对自己的引用计数进行减1,当引用计数到了0,对象就会调用自己的dealloc处理,对象就被销毁。

二: Autorelease对象什么时候释放

如果答案是“当前作用域大括号结束时释放”,显然木有正确理解Autorelease机制。 在没有手加Autorelease Pool的情况下,Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop

三: 核心代码

void *context = objc_autoreleasePoolPush();

objc_autoreleasePoolPop(context);

这两个函数都是对AutoreleasePoolPage的简单封装,所以自动释放机制的核心就在于这个类。@autoreleasepool {} 在编译时 @autoreleasepool {} 被转换为一个__AtAutoreleasePool ,通常这个结构体会在初始化时调用 objc_autoreleasePoolPush()方法,在析构时调用 objc_autoreleasePoolPop () 方法。而这些方法都是对AutoreleasePoolPage的简单封装。AutoreleasePool并没有单独的结构,而是由若干个AutoreleasePoolPage以双向链表的形式组合而成,分别对应结构中的parent指针和child指针。parent 指向父结点,第一个结点的 parent 值为 nil;child 指向子结点,最后一个结点的 child 值为 nil )一个AutoreleasePoolPage的空间被占满时,会新建一个AutoreleasePoolPage对象,通过parent和child指针连接成链表,后来的autorelease对象在新的page加入。

四:从数据结构理解AutoreleasePoolPage

1.结构图

AutoreleasePoolPage是一个C++实现的类

AutoreleasePoolPage.jpg AutoreleasePool并没有单独的结构,而是由若干个AutoreleasePoolPage以双向链表的形式组合而成(分别对应结构中的parent指针和child指针)。 AutoreleasePool是按线程一一对应的(结构中的thread指针指向当前线程)。 AutoreleasePoolPage每个对象会开辟4096字节内存(也就是虚拟内存一页的大小),除了上面的实例变量所占空间,剩下的空间全部用来储存autorelease对象的地址。

2.往自动释放池里添加对象的过程

AutoreleasePoolPage对象的空间用来储存autorelease对象的地址。图中的 *next指针作为游标指向栈顶最新add进来的autorelease对象的下一个位置。 一个AutoreleasePoolPage的空间被占满时,会新建一个AutoreleasePoolPage对象,连接链表,后来的autorelease对象在新的page加入。

3.push过程

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

image.png

图中的情况,这一页再加入一个autorelease对象就要满了(也就是next指针马上指向栈顶),这时就要执行上面说的操作,建立下一页page对象,与这一页链表连接完成后,新page的next指针被初始化在栈底(begin的位置),然后继续向栈顶添加新对象。

所以,向一个对象发送- autorelease消息,就是将这个对象加入到当前AutoreleasePoolPage的栈顶next指针指向的位置

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

image.png

4.pop过程

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

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

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

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

image.png

5.哨兵对象

POOL_SENTINEL(哨兵对象)只是 nil 的别名

#define POOL_SENTINEL nil

每创建一个池子,会在首部创建一个哨兵对象,作为标记。最外层池子的顶端会有一个next指针。当链表容量满了,就会在链表的顶端,并指向下一张表。 在每个自动释放池初始化调用 objc_autoreleasePoolPush 的时候,都会把一个 POOL_SENTINEL push 到自动释放池的栈顶,并且返回这个 POOL_SENTINEL 哨兵对象。

6.总结

每调用一次 push 操作就会创建一个新的 autoreleasepool ,实质上每当进行一次objc_autoreleasePoolPush调用时,runtime向当前的AutoreleasePoolPage中add进一个哨兵对象,值为0(也就是个nil)(往 AutoreleasePoolPage 中的 next 位置插入一个 POOL_SENTINEL),当需要release时,objc_autoreleasePoolPop(哨兵对象)作为入参,根据传入的哨兵对象地址找到哨兵对象所处的page。在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次- release消息,并向回移动next指针到正确位置。