在上一篇文章中,详细分析了IOS内存管理的内存布局、内存管理方案、引用计数等内容,本篇文章将继续上篇文章的内容探索自动释放池
autoreleasepool
的相关知识。
1、autoreleasepool初探
熟悉OC开发的都知道,在main
函数中就有@autoreleasepool
这样一个东西,其实这就是自动释放池。那么@autoreleasepool
的底层实现是什么样的呢?我们在命令行中使用 clang -rewrite-objc main.m -o main.cpp
让编译器重新改写这个文件,讲得到一个main.cpp文件,打开该文件,找到其中的main函数。
int main(int argc, const char *argv[])
{
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
}
return 0;
}
我们可以看到@autoreleasepool
转化成了__AtAutoreleasePool
这样一个结构体,那么意味着@autoreleasepool
的本质就是__AtAutoreleasePool
结构体。
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
这个结构体会在初始化时调用objc_autoreleasePoolPush()
方法,会在析构时调用
objc_autoreleasePoolPop
方法。
这就说明了main函数在实际工作的时候是这样的:
int main(int argc, const char *argv[])
{
void * atautoreleasepoolobj = objc_autoreleasePoolPush();
// do whatever you want
objc_autoreleasePoolPop(atautoreleasepoolobj);
return 0;
}
似乎一切都是围绕着objc_autoreleasePoolPush()
和objc_autoreleasePoolPop
这两个方法展开。那么我们来看下这两个方法的源码实现:
void *
objc_autoreleasePoolPush(void)
{
// 调用了AutoreleasePoolPage中的push方法
return AutoreleasePoolPage::push();
}
void
objc_autoreleasePoolPop(void *ctxt)
{
// 调用了AutoreleasePoolPage中的pop方法
AutoreleasePoolPage::pop(ctxt);
}
上面的两个方法看上去是对AutoreleasePoolPage
对应静态方法push
和pop
的封装。
2、AutoreleasePoolPage
在runtime中的源码(objc4-756.2版本)中找到了一段注释,这段注释对我们理解AutoreleasePoolPage
的底层结构会有所帮助。
- A thread's autorelease pool is a stack of pointers.
- Each pointer is either an object to release, or POOL_BOUNDARY which is an autorelease pool boundary.
- A pool token is a pointer to the POOL_BOUNDARY for that pool. When the pool is popped, every object hotter than the sentinel is released.
- The stack is divided into a doubly-linked list of pages. Pages are added and deleted as necessary.
- Thread-local storage points to the hot page, where newly autoreleased objects are stored.
翻译中文如下:
- 一个线程的自动释放池是一个指针的堆栈结构。
- 每个指针代表一个需要释放的对象或者POOL_BOUNDARY(自动释放池边界)
- 一个 pool token 就是这个 pool 所对应的 POOL_BOUNDARY 的内存地址。当这个 pool 被 pop 的时候,所有内存地址在 pool token 之后的对象都会被 release。
- 这个堆栈被划分成了一个以 page 为结点的双向链表。pages 会在必要的时候动态地增加或删除。
- Thread-local storage(线程局部存储)指向 hot page ,即最新添加的 autoreleased 对象所在的那个 page 。
从上面这段注释中我们可以知道自动释放池是一种栈的结构,遵循先进后出的原则,每一个自动释放池都是由一系列的AutoreleasePoolPage
组成的,而AutoreleasePoolPage
是以双向链表的形式连接起来。
2.1、AutoreleasePoolPage结构
来看一下AutoreleasePoolPage
的代码定义(只列出了关键代码,部分代码省略)。
class AutoreleasePoolPage
{
# define EMPTY_POOL_PLACEHOLDER ((id*)1)
# define POOL_BOUNDARY nil
static pthread_key_t const key = AUTORELEASE_POOL_KEY;
static uint8_t const SCRIBBLE = 0xA3; // 0xA3A3A3A3 after releasing
// AutoreleasePoolPage的大小,通过宏定义,可以看到是4096字节
static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
PAGE_MAX_SIZE; // must be multiple of vm page size
#else
PAGE_MAX_SIZE; // size and alignment, power of 2
#endif
static size_t const COUNT = SIZE / sizeof(id);
magic_t const magic;//16字节
id *next;//8字节
pthread_t const thread;//8字节
AutoreleasePoolPage * const parent;//8字节
AutoreleasePoolPage *child;//8字节
uint32_t const depth;//4字节
uint32_t hiwat;//4字节
}
- magic:用来校验
AutoreleasePoolPage
的结构是否完整。- *next:next指向的是下一个
AutoreleasePoolPage
中下一个为空的内存地址(新来的对象会存储到next
处),初始化时指向begin()
。- thread:保存了当前页所在的线程(一个
AutoreleasePoolPage
属于一个线程,一个线程中可以有多个AutoreleasePoolPage
)。- *parent:指向父节点,第一个
parent
节点为nil
。- *child:指向子节点,最后一个
child
节点为nil
。- depth:代表深度,从0开始,递增+1。
- hiwat:代表
high water Mark
最大入栈数。- SIZE:
AutoreleasePoolPage
的大小,值为PAGE_MAX_SIZE
,4096个字节。- POOL_BOUNDARY:只是
nil
的别名。在每个自动释放池初始化调用objc_autoreleasePoolPush
的时候,都会把一个POOL_SENTINEL
push到自动释放池的栈顶,并且返回这个POOL_SENTINEL
自动释放池边界。而当方法objc_autoreleasePoolPop
调用时,就会向自动释放池中的对象发送release
消息,直到第一个POOL_SENTINEL
。
在AutoreleasePoolPage
中的第一个对象是存储在next
后面,那么就形成如下图所示这样一个结构。
其中的56个字节存储的AutoreleasePoolPage
的成员变量,其他的区域存储加载到自动释放池的对象。
当next==begin()
时表示AutoreleasePoolPage
为空,当next==end()
的时表示AutoreleasePoolPage
已满。
2.2、AutoreleasePoolPage容量
在上一个小节的内容中我们分析了AutoreleasePoolPage
的结构,了解到每一个AutoreleasePoolPage
的大小是4096字节,其中56字节用于存储成员变量,剩下的区域存储加载到自动释放池的对象,那么似乎答案呼之欲出,一个AutoreleasePoolPage
可以存储(4096-56)/8=505个对象。但是有一个注意的点,第一个page存放的需要释放的对象的容量应该是504个,因为在创建page的时候会在next
的位置插入1POOL_SENTINEL
。
2.3、push方法
通过前面小节的分析,我们知道objc_autoreleasePoolPush
的本质就是调用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;
}
在push
方法中实际上调用的是autoreleaseFast
方法,并且首先将一个POOL_BOUNDARY
对象插入到栈顶。slowpath
表示小概率发生。
2.3.1、autoreleaseFast方法
如下是autoreleaseFast
方法的源码
static inline id *autoreleaseFast(id obj)
{
// hotPage就是当前正在使用的AutoreleasePoolPage
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
// 有hotPage且hotPage不满,将对象添加到hotPage中
return page->add(obj);
} else if (page) {
// 有hotPage但是hotPage已满
// 使用autoreleaseFullPage初始化一个新页,并将对象添加到新的AutoreleasePoolPage中
return autoreleaseFullPage(obj, page);
} else {
// 无hotPage
// 使用autoreleaseNoPage创建一个hotPage,并将对象添加到新创建的page中
return autoreleaseNoPage(obj);
}
}
autoreleaseFast
方法的代码很简单,只要是三个判断分支。
- 如果有
hotPage
且没有满,则调用add
方法将对象添加到hotPage中。否则执行第2步。- 如果有
hotPage
但是已经满了,则调用autoreleaseFullPage
方法初始化一个新页,并将对象添加到新的AutoreleasePoolPage
中。否则执行第3步。- 如果没有
hotPage
,则调用autoreleaseNoPage
方法创建一个hotPage
,并将对象添加到新创建的page
中
hotPage 可以理解为当前正在使用的 AutoreleasePoolPage。
2.3.2、add 添加对象
add
方法将对象添加到AutoreleasePoolPage
中。
id *add(id obj)
{
ASSERT(!full());
unprotect();
id *ret = next; // faster than `return next-1` because of aliasing
*next++ = obj;//将obj存放在next处,并将next指向下一个位置
protect();
return ret;
}
这个方法其实就是一个压栈操作,将对象添加到AutoreleasePoolPage
中,然后移动栈顶指针。
2.3.3、autoreleaseFullPage
autoreleaseFullPage
方法会重新开辟一个新的AutoreleasePoolPage
页,并将对象添加到其中。
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
// The hot page is full.
// Step to the next non-full page, adding a new page if necessary.
// Then add the object to that page.
assert(page == hotPage());
assert(page->full() || DebugPoolAllocation);
do {
// 如果page->child不为空,那么使用page->child
if (page->child) page = page->child;
// 否则的话,初始化一个新的AutoreleasePoolPage
else page = new AutoreleasePoolPage(page);
} while (page->full());
// 将找到的合适的page设置成hotPage
setHotPage(page);
// 将对象添加到hotPage中
return page->add(obj);
}
遍历找到未满的的page,如果没有找到则初始化一个新的page,并将page设置为hotPage,同时将对象添加到这个page中。
2.3.4、autoreleaseNoPage
如果当前内存中不存在hotPage
,就会调用autoreleaseNoPage
方法初始化一个AutoreleasePoolPage
。
id *autoreleaseNoPage(id obj)
{
// Install the first page.
// 初始化一个AutoreleasePoolPage
// 当前内存中不存在AutoreleasePoolPage,则从头开始构建AutoreleasePoolPage,也就是其parent为nil
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
// 将初始化的AutoreleasePoolPage设置成hotPage
setHotPage(page);
// Push a boundary on behalf of the previously-placeholder'd pool.
// 添加一个边界对象(nil)
if (pushExtraBoundary) {
page->add(POOL_BOUNDARY);
}
// Push the requested object or pool.
// 将对象添加到AutoreleasePoolPage中
return page->add(obj);
}
当前内存中不存在AutoreleasePoolPage
,则从头开始构建AutoreleasePoolPage
,也就是其parent
为nil
。初始化之后,将当前页标记为hotPage
,然后会先向这个page
中添加一个POOL_SENTINEL
对象,来确保在pop
调用的时候,不会出现异常。最后,将对象添加到自动释放池中。
2.4、pop方法
上面小节我们探索了objc_autoreleasePoolPush
,下面我们看看objc_autoreleasePoolPop
。
objc_autoreleasePoolPop
的本质就是调用pop
方法。
static inline void pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
// Popping the top-level placeholder pool.
if (hotPage()) {
// Pool was used. Pop its contents normally.
// Pool pages remain allocated for re-use as usual.
pop(coldPage()->begin());
} else {
// Pool was never used. Clear the placeholder.
setHotPage(nil);
}
return;
}
page = pageForPointer(token);
stop = (id *)token;
if (*stop != POOL_BOUNDARY) {
if (stop == page->begin() && !page->parent) {
// Start of coldest page may correctly not be POOL_BOUNDARY:
// 1. top-level pool is popped, leaving the cold page in place
// 2. an object is autoreleased with no pool
} else {
// Error. For bincompat purposes this is not
// fatal in executables built with old SDKs.
return badPop(token);
}
}
if (PrintPoolHiwat) printHiwat();
page->releaseUntil(stop);
// memory: delete empty children
if (DebugPoolAllocation && page->empty()) {
// special case: delete everything during page-per-pool debugging
AutoreleasePoolPage *parent = page->parent;
page->kill();
setHotPage(parent);
} else if (DebugMissingPools && page->empty() && !page->parent) {
// special case: delete everything for pop(top)
// when debugging missing autorelease pools
page->kill();
setHotPage(nil);
}
else if (page->child) {
// hysteresis: keep one empty child if page is more than half full
if (page->lessThanHalfFull()) {
page->child->kill();
}
else if (page->child->child) {
page->child->child->kill();
}
}
}
上面方法做了如下几件事:
- 调用
pageForPointer
获取当前token所在的page。- 调用
releaseUntil
方法释放栈中的对象,直到stop
。- 调用
child
的kill
方法。
2.4.1、pageForPointer找到page
pageForPointer
方法主要是通过通过内存地址的操作,获取当前token所在页的首地址。
static AutoreleasePoolPage *pageForPointer(uintptr_t p)
{
AutoreleasePoolPage *result;
uintptr_t offset = p % SIZE;
ASSERT(offset >= sizeof(AutoreleasePoolPage));
result = (AutoreleasePoolPage *)(p - offset);
result->fastcheck();
return result;
}
将指针与页面的大小(4096)取模,可以得到当前指针的偏移量。然后将指针的地址减偏移量便可以得到首地址。
2.4.2、releaseUntil释放对象
void releaseUntil(id *stop)
{
// Not recursive: we don't want to blow out the stack
// if a thread accumulates a stupendous amount of garbage
// 释放AutoreleasePoolPage中的对象,直到next指向stop
while (this->next != stop) {
// Restart from hotPage() every time, in case -release
// autoreleased more objects
// hotPage可以理解为当前正在使用的page
AutoreleasePoolPage *page = hotPage();
// fixme I think this `while` can be `if`, but I can't prove it
// 如果page为空的话,将page指向上一个page
// 注释写到猜测这里可以使用if,我感觉也可以使用if
// 因为根据AutoreleasePoolPage的结构,理论上不可能存在连续两个page都为空
while (page->empty()) {
page = page->parent;
setHotPage(page);
}
page->unprotect();
// obj = page->next; page->next--;
id obj = *--page->next;
memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
page->protect();
// POOL_BOUNDARY为nil,是哨兵对象
if (obj != POOL_BOUNDARY) {
// 释放obj对象
objc_release(obj);
}
}
// 重新设置hotPage
setHotPage(this);
#if DEBUG
// we expect any children to be completely empty
for (AutoreleasePoolPage *page = child; page; page = page->child) {
assert(page->empty());
}
#endif
}
因为AutoreleasePool
实际上就是由AutoreleasePoolPage
组成的双向链表,因此,*stop
可能不是在最新的AutoreleasePoolPage
中,即hotPage
,这时需要从hotPage
开始,一直释放,直到stop
,中间所经过的所有AutoreleasePoolPage
里面的对象都要释放。
对象的释放objc_release
方法请移步前面的文章iOS内存管理一:Tagged Pointer&引用计数。
2.4.3、kill方法
kill
方法删除双向链表中的每一个page
void kill()
{
// Not recursive: we don't want to blow out the stack
// if a thread accumulates a stupendous amount of garbage
AutoreleasePoolPage *page = this;
// 找到链表最末尾的page
while (page->child) page = page->child;
AutoreleasePoolPage *deathptr;
// 循环删除每一个page
do {
deathptr = page;
page = page->parent;
if (page) {
page->unprotect();
page->child = nil;
page->protect();
}
delete deathptr;
} while (deathptr != this);
}
3、自动释放池和线程
官方文档Using Autorelease Pool Blocks中关于自动释放池和线程的关系有如下一段描述。
Each thread in a Cocoa application maintains its own stack of autorelease pool blocks. If you are writing a Foundation-only program or if you detach a thread, you need to create your own autorelease pool block.
If your application or thread is long-lived and potentially generates a lot of autoreleased objects, you should use autorelease pool blocks (like AppKit and UIKit do on the main thread); otherwise, autoreleased objects accumulate and your memory footprint grows. If your detached thread does not make Cocoa calls, you do not need to use an autorelease pool block.
翻译成中文如下:
应用程序中的每个线程都维护自己的自动释放池块堆栈。如果您正在编写一个仅限基础的程序,或者正在分离一个线程,那么您需要创建自己的自动释放池块。
如果您的应用程序或线程是长生命周期的,并且可能会生成大量的自动释放对象,那么您应该使用自动释放池块(如在主线程上使用AppKit和UIKit);否则,自动释放的对象会累积,内存占用会增加。如果分离的线程不进行Cocoa调用,则不需要使用自动释放池块。
从上面这段秒速我们可以知道自动释放池和线程是紧密相关的,每一个自动释放池只对应一个线程。
4、AutoreleasePool和RunLoop
一般很少会将自动释放池和RunLoop
联系起来,但是如果打印[NSRunLoop currentRunLoop]
结果中会发现和自动释放池相关的回调。
<CFRunLoopObserver 0x6000024246e0 [0x7fff8062ce20]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff48c1235c), context = <CFArray 0x600001b7afd0 [0x7fff8062ce20]>{type = mutable-small, count = 1, values = (0 : <0x7fc18f80e038>)}}
<CFRunLoopObserver 0x600002424640 [0x7fff8062ce20]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff48c1235c), context = <CFArray 0x600001b7afd0 [0x7fff8062ce20]>{type = mutable-small, count = 1, values = (0 : <0x7fc18f80e038>)}}
即App启动后,苹果会给RunLoop
注册很多个observers
,其中有两个是跟自动释放池相关的,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()
。\
- 第一个
observer
监听的是activities=0x1(kCFRunLoopEntry)
,也就是在即将进入loop
时,其回调会调用_objc_autoreleasePoolPush()
创建自动释放池; - 第二个
observer
监听的是activities = 0xa0(kCFRunLoopBeforeWaiting | kCFRunLoopExit)
,
即监听的是准备进入睡眠和即将退出loop
两个事件。在准备进入睡眠之前,因为睡眠可能时间很长,所以为了不占用资源先调用_objc_autoreleasePoolPop()
释放旧的释放池,并调用_objc_autoreleasePoolPush()
创建新建一个新的,用来装载被唤醒后要处理的事件对象;在最后即将退出loop
时则会_objc_autoreleasePoolPop()
释放池子。
5、总结
- 自动释放池是由
AutoreleasePoolPage
以双向链表的方式实现的。 - 当对象调用
autorelease
方法时,会将对象加入AutoreleasePoolPage
的栈中。 - 调用
AutoreleasePoolPage::pop
方法会向栈中的对象发送release
消息。 - 自动释放池和线程是紧密相关的,每一个自动释放池只对应一个线程