前言
内存管理里的篇章还有个自动释放池(AutoreleasePool),它是一种内存回收机制,放入autoreleasePool中的对象会延迟释放。那么这个自动释放池是什么,又有怎样的特性?下面我们将对它进行深入分析
结构分析
-
在
main.m有@autoreleasepool,通过clang查看如下:在
C++源码中@autoreleasepool是个__autoreleasepool的构造函数,通过搜索AtAutoreleasePool发现其代码如下:struct __AtAutoreleasePool { __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();} ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);} void * atautoreleasepoolobj; };- 通过观察结构,发现在构造函数时会调用
objc_autoreleasePoolPush,析构时调用objc_autoreleasePoolPop,再结合OC断点和汇编,最终确定代码在libobjc.A.dylib,于是在objc4-818.2源码中分析核心代码
- 通过观察结构,发现在构造函数时会调用
-
通过搜索
objc_autoreleasePoolPush和objc_autoreleasePoolPop发现,他们的源码比较类似都是调用AutoreleasePoolPage中的函数,一个是调用push,一个是调用pop:void * objc_autoreleasePoolPush(void) { return AutoreleasePoolPage::push(); } void objc_autoreleasePoolPop(void *ctxt) { AutoreleasePoolPage::pop(ctxt); }下面先分析下
AutoreleasePoolPage
AutoreleasePoolPage
-
- 找到
AutoreleasePoolPage,在方法的开头可以看到注释:
- 在注释中可以得到:自动释放池存放的是指针,这些指针要么指向对象,要么指向释放池的边界
POOL_BOUNDARY,池中还有一个哨兵对象,自动释放池是一个双向链表。
- 找到
-
AutoreleasePoolPage有自己的构造函数和析构函数:
AutoreleasePoolPageData
-
AutoreleasePoolPage继承AutoreleasePoolPageData,它是一个结构体相关参数作用如下:
magic:用来校验AutoreleasePoolPage的结构是否完整next:指向最新添加的autoreleased对象下一个位置,就是这个对象初始化时指向的begin()thread:指向当前线程parent:指向父结点,第一个节点的parent值为nilchild:指向子结点,最后一个结点的child值为nildepth:代表深度,从0开始往后递增1。也就是每增一页就递增1hiwat:代表high water mark,最大入栈数量标记
-
下面在代码中分析
AutoreloeasePool的结构
代码分析
-
创建一个空的工程,将环境改成
MRC然后在@autoreleasePool中加入对象,并打印autoreleasePool中的相关内容:extern void _objc_autoreleasePoolPrint(void); int main(int argc, char * argv[]) { @autoreleasepool { NSObject *obj = [[NSObject alloc] autorelease]; _objc_autoreleasePoolPrint(); } return 0; }-
_objc_autoreleasePoolPrint是在objc源码中是一个打印自动释放池内容的函数。
-
@autoreleasepool是自动释放池,autorelease是将对象加入自动释放池
-
-
打印结果如下:
- 可以观察到自动释放池前面有
56字节的一些相关初始化成员变量,然后有个8字节的哨兵对象,之后才是加入自动释放池的对象。
- 可以观察到自动释放池前面有
首页结构图
-
根据上面的分析,
autoreleasePool第一页的结构如下图
自动释放池是怎么加入销毁对象的,一页能够加入多少销毁对象呢,以及对象的释放在什么时候?下面我们将从原理去分析这些特性
原理
push
-
首先来看看
push代码static inline void *push() { id *dest; if (slowpath(DebugPoolAllocation)) { // Each autorelease pool starts on a new pool page. dest = autoreleaseNewPage(POOL_BOUNDARY); } else { dest = autoreleaseFast(POOL_BOUNDARY); } ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY); return dest; }不看
debug的代码,于是可以定位到autoreleaseFast代码 -
autoreleaseFast代码如下:static inline id *autoreleaseFast(id obj) { AutoreleasePoolPage *page = hotPage(); // 获取hotpage if (page && !page->full()) { // 如果page存在且没有存满,则添加对象 return page->add(obj); } else if (page) { // 存满则新增 return autoreleaseFullPage(obj, page); } else { // page不存在,则创建page return autoreleaseNoPage(obj); } }- 从
hotPage取到page后主要分为三种情况:-
page存在且没有满,则添加
-
page满了,则调用autoreleaseFullPage方法分页后添加
-
page不存在就调用autoreleaseNoPage进行相关创建并添加操作
-
下面针对这三种情况进行详细分析
- 从
add
-
add代码的核心是个内存平移的操作next指针最开始指向begin位置,当进来一个obj,next就会往下平移一个单位直到存满next指针平移过程如下图:
判断存满full
-
首先来看看存满的条件
full:- 当
next指向end位置,也就是page的末尾时就代表存满了,可以看到size的最大值是2^12也就是4096,由于里面的成员变量占56字节,哨兵对象占8字节,所以可以添加的对象占4032字节,也就是可以添加504个销毁对象。
- 当
-
代码验证:
-
当添加第
505个时就会进行分页,打印如下- 第
505个对象添加在了新的page中,新的page中处理初始化需要的成员变量外,并没有哨兵对象,也就是新增的页面占用4040个字节,也就是 可以存储505个对象
- 第
-
疑问:为什么第二页没有哨兵对象?
- 答:因为哨兵对象有"监控"
page边界的作用,第一页已经满了已经不需要add操作,所以它可以去第二页工作,由于没有存满之前都在当前页工作,所以只需要一个哨兵对象就足够了
- 答:因为哨兵对象有"监控"
-
结论:
1. 第一页最多可以存储504个对象,新增页最多存储505个对象
2. 新增页没有哨兵对象
autoreleaseFullPage
-
当存满后会走
autoreleaseFullPage方法- 此处主要是
do-while循环中找到最后一张page- 如果没有满,则将
page设置成hot,再调用add添加obj - 如果满了,就调用
AutoreleasePoolPage创建新page,然后再add添加obj。
- 如果没有满,则将
- 此处主要是
-
AutoreleasePoolPage方法主要是进行page初始化,并设置成员变量的值,如果有parent,则设置parent的child为自己
autoreleaseNoPage
-
当没有取到
hotPage,也就是第一个page不存在时就会进行创建:- 创建的过程和上面存满的过程一样,但第一个界面创建后会先添加一个
哨兵对象,然后再添加销毁对象
- 创建的过程和上面存满的过程一样,但第一个界面创建后会先添加一个
pop
-
pop主要代码如下- 如果
token指向哨兵对象,则说明第一页是空的,然后做一些相应处理 popPageDebug方法最终会走到popPage函数,再查看popPage的实现
- 如果
popPage
代码如下
-
popPage首先会调用releaseUntil将对象进行release:
- 这块代码主要是在
while循环中进行内存平移取到对象,然后调用objc_release方法进行释放 - 平移取对象的过程如下图:
-
- 对象释放后,然后当前页是空的,就会找到
parent,然后将当前页kill,并将parent设置成hotPage
- 对象释放后,然后当前页是空的,就会找到
-
- 如果是
page是第一页,则直接kill,并将hotPage设置为nil
- 如果是
-
- 如果当前
page有child:
- 如果当前页半满,则将
child直接kill - 如果
child还有child,则将child的child直接kill
- 如果当前
kill
-
page销毁的核心是kill函数,它的实现如下:
kill,流程比较简单,就是获取当前页面的child,从最后一张开始page开始delete,直到当前页才停止
整个
pop过程是:先将销毁对象release,然后从最后一张开始删除,直到第一张。第一张page销毁对象release后将hotPage置为nil
多page结构图
相关题目
-
AutoreleasePool能够嵌套使用吗?
- 使用代码测试如下
int main(int argc, char * argv[]) { @autoreleasepool { NSObject *obj1 = [[NSObject alloc] autorelease]; NSLog(@"obj1 : %@", obj1); @autoreleasepool { NSObject *obj2 = [[NSObject alloc] autorelease]; NSLog(@"obj2 : %@", obj2); _objc_autoreleasePoolPrint(); } _objc_autoreleasePoolPrint(); } return 0; }- 打印结果:
-
从结果中可以看出,第一次打印里面有两个哨兵对象,也就是包含的自动释放池被压进了外面的自动释放池,所以打印会有两个哨兵对象,而出作用域后,里面的自动释放池释放了,所以下面的打印只有一个哨兵对象和
obj1了。 -
答:自动释放池可以嵌套使用
-
ARC中的对象怎么加入自动释放池的?
- 我们知道在
MRC中,创建的对象需要加上autorelease才可以入池,在ARC中没有autorelease,可以使用__autoreleasing修饰,将对象加入自动释放池:
int main(int argc, char * argv[]) { @autoreleasepool { NSObject *obj1 = [NSObject alloc] ; NSLog(@"obj1 : %@", obj1); NSObject * __autoreleasing obj2 = [NSObject alloc] ; NSLog(@"obj2 : %@", obj2); _objc_autoreleasePoolPrint(); } return 0; }打印结果如下:
ARC入池主要有以下情形:如果不是alloc/new/copy/mutableCopy创建,则自动将返回对象注册到Autoreleasepool