- 小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
1. 自动释放池
1.1 自动释放池介绍
自动释放池是OC中的一种内存自动回收机制,它可以延迟加入AutoreleasePool中的变量release的时机,即当我们创建了一个对象,并把他加入到了自动释放池中时,他不会立即被释放,会等到一次runloop结束或者作用域超出autoreleasepool{}之后再被释放
1.2 自动释放池底层原理
要探究自动释放池的底层结构,那么就要用clang或者xcrun将代码转换成cpp文件。
转化完之后可以看到,@autoreleasepool{}变成了{__AtAutoreleasePool __autoreleasepool; }
在生成的cpp文件中搜索__AtAutoreleasePool,发现其是一个结构体,那么也就相当于,@autoreleasepool{}调用了__AtAutoreleasePool的构造和析构函数:objc_autoreleasePoolPush()和 objc_autoreleasePoolPop(atautoreleasepoolobj);
下断点后发现objc_autoreleasePoolPush在objc源码中,接下来就去源码中探索。
objc_autoreleasePoolPush
这里可以看到objc_autoreleasePoolPush和objc_autoreleasePoolPop分别调用了AutoreleasePoolPage::push();和AutoreleasePoolPage::pop(ctxt);。那么这里的AutoreleasePoolPage是什么呢?
点进来看到AutoreleasePoolPage继承自AutoreleasePoolPageData,并且可以从注释这里看到,自动释放池和线程有一定的关系,而且是栈结构存储,里面储存着指针。每个指针指向要释放的对象或者是POOL_BOUNDARY,也就是自动释放池的边界。自动释放池是一个类,进行不断的压栈对象,意味着会不断进栈和出栈,但是这里不能无限制的出栈。如果一直不断的出栈,那么指针就会不断的平移和kill,如果没有边界的话,那么就会破坏别人的内存,之后如果访问被破坏的对象就会造成野指针的问题。这里还可以看到自动释放池是一个doubly—linkes list of pages,也就是双向链表。
再来看AutoreleasePoolPageData,看到这里的一些属性和构造函数。
magic: 用来校验 AutoreleasePoolPage 的结构是否完整;next: 指向最新添加的 autoreleased 对象的下一个位置,初始化时指向 begin() ;thread: 指向当前线程;parent: 指向父结点,第一个结点的 parent 值为 nil ;child: 指向子结点,最后一个结点的 child 值为 nil ;depth: 代表深度,从 0 开始,往后递增 1;hiwat: 代表 high water mark 最大入栈数量标记
接下来探索autoreleasepool的压栈情况,将build settings的 automatic reference counting设为No。
创建一个nsobject对象并调用autorelease,引入并调用_objc_autoreleasePoolPrint方法后运行。
运行后得到下面的的打印。这里的对象一个为哨兵对象,一个为自己添加的NSObject对象。
接下来看objc_autoreleasePoolPush。这里会走到autoreleaseFast里面。
看到autoreleaseFast,这里会进行判断。
- 如果page存在并且没有满,那么调用
page->add添加这个对象到page里面, - 如果page存在但是page满了,那么调用
autoreleaseFullPage. - 如果page不存在则调用
autoreleaseNoPage。 那么第一次来的话,就会调用autoreleaseNoPage。
autoreleaseNoPage
看到autoreleaseNoPage。这里主要是进行AutoreleasePoolPage的创建,然后将当前页面设为hotpage,添加哨兵对象,最后添加要添加的对象。
看到AutoreleasePoolPage的构造函数,这里会调用AutoreleasePoolPageData的构造函数来进行属性的初始化。然后如果下面判断如果parent存在,那么就将parent的child设为自己。这里可以看到外面传的是nil,所以parent是不存在的。
这里还有调用begin,到begin里面打下断点后运行。
这里看到this是AutoreleasePoolPage,并且大小为56。
再看到AutoreleasePoolPage的结构,发现大小确实为56。
这里结构体创建是占用堆区内存,static修饰的在全局区不占堆区内存。这里一个uint32_t 4个字节,4个uint32_t就是16个字节。
也就是说,这里从成员变量之下开始插入。结构如下:
这里的56就是结构体的大小,之后0x10480a038是哨兵对象,然后就是自己添加的要销毁的对象。
AutoreleasePoolPageData里面还调用了objc_thread_self,这里调用tls_get_direct获取当前线程。
autoreleaseFullPage
接下来看到当autoreleasePage满的情况下,调用的autoreleaseFullPage。 这里递归找到最后一个子页面,然后创建新的页面,并且把新的页面设为HotPage,最后添加要添加的释放对象。

形成了如下的结构,分页是因为这里会不断的出栈入栈,对内存操作非常频繁,如果只有一个页面,那么所有的对象都在这一个页面里面,那么操作就会变得繁杂,管理变得不便。并且如果这里局部发生问题,那么就会影响整个页面。而如果是分页的话,就只会影响局部的页面。并且,分页不需要在内存上连续。
那么这里什么时候会满呢。这里for循环504次,看到这里页面有个标签full。
这里调整为505后,发现分页了。并且可以看到第二页这里是没有哨兵对象了的。所以这里可以看出,其实一页可以存505个对象的,但是由于第一页多了一个哨兵对象,所以只能存504个对象。所以页的大小为505 * 8 + 56 = 4096,也就是4k。
page->add
这里主要做的就是通过内存平移储存objc。
objc_autoreleasePoolPop
这里会判断hotpage是否存在,然后对页面进行移动,调整,然后调用popPage移除。
popPage会拿到parent页面,将当前页面删除之后将parent页面设为HotPage
page->kill 里面会删除page。
1.3 自动释放池能否嵌套使用
这里嵌套之后运行,发现可以正常运行,里面嵌套的自动释放池被添加到外层的自动释放池里面,并且在作用域结束之后被释放了。
1.4 自动释放池的入池条件
MRC情况下
这里可以看到,在MRC情况下,如果对象没有调用 autorelease方法,是不会被添加到自动释放池里面的。
ARC情况下
这里可以看到,在ARC情况下,如果对象调用 alloc方法,是不会被添加到自动释放池里面的。其实在ARC里面,以alloc,new,copy,mutablecopy命名生成的对象是不会被添加到自动释放池里面的。