代码
在MRC环境下
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[pool release];
在ARC or MRC 环境下均可使用
@autoreleasepool {
}
内存管理
Cocoa
的内存管理主要依赖于Reference Counting
, 而NSAutoReleasePool
就是用来支持它的。
Autorelease pool
中存放的对象会在其自身干枯(drain)时被release
。
对象的销毁
我们都知道当一个object
的release
方法被触发时, 这个对象就被销毁了, 再也不能对它有任何引用, 否则就会出现异常。
但如果在销毁它时触发的是autorelease
方法, 那这个object
就进入了对应的autorelease pool
, 它的生命就被延长了(当pool drain
时才真正被销毁).
在引用计数的环境中,Cocoa期望有一个可以随时可用的自动释放池。如果一个池不可用,则自动释放的对象不会被释放,您会泄漏内存。在这种情况下,您的程序通常会记录适当的警告消息。
应用程序包在事件循环的每一个循环开始时在主线程上创建一个自动释放池,并在结束时将其耗尽,从而释放在处理事件时生成的任何自动释放的对象。如果您使用的是应用程序包,那么您通常不需要创建自己的池。如果您的应用程序在事件循环中创建了许多临时的自动回调对象,那么创建“本地”自动回调池将有助于最小化峰值内存占用。
创建autorelease pool
NSAutoReleasePool
的初始化与普通的NSObject
一样, 都是alloc+init
, 不过pool
不能被retain
, 因为在drain
的时候默认就销毁它自身了.
还有一点需要注意的是, 通常在销毁pool
的时候用的不是它的release
方法, 而是drain
!
原因是为了让程序同时兼容Reference Counting
内存管理环境 与 Garbge Collection
环境, 因为在Garbage Colloection
环境中drain
的作用是触发collect garbage
动作.
一般来说在应用的main thread
中, 已经存在了一个autorelease pool
. 有两种情况需要开发者自己新建autorelease pool
:
1、在main thread
中, 在某个方法中出现大量的autoreleased objects
, 为了避免memory footprint
的增大, 可以手动创建一些autorelease pool
用来drain objects
.
2、创建新的thread
, 并在其中访问了Cocoa
, 需要在访问的前创建autorelease pool
, 访问结束后drain
.
最后一点, 在每个thread
中都会维持一个stack
(栈堆), 其中放置着所有在这个thread
中创建但未销毁的pool
, 每当一个新的pool
创建后, 它就位于stack
的最顶端, 相应autoreleased object
就会放入其中. 当pool drain
的时候, 它就会从stack
的顶端移除, 并且release掉其包含的
objects`.
AutoreleasePoolPage
在MRC环境下,我们可以看到-[NSAutoreleasePool release]
方法最终是通过调用 AutoreleasePoolPage::pop(void *)
函数来负责对autoreleasepool
中的autoreleased
对象执行release
操作的。
其实,autoreleasepool
是没有单独的内存结构的,它是通过以 AutoreleasePoolPage
为结点的双向链表来实现的。
我们打开 runtime
的源码工程,在 NSObject.mm
文件的第 671-1228 行可以找到 autoreleasepool
的实现源码。
通过阅读源码,我们可以知道:
1、每一个线程的 autoreleasepool
其实就是一个指针的堆栈;
2、每一个指针代表一个需要release
的对象或者 POOL_BOUNDARY
(哨兵对象,代表一个autoreleasepool
的边界);
3、一个pool token
就是这个pool
所对应的POOL_BOUNDARY
的内存地址。当这个pool
被pop
的时候,所有内存地址在 pool token
之后的对象都会被release
;
4、这个堆栈被划分成了一个以 pool page
为结点的双向链表。pool pages
会在必要的时候动态地增加或删除;
5、Thread-local storage(
线程局部存储)指向hot page
,即最新添加的 autoreleased
对象所在的那个page
。
一个空的AutoreleasePoolPage
的内存结构如下所示:
magic_t const magic;
id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
magic 用来校验 AutoreleasePoolPage 的结构是否完整; next 指向最新添加的 autoreleased 对象的下一个位置,初始化时指向 begin() ; thread 指向当前线程; parent 指向父结点,第一个结点的 parent 值为 nil ; child 指向子结点,最后一个结点的 child 值为 nil ; depth 代表深度,从 0 开始,往后递增 1; hiwat 代表 high water mark 。
另外,当 next == begin()
时,表示 AutoreleasePoolPage
为空;当 next == end()
时,表示 AutoreleasePoolPage
已满。
bool empty() {
return next == begin();
}
bool full() {
return next == end();
}
Autorelease Pool Blocks
我们使用 clang -rewrite-objc
命令将下面的Objective-C
代码重写成C++
代码:
@autoreleasepool {
}
将会得到以下输出结果(只保留了相关代码):
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;
};
不得不说,苹果对@autoreleasepool {}
的实现真的是非常巧妙,真正可以称得上是代码的艺术。
苹果通过声明一个 __AtAutoreleasePool
类型的局部变量 __autoreleasepool
来实现@autoreleasepool {}
。
当声明 __autoreleasepool
变量时,构造函数__AtAutoreleasePool()
被调用,即执行atautoreleasepoolobj = objc_autoreleasePoolPush();
;
当出了当前作用域时,析构函数~__AtAutoreleasePool()
被调用,即执行 objc_autoreleasePoolPop(atautoreleasepoolobj);
。
也就是说@autoreleasepool {}
的实现代码可以进一步简化如下
/* @autoreleasepool */
{
void *atautoreleasepoolobj = objc_autoreleasePoolPush();
// 用户代码,所有接收到 autorelease 消息的对象会被添加到这个 autoreleasepool 中
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
因此,单个autoreleasepool
的运行过程可以简单地理解为 objc_autoreleasePoolPush()
、[对象 autorelease]
和 objc_autoreleasePoolPop(void *)
三个过程。
objc_autoreleasePoolPush()操作
上面提到的objc_autoreleasePoolPush()
函数本质上就是调用的 AutoreleasePoolPage
的push
函数。
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
因此,我们接下来看看 AutoreleasePoolPage
的push
函数的作用和执行过程。
一个 push
操作其实就是创建一个新的autoreleasepool
,对应 AutoreleasePoolPage
的具体实现就是往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);
}
}
autoreleaseFast
函数在执行一个具体的插入操作时,分别对三种情况进行了不同的处理:
【1】当前 page
存在且没有满时,直接将对象添加到当前 page
中,即 next
指向的位置;
【2】当前page
存在且已满时,创建一个新的page
,并将对象添加到新创建的page
中;
【3】当前 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
函数。
objc_object::rootAutorelease()
{
if (isTaggedPointer()) return (id)this;
if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;
return rootAutorelease2();
}
__attribute__((noinline,used))
id
objc_object::rootAutorelease2()
{
assert(!isTaggedPointer());
return AutoreleasePoolPage::autorelease((id)this);
}
AutoreleasePoolPage
的autorelease
函数的实现对我们来说就比较容量理解了,它跟 push
操作的实现非常相似。
只不过push
操作插入的是一个POOL_SENTINEL
,而 autorelease
操作插入的是一个具体的autoreleased
对象。
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;
}
objc_autoreleasePoolPop(void *)操作
同理,前面提到的 objc_autoreleasePoolPop(void *)
函数本质上也是调用的AutoreleasePoolPage
的 pop
函数。
void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
pop
函数的入参就是 push
函数的返回值,也就是POOL_SENTINEL
的内存地址,即pool token
。
当执行pop
操作时,内存地址在 pool token
之后的所有 autoreleased
对象都会被release
。
直到pool token
所在 page
的 next
指向pool token
为止。
参考链接
NSAutoreleasePool
https://developer.apple.com/documentation/foundation/nsautoreleasepool
对官方文档中NSAutoreleasePool的个人翻译
http://blog.youkuaiyun.com/wiki_su/article/details/77198341
关于NSAutoReleasePool的理解
http://eleda.iteye.com/blog/1108700
Objective-C Autorelease Pool 的实现原理
http://www.cocoachina.com/ios/20150610/12093.html
(这篇文章的runtime源代码是老版本的了,本文的代码是新版的runtime源码。)