自动释放池的内存管理
首先我们来看一下如下代码的运行情况可以看到内存在不断的增加
现在我们在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