自动释放池的内存管理
首先我们来看一下如下代码的运行情况可以看到内存在不断的增加
现在我们在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个字节是为了保存页的基本结构数据用的。
将代码中for的次数改为505次,打印如下
- 打印得到的
0x103009000是页表的起如地址;0x103009038是哨兵对象的地址;- 后面的依次是加入自动释放池的对象的next指向的地址;
再来看一下第二页可以存放多少个对象此时发现,第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

京公网安备 11010502036488号