autoreleasepool
在iOS中,除了需要手动retain,release(现在已经交给了ARC自动生成)外,我们还可以将对象扔到自动释放池中,由自动释放池来自动管理这些对象。我们可以这样使用autoreleasepool:
int main(int argc, char * argv[]) {
@autoreleasepool {
NSString *a = [NSString stringWithFormat:@"%d", 1];
}
}
用clang -rewrite-objc
重写后,得到:
int main(int argc, char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
NSString *a = ((NSString * _Nonnull (*)(id, SEL, NSString * _Nonnull, ...))(void *)objc_msgSend)((id)objc_getClass("NSString"), sel_registerName("stringWithFormat:"), (NSString *)&__NSConstantStringImpl__var_folders_8k_3pbszhls2czcmz0w335cvc0w0000gn_T_main_1a8fc0_mi_1, 1);
}
}
这时会发现, @autoreleasepool 被改写为了 __AtAutoreleasePool __autoreleasepool
这样一个对象。__AtAutoreleasePool
的定义为:
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
于是,关于@autoreleasepool的代码可以被改写为:
objc_autoreleasePoolPush();
// Do your code
objc_autoreleasePoolPop(atautoreleasepoolobj);
置于@autoreleasepool的{}
中的代码实际上是被一个push和pop操作所包裹。当push时,会压栈一个autoreleasepage,在{}
中的所有的autorelease对象都会放到这个page中。当pop时,会出栈一个autoreleasepage,同时,所有存储于这个page的对象都会做release操作。这就是autoreleasepool的实现原理。
objc_autoreleasePoolPush()
和objc_autoreleasePoolPop(atautoreleasepoolobj)
的实现如下:
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
它们都分别调用了AutoreleasePoolPage
类的静态方法push和pop。AutoreleasePoolPage
是runtime中autoreleasepool的核心实现,下面,我们就来了解一下它。
AutoreleasePoolPage
AutoreleasePoolPage
在runtime中的定义如下:
class AutoreleasePoolPage
{
magic_t const magic; // 魔数,用于自身的完整性校验 16字节
id *next; // 指向autorelePool page中的下一个可用位置 8字节
pthread_t const thread; // 和autorelePool page中相关的线程 8字节
AutoreleasePoolPage * const parent; // autoreleasPool page双向链表的前向指针 8字节
AutoreleasePoolPage *child; // autoreleasPool page双向链表的后向指针 8字节
uint32_t const depth; // 当前autoreleasPool page在双向链表中的位置(深度) 4字节
uint32_t hiwat; // high water mark. 最高水位,可用近似理解为autoreleasPool page双向链表中的元素个数 4字节
// SIZE-sizeof(*this) bytes of contents follow
}
每个AutoreleasePoolPage
的大小为一个SIZE
,即内存管理中一个页的大小。这在Mac中是4KB
,而在iOS中,这里没有相关代码,估计差不多。
对象指针栈
由源码可用看出,在AutoreleasePoolPage
类中共有7个成员属性,大小为56Bytes,按照一个Page是4KB计算,显然还有4040Bytes没有用。而这4040Bytes空间,就用来存储AutoreleasePoolPage
所管理的对象指针。因此,一个AutoreleasePoolPage
的内存布局如下图(摘自Draveness的博客):
在autoreleasepool中的对象指针是按照栈的形式存储的,栈低是一个POOL_BOUNDARY
哨兵,之后对象指针依次入栈存储。
POOL_BOUNDARY
在图中可用看到,除了AutoreleasePoolPage
类中的7个成员之外,还有一个叫POOL_BOUNDARY
, 其实这是一个nil指针,AutoreleasePoolPage
中的next
指针用来指向栈中下一个入栈位置。
# define POOL_BOUNDARY nil
它作为一个哨兵,当需要将AutoreleasePoolPage
中存储的对象指针依次出栈时,会执行到POOL_BOUNDARY
为止。
双向链表
在图中也可以看出,单个的AutoreleasePoolPage
是以栈的形式存储的。
当加入到autoreleasepool中的元素太多时,一个AutoreleasePoolPage
就不够用的了。这时候我们需要新创建一个AutoreleasePoolPage
,多个AutoreleasePoolPage
之间通过双向链表的形式串起来。
成员parent
和child
就是用来构造双向链表的。
下面我们就结合具体的代码,来看一下AutoreleasePoolPage
是如何在系统中发挥作用的。
Push
当用户调用@autoreleasepool{}的时候,系统首先会调用AutoreleasePoolPage::push()
方法,来创建或获取当前的hotPage,并向对象栈中插入一个POOL_BOUNDARY
。
static inline void *push()
{
id *dest;
dest = autoreleaseFast(POOL_BOUNDARY);
assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
我们也可以调用autorelease(id obj)方法将某个特定的对象指针插入到AutoreleasePoolPage
中:
static inline id autorelease(id obj)
{
assert(obj);
assert(!obj->isTaggedPointer()); // 注意这个assert,由于tagged pointer不遵循引用计数规则,所以也不会有autorelease操作。
id *dest __unused = autoreleaseFast(obj);
assert(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj);
return obj;
}
可以看到,无论是push
还是autorelease
方法,最后都是调用了autoreleaseFast(obj)
,该方法会将一个id放入到autoreleasePage
中。:
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
return page->add(obj);
} else if (page) {
return autoreleaseFullPage(obj, page);
} else {
return autoreleaseNoPage(obj);
}
}
可以看到方法实现逻辑也很简单:
- 首先取出当前的
hotPage
,所谓hotPage
,就是在autoreleasePage
链表中正在使用的autoreleasePage
节点。 - 如果有hotPage,且hotPage还没满,这将obj加入到page中。
- 如果有hotPage,但是已经满了,则进入page full逻辑(
autoreleaseFullPage
)。 - 如果没有hotPage,进入no page逻辑
autoreleaseNoPage
。
hotPage
hotPage是autoreleasePage
链表中正在使用的autoreleasePage
节点。实质上是指向autoreleasepage的指针,并存储于线程的TSD
(线程私有数据:Thread-specific Data)中:
static inline AutoreleasePoolPage *hotPage()
{
AutoreleasePoolPage *result = (AutoreleasePoolPage *)
tls_get_direct(key);
if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
if (result) result->fastcheck();
return result;
}
从这段代码可以看出,
autoreleasepool是和线程绑定的,一个线程对应一个autoreleasepool。而autoreleasepool虽然叫做自动释放池,其实质上是一个双向链表。
在介绍runloop的时候,我们也曾提到过,runloop和线程也是一一对应的,并且在当前线程的runloop指针,也会存储到线程的TSD
中。这是runtime对于TSD的一个应用。
add object
如果有hot page,先判断page 是否已经full了,判断逻辑是next*
是否等于end()
:
bool full() {
return next == end();
}
关于begin()
和end()
,定义如下,结合page的图示,应该比较容易理解:
id * begin() {
return (id *) ((uint8_t *)this+sizeof(*this));
}
id * end() {
return (id *) ((uint8_t *)this+SIZE);
}
如果page没有满,这调用page的add方法:
id *add(id obj)
{
assert(!full());
id *ret = next; // faster than `return next-1` because of aliasing
*next = obj;
next++;
return ret;
}
逻辑比较简单,就是将obj置于next的位置,next++,然后返回obj的位置。
autoreleaseFullPage
如果hot page满了,就需要在链表中‘加页’,同时将新页置为hot page:
static __attribute__((noinline))
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 {
if (page->child) page = page->child;
else page = new AutoreleasePoolPage(page);
} while (page->full());
setHotPage(page);
return page->add(obj);
}
这一段代码重点需要关注的是寻找可用page
的do while
逻辑。
其实注释中已经写得很清楚,系统会首先尝试在hot page
的child pages
中挑出第一个没有满的page,如果没有符合要求的child page
,则只能创建一个新的new AutoreleasePoolPage(page)
。
最后,将挑选出的page作为当前线程的hot page
(实际上存储到了TSD中),并将obj存到新的hot page
中。
autoreleaseNoPage
若当前线程没有hot Page
,则说明当前的线程还未建立起autorelease pool 。这时,就会调用autoreleaseNoPage
:
static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
// "No page" could mean no pool has been pushed
// or an empty placeholder pool has been pushed and has no contents yet
assert(!hotPage());
bool pushExtraBoundary = false;
if (haveEmptyPoolPlaceholder()) { // 如果当前线程只有一个虚拟的空池,则这次需要真正创建一个page
// We are pushing a second pool over the empty placeholder pool
// or pushing the first object into the empty placeholder pool.
// Before doing that, push a pool boundary on behalf of the pool
// that is currently represented by the empty placeholder.
pushExtraBoundary = true;
}else if (obj == POOL_BOUNDARY && !DebugPoolAllocation) { // 如果obj == POOL_BOUNDARY,这里苹果有个小心机,它不会真正创建page,而是在线程的TSD中做了一个空池的标志
// We are pushing a pool with no pool in place,
// and alloc-per-pool debugging was not requested.
// Install and return the empty pool placeholder.
return setEmptyPoolPlaceholder();
}
// We are pushing an object or a non-placeholder'd pool.
// 创建线程的第一个page,并置为hot page。
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);
// 如果之前只是做了空池标记,这里还需要在栈中补上POOL_BOUNDARY,作为栈底哨兵
if (pushExtraBoundary) {
page->add(POOL_BOUNDARY);
}
// Push the requested object or pool. 注意,这里的注释,进入page的不光可以有object,还可以是pool。
return page->add(obj);
}
当系统发现当前线程没有对应的autoreleasepool时,我们自然的想到需要为线程创建一个page。但是苹果其实在这里是耍了一个小心机的
,当在创建第一个page时,苹果并不会真正创建一个page,因为它害怕创建了page后,并没有真正的object需要插入page,这样就造成了无谓的内存浪费。
在没有第一个真正的object入栈之前,苹果是这样做的:仅仅在线程的TSD中做了一个EMPTY_POOL_PLACEHOLDER
标记,并返回它。这里没有真正的new
一个AutoreleasePoolPage
。
Pop
当autoreleasepool需要被释放时,会调用Pop方法。而Pop方法需要接受一个void *token参数,来告诉池子,需要一直释放到token对应的那个page:
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 {
// 这是为了兼容旧的SDK,看来在新的SDK里面,token 可能的取值只有两个:POOL_BOUNDARY, page->begin() && !page->parent
// Error. For bincompat purposes this is not
// fatal in executables built with old SDKs.
return badPop(token);
}
}
// 对page中的object做objc_release操作,一直到stop
page->releaseUntil(stop);
// memory: delete empty children 删除多余的child,节约内存
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();
}
}
}
何时需要autoreleasePool
OK,以上就是autoreleasepool的内容。那么在ARC的环境下,我们何时需要用@autoreleasepool呢?
一般的,有下面两种情况:
- 创建子线程。当我们创建子线程的时候,需要将子线程的runloop用@autoreleasepool包裹起来,进而达到自动释放内存的效果。因为系统并不会为子线程自动包裹一个@autoreleasepool,这样加入到autoreleasepage中的元素就得不到释放。
- 在大循环中创建autorelease对象。当我们在一个循环中创建autorelease对象(不是用alloc创建的对象),该对象会加入到autoreleasepage中,而这个page中的对象,会等到外部池子结束才会释放。在主线程的runloop中,会将所有的对象的释放权都交给了RunLoop 的释放池,而RunLoop的释放池会等待这个事件处理之后才会释放,因此就会使对象无法及时释放,堆积在内存造成内存泄露。关于这一点,可以参考博客RunLoop和autorelease的一道面试题