浅谈 AutoreleasePool 的实现原理
面试题:
autorelease 对象什么时候释放。
autorelease 的本质就是延迟调用 release 方法
在 MRC 的环境下,可以通过调用 [obj autorelease] 将对象添加到当前的 autoreleasepool 中,来延迟释放内存;
在 ARC的环境下,当我们创建一个对象,可以通过 __autoreleasing 修饰符,会将对象添加到当前的 autoreleasepool 中,当 autoreleasepool 销毁时,会对 autoreleasepool 里面的所有对象做一次 release 操作。
注意:
- 编译器会检查方法名是否以
alloc、new、copy、mutableCopy开始,如果不是则自动将返回值的对象注册到autoreleasepool中; - 以
__weak修饰的对象,会注册到autoreleasepool中。 - 调用
Foundation对象的类方法(比如,[NSMutableDictionary dictionary]、[NSArray array]等)会注册到autoreleasepool中。 id的指针或对象的指针在没有显式地指定修饰符时候,会被默认附加上__autoreleasing修饰符。
在没有手动加入 autoreleasepool 的情况下,autorelease 对象是在当前的 runloop 迭代结束时释放的,而它能够释放的原因是系统在每个 runloop 迭代中都加入了自动释放池 push 和 pop。
当 autoreleasepool 销毁时,在调用堆栈中可以发现,系统调用了 -[NSAutoreleasePool release] 方法,这个方法最终通过调用 AutoreleasePoolPage::pop(void *) 函数来负责对 autoreleasepool 中的 autorelease 对象执行 release 操作。
AutoreleasePool 的实现原理
1 | int main(int argc, char * argv[]) { |
@autoreleasepool
使用 clang -rewrite-objc 命令将下面的 Objective-C 代码重写成 C++ 代码:
1 | clang -rewrite-objc main.m |
声明一个 __AtAutoreleasePool 类型的局部变量 __autoreleasepool 来实现 @autoreleasepool {}。当声明 __autoreleasepool 变量时,构造函数 __AtAutoreleasePool() 被调用,即执行:
1 | atautoreleasepoolobj = objc_autoreleasePoolPush(); |
当出了当前作用域时,析构函数 ~__AtAutoreleasePool() 被调用,即执行:
1 | objc_autoreleasePoolPop(atautoreleasepoolobj); |
也就是说 @autoreleasepool {} 的实现代码可以进一步简化如下:
1 | /* @autoreleasepool */ { |
因此,单个 autoreleasepool 的运行过程可以简单地理解为 objc_autoreleasePoolPush() 、[obj release] 和 objc_autoreleasePoolPop(void *) 三个过程。
AutoreleasePoolPage

从图中可以看出
AutoreleasePoolPage是由双向链表来实现的,parent和child就是用来构造双向链表的指针。magic用来校验AutoreleasePoolPage的结构是否完整;AutoreleasePool是按线程一一对应的,结构中的thread指针指向当前线程。AutoreleasePoolPage会为每个对象会开辟4096字节内存。id *next指向了下一个为空的内存地址(初始化为栈底),如果有添加进来的autorelease对象,移动到下一个为空的内存地址中。
如果 AutoreleasePoolPage 里面的 autorelease 对象满了,也就是 id *next 指针指向了栈顶,会新建一个 AutoreleasePoolPage 对象,连接链表,后来添加的 autorelease 对象在新的 AutoreleasePoolPage 加入,id *next 指针指向新的 AutoreleasePoolPage 为空的内存地址,即栈底。所以,向一个对象发送 release 消息,就是将这个对象加入到当前 AutoreleasePoolPage 的 id *next 指针指向的位置。
POOL_SENTINEL(哨兵对象)

POOL_SENTINEL 只是 nil 的别名。
在每个自动释放池初始化调用 objc_autoreleasePoolPush 的时候,都会把一个 POOL_SENTINEL push 到自动释放池的栈顶,并且返回这个 POOL_SENTINEL 哨兵对象。
而当方法 objc_autoreleasePoolPop 调用时,就会向自动释放池中的对象发送 release 消息,直到第一个 POOL_SENTINEL。
objc_autoreleasePoolPush
objc_autoreleasePoolPush() 函数本质上就是调用的 AutoreleasePoolPage 的 push 函数。
1 | void * objc_autoreleasePoolPush(void) { |
根据源码得出,每次执行 objc_autoreleasePoolPush 其实就是创建了一个新的 autoreleasepool,然后会把一个 POOL_SENTINEL push 到自动释放池的栈顶,并且返回这个 POOL_SENTINEL 哨兵对象。
1 | static inline void *push() { |
push 函数通过调用 autoreleaseFast 函数并传入哨兵对象 POOL_SENTINEL 来执行具体的插入操作。
1 | static inline id *autoreleaseFast(id obj) { |
autoreleaseFast 函数在执行一个具体的插入操作时,分别对三种情况进行了不同的处理:
- 当前
hotPage存在且没有满时,调用page->add(obj)方法将对象添加至AutoreleasePoolPage的栈中。 - 当前
hotPage存在且已满时,调用autoreleaseFullPage初始化一个新的page,调用page->add(obj)方法将对象添加至AutoreleasePoolPage的栈中。 - 当前
hotPage不存在时,调用autoreleaseNoPage创建一个hotPage,调用page->add(obj)方法将对象添加至AutoreleasePoolPage的栈中。
objc_autoreleasePoolPop
objc_autoreleasePoolPop(void *)函数本质上也是调用的AutoreleasePoolPage的pop函数。
1 | void objc_autoreleasePoolPop(void *ctxt) { |
pop 函数的入参就是 push 函数的返回值,也就是POOL_SENTINEL 的内存地址。根据这个内存地址找到所在的 AutoreleasePoolPage 然后使用 objc_release 释放 POOL_SENTINEL 指针之前的对象。
总结:
每调用一次 push 操作就会创建一个新的 autoreleasepool,然后往 AutoreleasePoolPage 中插入一个 POOL_SENTINEL,并且返回插入的 POOL_SENTINEL 的内存地址.
在执行 pop 操作的时候传入 POOL_SENTINEL,根据传入的哨兵对象地址找到哨兵对象所处的 page
在当前AutoreleasePoolPage中,然后使用 objc_release 释放 POOL_SENTINEL 指针之前的对象,并把 id next 指针到正确位置。