本文使用的 runtime 版本为 objc4-706。
对于 autorelease
的研究需要先从 @autoreleasepool { ... }
着手。首先对有 @autoreleasepool { ... }
的代码使用 clang -rewrite-objc
进行转换,在转换后的文件中,可以看到 @autoreleasepool { ... }
变成了这样:
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
...
}
当然还可以找到 __AtAutoreleasePool
的定义:
struct __AtAutoreleasePool {
__AtAutoreleasePool() {
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
~__AtAutoreleasePool() {
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};
可以看到,代码利用了变量声明和自动变量在代码块结束后自动销毁的特性,在构造函数和析构函数中调用了 objc_autoreleasePoolPush
和 objc_autoreleasePoolPop
函数。在 NSObject.mm
文件中可以找到这两个函数的实现:
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
可以发现,这两个函数只是对 AutoreleasePoolPage
这个 C++ 类的两个类方法 push
和 pop
的简单封装。
AutoreleasePoolPage
在 NSObject.mm
中可以找到 AutoreleasePoolPage
类的实现,先可以看一下它的成员变量:
class AutoreleasePoolPage
{
magic_t const magic;
id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
}
一个一个过一下这些成员变量:
-
magic
:这个变量的类型是magic_t
,是用来检查AutoreleasePoolPage
的内存没有被修改的,放在第一个也就是这个原因,防止前面地址有内容溢过来。 -
next
:类型是id *
,存放的是下一个被autorelease
的对象指针存放的地址。 -
thread
:对应的线程,这说明了自动释放池是对应线程的。 -
parent
和child
:用来保存前一个AutoreleasePoolPage
和后一个AutoreleasePoolPage
,就是一个双向链表,毕竟一个AutoreleasePoolPage
能存放的对象是有限的。 -
depth
:很明显是这个链表有多深。 -
hiwat
:一个在 DEBUG 时才有用的参数,表示最高有记录过多少对象(hi-water)。
可以注意到,这些成员变量并没有指示出对象记录在哪里,继续在 AutoreleasePoolPage
的实现里看一看,能发现一些有趣的东西:
static size_t const SIZE =
PAGE_MAX_SIZE; // size and alignment, power of 2
static void * operator new(size_t size) {
return malloc_zone_memalign(malloc_default_zone(), SIZE, SIZE);
}
AutoreleasePoolPage
重载了 new
操作符,这样一个新的对象需要 SIZE
这么多的内存空间,SIZE
的值 PAGE_MAX_SIZE
是一个根据机器不同的大小,在写这篇文章的机器上(i386)上是 4096。AutoreleasePoolPage
的成员变量大小加在一起也只有 56 字节,但是 new 它一个居然要 4096 字节,这剩下的 4040 字节肯定就是存放被 autorelease
的对象的地方了。AutoreleasePoolPage
的实现中有这些个函数:
id * begin() {
return (id *) ((uint8_t *)this+sizeof(*this));
}
id * end() {
return (id *) ((uint8_t *)this+SIZE);
}
bool empty() {
return next == begin();
}
bool full() {
return next == end();
}
bool lessThanHalfFull() {
return (next - begin() < (end() - begin()) / 2);
}
可以看到,begin
就是成员变量结束的地址(this+sizeof(*this)
),end
就是整个申请的内存结束的地方了,其余的函数很好看懂。对于成员变量 next
来说,可以看一下构造函数:
AutoreleasePoolPage(AutoreleasePoolPage *newParent)
: magic(), next(begin()), thread(pthread_self()),
parent(newParent), child(nil),
depth(parent ? 1+parent->depth : 0),
hiwat(parent ? parent->hiwat : 0)
{
if (parent) {
parent->check();
assert(!parent->child);
parent->unprotect();
parent->child = this;
parent->protect();
}
protect();
}
可以看到 next(begin())
,next
的初始值就是 begin
。结合上面对 next
的描述,就能理解这个初始值的意义了。
autorelease
现在来研究一下 autorelease
是怎么实现的,autorelease
的入口是 objc_autorelease
函数:
__attribute__((aligned(16)))
id
objc_autorelease(id obj)
{
if (!obj) return obj;
if (obj->isTaggedPointer()) return obj;
return obj->autorelease();
}
就是很简单的进行了判空和判断 tagged pointer 后,就将实现交给了 objc_object
结构体的 autorelease
函数:
// Equivalent to [this autorelease], with shortcuts if there is no override
inline id
objc_object::autorelease()
{
if (isTaggedPointer()) return (id)this;
if (fastpath(!ISA()->hasCustomRR())) return rootAutorelease();
return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_autorelease);
}
走的也是 reatin
和 release
的老套路,如果没有自定义的实现,就走默认实现 rootAutorelease
,否则直接给自定义实现发消息。继续查看默认实现:
// Base autorelease implementation, ignoring overrides.
inline id
objc_object::rootAutorelease()
{
if (isTaggedPointer()) return (id)this;
if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;
return rootAutorelease2();
}
其中 prepareOptimizedReturn
函数是 ARC 对 autorelease
的优化,本篇文章不做研究,继续查看 rootAutorelease2
:
__attribute__((noinline,used))
id
objc_object::rootAutorelease2()
{
assert(!isTaggedPointer());
return AutoreleasePoolPage::autorelease((id)this);
}
果不其然,是调用了 AutoreleasePoolPage
里的实现(这不废话吗前面还讲了那么多关于 AutoreleasePoolPage
手动捂脸)。
继续追查 autorelease
函数:
static inline id autorelease(id obj)
{
assert(obj);
assert(!obj->isTaggedPointer());
id *dest __unused = autoreleaseFast(obj);
assert(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj);
return obj;
}
可以看到,实际工作交给了 autoreleaseFast
函数,文章之后再对这个函数继续分析。
push
结合文章最开始的分析,push
函数就是往 AutoreleasePoolPage
这一整个内存空间里压入一个自动释放池,看一下 push
的实现:
# define POOL_BOUNDARY nil
static inline void *push()
{
id *dest = autoreleaseFast(POOL_BOUNDARY);
assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
push
的实现里很有意思的往 autoreleaseFast
函数里传入了一个叫 POOL_BOUNDARY
(池边界)的东西,可以看到它其实就是 nil
。新建一个自动释放池为什么要和 autorelease
调用一样的函数呢?接下来分析一下 autoreleaseFast
函数。
autoreleaseFast
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);
}
}
在 autoreleaseFast
中,首先需要拿到一个 hot page,这个其实就是所在线程正在使用的 AutoreleasePoolPage
,hotPage
的实现有一点需要注意的地方:
// EMPTY_POOL_PLACEHOLDER is stored in TLS when exactly one pool is
// pushed and it has never contained any objects. This saves memory
// when the top level (i.e. libdispatch) pushes and pops pools but
// never uses them.
# define EMPTY_POOL_PLACEHOLDER ((id*)1)
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;
}
hotPage
的实现很简单,使用 tls_get_direct
获得线程(TLS, Thread-local storage)的 AutoreleasePoolPage
对象,fastcheck
是对 magic
的检查,但是如果发现结果是 EMPTY_POOL_PLACEHOLDER
也就是 1 的话,也返回 nil
。
EMPTY_POOL_PLACEHOLDER
从注释的说明可以知道,是对当只有一个自动释放池创建了(push 了)并且没有任何对象被 autorelease
时的优化。现在只需要知道它的存在就好。
回到 autoreleaseFast
函数,在拿到 page
后,需要对 page
的不同情况进行不同的处理。
先看最简单的情况,也就是有 hot page 并且它没有满的情况,这个时候调用了 page
的 add
方法:
id *add(id obj)
{
assert(!full());
id *ret = next; // faster than `return next-1` because of aliasing
*next++ = obj;
return ret;
}
可以看到,这就是将 obj
存到 next
的位置,并将 next
加 1,典型的入栈操作。如果 obj
是一个对象(autorelease
方法的调用),这就是将对象保存在自动释放池了,如果 obj
是 POOL_BOUNDARY
也就是 nil
(push
方法的调用)则这里便是自动释放池的分界。
继续看 page
为 nil
的情况,也就是对 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()) {
// 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) {
// 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.
// Install the first page.
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);
// Push a boundary on behalf of the previously-placeholder'd pool.
if (pushExtraBoundary) {
page->add(POOL_BOUNDARY);
}
// Push the requested object or pool.
return page->add(obj);
}
进入到这个函数,会有两种情况,注释里也已经说明了,也是刚才 hotPage
函数实现注意的地方。hot page 是 EMPTY_POOL_PLACEHOLDER
也会被当作是 no page,进入这个函数。
我们先假设一种情况,是第一个自动释放池创建时,首先对 haveEmptyPoolPlaceholder
函数的结果进行判断:
static inline bool haveEmptyPoolPlaceholder()
{
id *tls = (id *)tls_get_direct(key);
return (tls == EMPTY_POOL_PLACEHOLDER);
}
这个函数其实就是判断 hot page 是不是 EMPTY_POOL_PLACEHOLDER
,因为我们现在假设为第一次创建自动释放池,所以这个函数的返回值便是 false
,并且 obj
参数的值是 POOL_BOUNDARY
,因此 autoreleaseNoPage
会调用 setEmptyPoolPlaceholder
并返回,而 setEmptyPoolPlaceholder
的实现:
static inline id* setEmptyPoolPlaceholder()
{
assert(tls_get_direct(key) == nil);
tls_set_direct(key, (void *)EMPTY_POOL_PLACEHOLDER);
return EMPTY_POOL_PLACEHOLDER;
}
就是将 hot page 设置为 EMPTY_POOL_PLACEHOLDER
。这样,在第一次创建(push)一个自动释放池时,并没有生成 AutoreleasePoolPage
对象,而是使用了一个占位符。
现在进入第二种情况,在上面的情况发生完之后,有一个对象被 autorelease
了,流程也会进入 autoreleaseNoPage
,但是现在 haveEmptyPoolPlaceholder
返回的是 true
了,将会把 pushExtraBoundary
也设置为 true
。
这样在接下来的代码中,会创建新的对象 page
并将它设置为 hot page,因为发现 pushExtraBoundary
为 ture
,因此还需要 add
一个 POOL_BOUNDARY
。最后再将对象也加入,就完事了。
最后看到 page
满了的情况,也就是对 autoreleaseFullPage
函数的调用:
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
有没有还没满的 child
(顺链表往下查),没有的话就新建一个,再使用 add
函数将 obj
记录。
pop
其实现在大概能感觉到,自动释放池其实就是个用链表实现的一个栈。继续看 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);
}
}
page->releaseUntil(stop);
// memory: delete empty children
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();
}
}
}
参数 token
,传入的是 push
返回值,其实就是 push
函数插入 POOL_BOUNDARY
的地址(指针),在 pop
里表示要一直释放到 token
指向的地址为止。
pop
函数一开始会检查 token
是不是 EMPTY_POOL_PLACEHOLDER
。当 token
是 EMPTY_POOL_PLACEHOLDER
时,会继续检查是否有 hot page(理论上来说不应该会有 hot page,一个疑问),如果没有 hot page,则直接将 hot page 设置为 nil
,如果有 hot page,则重新调用 pop
,传入的 token
为 coldPage()-begin()
,coldPage
的实现如下:
static inline AutoreleasePoolPage *coldPage()
{
AutoreleasePoolPage *result = hotPage();
if (result) {
while (result->parent) {
result = result->parent;
result->fastcheck();
}
}
return result;
}
很明显,所谓的 cold page 就是线程的第一个 AutoreleasePoolPage
。
当 pop
函数的 token
不是 EMPTY_POOL_PLACEHOLDER
时,进入正常的 pop 流程,首先要获取到 token
也就是一个内存地址的所在 page,也就是 pageForPointer
函数的工作:
static AutoreleasePoolPage *pageForPointer(const void *p)
{
return pageForPointer((uintptr_t)p);
}
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;
}
因为 AutoreleasePoolPage
对象是根据 SIZE
的大小来对齐的,所以使用地址 p
的值对 SIZE
取余就能获取到 p
和所在 page 地址的偏移值(offset
),从而得到所在 page,最后会对所在 page 的 magic
进行检查,也就是 fastcheck
所做的工作。
获得了 page 以后,pop
函数还会检查在 token
这个地址存储的内容是否是 POOL_BOUNDRAY
:
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);
}
}
正常来说,在这个地方 token
就应该得是 POOL_BOUNDARY
,因为 push
函数每次都是添加的 POOL_BOUNDARY
。但这个地方进行了判断,其中如果如果 token
就是 page
的 begin
,并且 page
是第一个的话,则认为是正常情况(这其实是没有 push
就直接 autorelease
了)。否则进入 badPop
流程,这个流程会在最新的 SDK (10.12, 10.0, 10.0, 3.0)上会直接产生 fatal。
接下来的正常流程,也就是 token
所指向的地址存储的内容为 POOL_BOUNDARY
时,调用 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
while (this->next != stop) {
// Restart from hotPage() every time, in case -release
// autoreleased more objects
AutoreleasePoolPage *page = hotPage();
// fixme I think this `while` can be `if`, but I can't prove it
while (page->empty()) {
page = page->parent;
setHotPage(page);
}
id obj = *--page->next;
memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
if (obj != POOL_BOUNDARY) {
objc_release(obj);
}
}
setHotPage(this);
}
实现很容易看懂,就是循环直到到 stop
给每个对象调用 objc_release
也就等同于发送 release
消息。其中每次都从 hot page 开始的原因注释里进行了说明,是怕 release
方法里又 autorelease
了对象。
最后,pop
函数还要删除不需要的空的 page:
// memory: delete empty children
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();
}
}
做了点小优化,如果现在这个 page
只剩下不到一半的空间了,则多留一个 child
。kill
的实现如下:
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;
while (page->child) page = page->child;
AutoreleasePoolPage *deathptr;
do {
deathptr = page;
page = page->parent;
if (page) {
page->child = nil;
}
delete deathptr;
} while (deathptr != this);
}
其实就是沿着链表删除。
总结
总的来看,自动释放池的实现思想是很简单的:
- 对每个线程来说,用一个由
AutoreleasePoolPage
的组成的双向链表维护一个栈,被autorelease
的对象记录在这个栈中; - 使用
POOL_BOUNDARY
也就是nil
来对自动释放池进行分隔。
当然,其中实现还是有着各种有趣的细节的。
本文原始地址:Objective-C 小记(8)autorelease