前言
自动释放池这个东西我们新建工程在main文件里面就有@autoreleasepool的相关代码,那么它到底是啥,它有啥用途?今天我们就对它进行一番探索。
autoreleasepool源码引入
还原cpp代码
我们xcrun或clang一下mian文件得到相关cpp代码:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
}
return 0;
}
搜索__AtAutoreleasePool这个结构体得到代码:
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
这个意思是在进入作用域会调用__AtAutoreleasePool结构体的构造函数,进而调用objc_autoreleasePoolPush函数,出了作用域会调用结构体的析构函数,进而执行objc_autoreleasePoolPop函数。
在探索objc_autoreleasePoolPush和objc_autoreleasePoolPop之前我们关注下__AtAutoreleasePool的来源是怎么从@autoreleasepool变过来的
llvm查看__AtAutoreleasePool结构体
全局搜索llvm的代码找到__AtAutoreleasePoolx相关代码如下:
以上是结构体定义的地方
以上解释了
@autoreleasepool被替换成了/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;的原因
汇编探索@autoreleasepool
打开汇编,对@autoreleasepool打上断点跟进流程:
于是我们定位到objc_autoreleasePoolPush来自libobjc.A.dylib
autoreleasePoolPush源码分析
打开libobjc源码搜索objc_autoreleasePoolPush:
AutoreleasePoolPage这个是啥,点击去发现是个类,继承自AutoreleasePoolPageData:
点进自AutoreleasePoolPageData去:
查看AutoreleasePoolPageData的注释部分
这段话有几个关键点:
- 跟
线程相关,是个栈结构(先进后出),存的是8字节的指针 - 存的指针是一个对象或者是一个boundary的
哨兵边界 token指向边界用来释放池子双向链表结构TSL存储hot页
查看AutoreleasePoolPageData其中的成员变量作用如下:
magic⽤来校验AutoreleasePoolPage的结构是否完整;next指向最新添加的autoreleased对象的下⼀个位置,初始化时指向begin();thread指向当前线程;parent指向⽗结点,第⼀个结点的parent值为nil;child指向⼦结点,最后⼀个结点的child值为nil;depth代表深度,从0开始,往后递增1;hiwat代表high water mark最⼤⼊栈数量标记,这个一般用不到
AutoreleasePoolPage结构体大小
分析
magic_t结构体大小:
M0,M1_len是全局区,不占堆区空间,m[4]占用4个4字节空间,所以magic_t结构体大小是16个字节
AutoreleasePoolPage的其余成员的大小:
next,thread,parent,child都是指针占用8字节,depth占用4字节,hiwat占用4字节
所以AutoreleasePoolPage的结构体大小是16+8*4+4+4=56字节
objc_autoreleasePoolPush流程
小概率开启DebugPoolAllocation宏的话每次都会新开一页,我们就不进行探索这个流程了
大概率会走autoreleaseFast流程,点进去:
其中
hotPage热页是存在TSL的:
autoreleaseFast流程:
TSL中获取热页- 热页存在并且没满的话走
add(obj)流程 - 热页存在并且满的话走
autoreleaseFullPage流程 - 热页不存在走
autoreleaseNoPage流程
第一次进来热页不存在,我会进入autoreleaseNoPage流程:
autoreleaseFullPage流程:
add(obj)流程看就是正常的压栈流程:
objc_autoreleasePoolPop流程
pop的流程就是push的逆流程,主要是出栈,更新热页,kill当前页,kill相关child页
删除页,需要先将页中存储的对象删除掉- 如果该页为空,
直接删除,并且设置上一页为当前页 - 如果该页已经是第一页了,就说明池子已空,就
清空池子 - 如果仍然有下一页,就先把下一页
删掉 lessThanHalfFull的判断应该是为了删除后下次新加入对象可以节省分配页的操作
- 这里就是删
掉每一页,栈顶开始删 - 删除的方式就是设置为
nil
autoreleasePoolPop的边界探索
autoreleasePoolPop会有一个满页的情况,那么要压入多少个对象会满出来呢,带着这个问题我们进行探索。
首先导出一个打印autoreleasePoolPop结构的函数方便调试:
extern _objc_autoreleasePoolPrint(void);
我们尝试压入一个对象:
打印结果发现压入了只有一个对象,并且并不是我们想要的对象,这是什么原因呢?
原来我们现在是ARC环境,ARC加入到自动释放池有一定条件,我们先把工程改成MRC环境试试
MRC
设置里面自动引用计数开关设置为关
MRC需要手动发送
autorelease消息,这时候我们看到我们的对象加入到了自动释放池,另外一个自然是哨兵对象了。
我们多加入一些对象探索一个池能加多少对象:
@autoreleasepool {
for (int i = 0; i < 504; i ++) {
SPObject *sp = [[SPObject alloc] autorelease];
}
_objc_autoreleasePoolPrint();
}
我们加入504个对象后出现了full标记,此时page数目还是1个。
加入504改成505结果如下:
这时候就分配了新的page,并且新的page是热页同时存储了一个对象(这一个没有哨兵对象),同时第一页的热页标记去掉了。
既然第二页没有存哨兵对象,那么我们第二页应该可以加入505个对象:
第一页:
第二页:
第三页:
运行结构跟我们猜想的一样。看到这里读者是否好奇为什么是504,505这样的数字,能从代码中得到验证不?于是我们查看源码找到了page大小的定义:
page大小为2^12=4096=4k个字节,其中开头56个字节存的是magic,next,thread,parent,child,depth,hiwat,第一页需要额外存一个哨兵对象指针占8字节,所以第一页能存(4096 - 56 - 8) / 8 = 504个对象,非第一页能存(4096 - 56) / 8 = 505个对象
ARC
obj方法的实现就是alloc
我们发现alloc生成的对象没有加入到自动释放池,而自定义方法obj生成的对象可以加入到自动释放池。
读者可以对方法名字进行探索,可以得到如下结论:使用alloc、new、copy、mutableCopy等方法进行初始化时,由系统管理对象,在适当的位置release。
对象的压栈
我们可以看到对象加入自动释放池的流程是objc_retainAutorelease-objc_autorelease-autorelease-rootAutorelease-rootAutorelease2-AutoreleasePoolPage::autorelease-autoreleaseFast
递归调用
运行以下代码测试是否可以递归执行@autoreleasepool
@autoreleasepool {
SPObject *sp = [SPObject obj];
NSLog(@"对象:%@",sp);
@autoreleasepool {
SPObject *sp2 = [SPObject obj];
NSLog(@"对象2:%@",sp2);
_objc_autoreleasePoolPrint();
}
_objc_autoreleasePoolPrint();
}
结论是可以递归调用
总结
@autoreleasepool被llvm替换成__AtAutoreleasePool结构体代码,在进入作用域执行objc_autoreleasePoolPush,出作用域执行objc_autoreleasePoolPopAutoreleasePoolPage的结构继承AutoreleasePoolPageData,其中的成员变量magic,next,thread,parent,child,depth,hiwat占56个字节- 自动释放池的结构:
- 跟
线程相关,是个栈结构(先进后出),存的是8字节的指针 - 存的指针是一个对象或者是一个boundary的
哨兵边界 token指向边界用来释放池子双向链表结构TSL存储hot页
- 跟
- 第一页能存
504个指针对象加一个哨兵,其他页可以存505个指针对象 ARC环境加入到自动释放池跟初始化方法名有关,alloc、new、copy、mutableCopy等方法由系统管理对象,在适当的位置release- 可以
递归调用