自动释放池的内存管理

首先我们来看一下如下代码的运行情况

可以看到内存在不断的增加

现在我们在for循环内部加一个自动释放池

可以看到加了自动释放池后,内存基本稳定在一个值

从以上的对比可以发现,自动释放池可以对创建的对象的内存进行管理,对于使用完毕的对象,可以及时的释放掉

再来看一下如下的代码的执行情况

从以上代码的执行结果可以看到,基本数据类型alloc/init、new、copy初始化的对象以及isTaggedPointer对象的创建并不会引起内存的增加,这是因为自动释放池不会对这些方式创建的变量进行管理。基本数据类型的局部变量在栈区,由系统进行管理,alloc/init、new、copy的初始化的对象ARC管理,isTaggedPointer小对象类型无需对内存进行管理,它的值存在指针中。除了以上提到的小对象alloc/init、new、copy初始化的对象以外的其他对象都可以由自动释放池进行内存的管理.
这里有一个iOS交流圈有兴趣的可以来了解一下:891 488 181 不管你是大牛还是小白都欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!

自动释放池的原理

我们使用clang对main.m文件进行编绎,得到一个main.cpp文件

xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main.cpp
打开main.cpp文件,找到main函数

可以看到@autoreleasepool {}这一句代码在底层被编绎成了__AtAutoreleasePool __autoreleasepool;这一句代码。

在main.cpp文件找到__AtAutoreleasePool

__AtAutoreleasePool是一个结构体,内部实现了一个构造函数objc_autoreleasePoolPush()创建了一个自动释放池对象atautoreleasepoolobj和一个析构函数objc_autoreleasePoolPop(atautoreleasepoolobj).

由此说明自动释放池的使用就是将作用域中的代码包含在__AtAutoreleasePool构造函数析构函数中,由atautoreleasepoolobj对象对作用域的内存进行管理.

通过查看汇编代码我们也能验证这一点

@autoreleasepool源码探究

我们打开一份objc4-781的源码,全局搜索objc_autoreleasePoolPush,找到它的实现

底层调用了AutoreleasePoolPage::push()

autoreleasepool的数据结构

我们继续跟进源码,查看AutoreleasePoolPage的数据结构

AutoreleasePoolPage继承自AutoreleasePoolPageData

  • magic:用来校验AutoreleasePoolPage结构是否完整;
  • next:指向最新添加的autoreleased对象的下一个位置,初始化时指向begin;
  • thread:指向当前线程;
  • parent:指向父结点,第一个AutoreleasePoolPage结点的父结点为nil;
  • child:指向子结点,最后一个AutoreleasePoolPage结点的子结点为nil;
  • depth:当前结点的深度,从0开始,往后递增;
  • hiwat:代表hige water mark最大入栈数量标记;

AutoreleasePoolPage的数据结构我们可以得出,autoreleasepool是一个以AutoreleasePoolPage分页管理的双向链表

objc_autoreleasePoolPush源码擦究

objc_autoreleasePoolPush底层调用的是AutoreleasePoolPage::push(),我们来查看一下push()的源码

  • POOL_BOUNDARY:是一个哨兵对象,它的值为nil;
  • autoreleaseNewPage:DebugPoolAllocation下的处理;
  • autoreleaseFast: 其他情况的处理.

下在查看autoreleaseFast()的源码

1.page->add(obj):当前页存在并且没满时的处理

next指向新加入的obj,并且next++,这样就可以把新加入的autorelease对象以next以单向链表的形式连接起来了。

2.autoreleaseFullPage(obj, page):页满情况的处理

  • 页满的情况下,do while循环找到最后一页,创建新的page;
  • setHotPage(page):将当前页标记为hotPage;
  • page->add(obj);:将obj加入页。

3.autoreleaseNoPage(obj):页不存在的处理

页不存在时,也是先创建新页,再将设置哨兵对象,最后将obj加入页中

AutoreleasePoolPage页表的内存结构

来看一下创建新页的源码

这里AutoreleasePoolPage的第一个结构体元素是magic ,而这里传的值是begin(),我们来看一下begin()的内容,并打断开看看

这里断点在begin()函数里面,这里的this就是AutoreleasePoolPage的结构体对象,在控制台打印了this的指针地址和对应结构体的大小,那么为什么是56呢?这里要断点住,@autoreleasepoo{}里面至少要创建一个对象.

AutoreleasePoolPage每个元素的所占的内存大小如下

由此得出,除了magic以外的其它元素所占的空间为40,再来看一下magic_t的内存情况

所以得出了AutoreleasePoolPage初始化时为什么内存空间平移56个字节,这56个字节是为了保存页的基本结构数据用的。

通过如下代码打印autoreleasepool的结构
  • 打印得到的0x103009000是页表的起如地址;
  • 0x103009038是哨兵对象的地址;
  • 后面的依次是加入自动释放池的对象的next指向的地址;
将代码中for的次数改为505次,打印如下

此时发现,第504个对象被放在了新的页中,说明第一页存放的页数为504页;

再来看一下第二页可以存放多少个对象

由此可以说明,第一页之后的页可以存放505个对象

为什么第一页是504个对象?为什么从第二页开始每页是505个对象

我们去查看一下判断页满的函数

我们继续跟进end()函数,最后找到一个宏定义的常数--4096

由此我们得到每一页的内存大小是4096个字节

通过以上我们可以得出以下算式:

  • 第一页的大小:504 * 8 + 8 + 56 = 4096;第一页一开始用56个字节存储了页的基本数据,用8字节存储哨兵,后面可以保存504个对象,每个对象指针占8字节;
  • 从第二页开始:505 * 8 + 56 = 4096;56个字节存储了页的基本数据,后面可以保存505个对象,每个对象指针占8字节;

最后得到如下图所示的双向链表结构

其实通过上面打印页的内存结构我们已经能够发现,哨兵只有在第一页打印了,确实,哨兵只有一个。这个哨兵是在自动释放池释放时起到一定边界作用的,当遇到哨兵时,释放停止.

objc_autoreleasePoolPop源码擦究

释放时调用的是AutoreleasePoolPage::pop(ctxt),pop函数实现如上图;

  • 释放时,将hotPage置为nil;将当前page置为coldPage;token指向当前页的begin();记录要释放的内存的开始位置和结束位置;调用popPage函数。
  • releaseUntil:释放对象;
  • page->kill():销毁当前页;
  • setHotPage(parent):将当前页的上一页设置为hotPage;
  • 如果当前页的子结点存在,也kill掉.

总结

自动释放池autorelease是一个以AutoreleasePoolPage为页,进行分页管理的双向链表;页的数据结构是AutoreleasePoolPageData;每一个autoreleasepool对象只有一个哨兵,哨兵放在第一页中;每一页的大小为4096字节;每一页的前56个字节存储页的AutoreleasePoolPageData结构体数据;第一页的第56往后8个字节存储哨兵,后面存储autorelease对象,总共可以存储504个;从第二页开始,每页可以存储505个对象;objc_autoreleasepoolpush是一个查找child,递增next,创建新页的过程;objc_autoreleasepoolpop是一个查找parent,递减next,释放对象,销毁page的过程,遇到哨兵对象即停止。

作者:形影相吊
链接:https://juejin.cn/post/6900043544304713735