备注:文章是抄袭Objective-C Autorelease Pool 的实现原理,这里记录下阅读这篇文章的收获。
内存管理一直是学习 Objective-C 的重点和难点之一,尽管现在已经是 ARC 时代了,但是了解 Objective-C 的内存管理机制仍然是十分必要的。其中,弄清楚 autorelease 的原理更是重中之重,只有理解了 autorelease 的原理,我们才算是真正了解了 Objective-C 的内存管理机制,autorelease 本质上就是延迟调用 release。
@autoreleasepool
main.m中包含的代码如下所示,使用 clang -rewrite-objc 命令将下面的 Objective-C 代码重写成 C++ 代码:
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"Hello, World!");
}
return 0;
}
将会得到以下输出结果:
extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void);
extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *);
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_x__wh4dgr654mx__qc0b9bqyg5w0000gn_T_main_826771_mi_0);
}
return 0;
}
通过声明一个 __AtAutoreleasePool
类型的局部变量 __autoreleasepool
来实现 @autoreleasepool {}
。当声明 __autoreleasepool
变量时,构造函数 __AtAutoreleasePool()
被调用,即执行 atautoreleasepoolobj = objc_autoreleasePoolPush()
; ;当出了当前作用域时,析构函数 ~__AtAutoreleasePool()
被调用,即执行 objc_autoreleasePoolPop(atautoreleasepoolobj)
; 。也就是说 @autoreleasepool {}
的实现代码可以进一步简化如下:
/* @autoreleasepool */ {
void *atautoreleasepoolobj = objc_autoreleasePoolPush();
// 用户代码,所有接收到 autorelease 消息的对象会被添加到这个 autoreleasepool 中
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
查看runtime的源码,objc_autoreleasePoolPush
和objc_autoreleasePoolPop
方法,实际调用的是AutoreleasePoolPage
的Push
和Pop
方法。
void *objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
void objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
那么autoreleasePoolPage是什么呢?
AutoreleasePoolPage
autoreleasepool没有单独的内存结构,它是通过以AutoreleasePoolPage为节点的双向链表来实现的。
每一个线程的 autoreleasepool 其实就是一个指针的堆栈;
每一个指针代表一个需要 release 的对象或者 POOL_SENTINEL(哨兵对象,代表一个 autoreleasepool 的边界);
一个 pool token 就是这个 pool 所对应的 POOL_BOUNDARY 的内存地址。当这个 pool 被 pop 的时候,所有内存地址在 pool token 之后的对象都会被 release ;
-
这个堆栈被划分成了一个以 page 为结点的双向链表。pages 会在必要的时候动态地增加或删除;
magic
用来校验 AutoreleasePoolPage 的结构是否完整;next
指向最新添加的 autoreleased 对象的下一个位置,初始化时指向 begin() ;thread
指向当前线程;parent
指向父结点,第一个结点的 parent 值为 nil ;child
指向子结点,最后一个结点的 child 值为 nil ;depth
代表深度,从 0 开始,往后递增 1;hiwat
代表 high water mark 。
push
我们在调用objc_autoreleasePoolPush()
方法,实际调用的是AutoreleasePoolPage的push方法,具体实现就是往 AutoreleasePoolPage 中的 next 位置插入一个 POOL_BOUNDARY ,并且返回插入的 POOL_BOUNDARY 的内存地址。这个地址也就是我们前面提到的 pool token ,在执行 pop 操作的时候作为函数的入参。
static inline void *push()
{
id *dest;
if (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 函数来执行具体的插入操作。
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);
}
}
- 当前 page 存在且没有满时,直接将对象添加到当前 page 中,即 next 指向的位置;
- 当前 page 存在且已满时,创建一个新的 page ,并将对象添加到新创建的 page 中;
- 当前 page 不存在时,即还没有 page 时,创建第一个 page ,并将对象添加到新创建的 page 中。
每调用一次 push 操作就会创建一个新的 autoreleasepool ,即往 AutoreleasePoolPage 中插入一个 POOL_BOUNDARY ,并且返回插入的 POOL_BOUNDARY 的内存地址。
autorelease 操作
通过NSObject.mm
源文件,我们可以找到-autorelease
方法的实现:
- (id)autorelease {
return ((id)self)->rootAutorelease();
}
通过查看 ((id)self)->rootAutorelease() 的方法调用,我们发现最终调用的就是 AutoreleasePoolPage 的 autorelease 函数。
id objc_object::rootAutorelease2()
{
assert(!isTaggedPointer());
return AutoreleasePoolPage::autorelease((id)this);
}
AutoreleasePoolPage 的 autorelease 函数的实现对我们来说就比较容量理解了,它跟 push 操作的实现非常相似。只不过 push 操作插入的是一个 POOL_BOUNDARY ,而 autorelease 操作插入的是一个具体的 autoreleased 对象。
static inline id autorelease(id obj)
{
assert(obj);
assert(!obj->isTaggedPointer());
id *dest __unused = autoreleaseFast(obj);
assert(!dest || *dest == obj);
return obj;
}
pop 操作
同理,前面提到的 objc_autoreleasePoolPop(void *) 函数本质上也是调用的 AutoreleasePoolPage 的 pop 函数。
void objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
pop 函数的入参就是 push 函数的返回值,也就是 POOL_BOUNDARY 的内存地址,即 pool token 。当执行 pop 操作时,内存地址在 pool token 之后的所有 autoreleased 对象都会被 release 。直到 pool token 所在 page 的 next 指向 pool token 为止。
下面是某个线程的 autoreleasepool 堆栈的内存结构图,在这个 autoreleasepool 堆栈中总共有两个 POOL_SENTINEL ,即有两个 autoreleasepool 。该堆栈由三个 AutoreleasePoolPage 结点组成,第一个 AutoreleasePoolPage 结点为 coldPage() ,最后一个 AutoreleasePoolPage 结点为 hotPage() 。其中,前两个结点已经满了,最后一个结点中保存了最新添加的 autoreleased 对象 objr3 的内存地址。
此时,如果执行 pop(token1) 操作,那么该 autoreleasepool 堆栈的内存结构将会变成如下图所示:
NSThread、NSRunLoop 和 NSAutoreleasePool
根据苹果官方文档中对 NSRunLoop 的描述,我们可以知道每一个线程,包括主线程,都会拥有一个专属的 NSRunLoop 对象,并且会在有需要的时候自动创建。
同样的,根据苹果官方文档中对 NSAutoreleasePool 的描述,我们可知,在主线程的 NSRunLoop 对象(在系统级别的其他线程中应该也是如此,比如通过 dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) 获取到的线程)的每个 event loop 开始前,系统会自动创建一个 autoreleasepool ,并在 event loop 结束时 drain 。我们上面提到的场景 1 中创建的 autoreleased 对象就是被系统添加到了这个自动创建的 autoreleasepool 中,并在这个 autoreleasepool 被 drain 时得到释放。
另外,NSAutoreleasePool 中还提到,每一个线程都会维护自己的 autoreleasepool 堆栈。换句话说 autoreleasepool 是与线程紧密相关的,每一个 autoreleasepool 只对应一个线程。
总而言之:每一个 autoreleasepool 只对应一个线程。每一个线程拥有一个专属的 NSRunLoop 对象。