Objective-C Autorelease Pool 的实现原理
自动释放池的前世今生 ---- 深入解析 Autoreleasepool
AutoreleasePool是通过一个以AutoreleasePoolPage为结点的双向链表来实现的。AutoreleasePoolPage满后会自动创建下一个AutoreleasePoolPage。
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{}的实现真的是非常巧妙,真正可以称得上艺术的代码。苹果通过一个__AtAutoreleasePool类型的局部变量 __autoreleasepool来实现@autoreleasepool{}。当声明__autoreleasepool时,构造函数被执行,即执行atautoreleasepoolobj = objc_autoreleasePoolPush();。当出了当前作用域时(即大括号),析构函数~__AtAutoreleasePool()被执行,即调用用objc_autoreleasePoolPop(atautoreleasepoolobj);。(微信团队对特殊字符的处理也用到了相同的技巧)。也就是说@autoreleasepool{}的代码实现可以简化为如下:
/* @autoreleasepool */ {
void *atautoreleasepoolobj = objc_autoreleasePoolPush();
// 用户代码,所有接收到 autorelease 消息的对象会被添加到这个 autoreleasepool 中
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
objc_autoreleasePoolPush和objc_autoreleasePoolPop的实现:
void *objc_autoreleasePoolPush(void) {
return AutoreleasePoolPage::push();
}
void objc_autoreleasePoolPop(void*ctxt) {
AutoreleasePoolPage::pop(ctxt);
}
由此可见内部其实调用的是AutoreleasePoolPage中相关的方法
那这里的 AutoreleasePoolPage 是什么东西呢?
其实,autoreleasepool 是没有单独的内存结构的,它是通过以 AutoreleasePoolPage 为结点的双向链表来实现的。
一个空的AutoreleasePool的内存结构如下:
1. magic 用来校验 AutoreleasePoolPage 的结构是否完整;
2. next 指向最新添加的 autoreleased 对象的下一个位置,初始化时指向 begin() ;
3. thread 指向当前线程;
4. parent 指向父结点,第一个结点的 parent 值为 nil ;
5. child 指向子结点,最后一个结点的 child 值为 nil ;
6. depth 代表深度,从 0 开始,往后递增 1;
7. hiwat 代表 high water mark 。
当next==begin()时,表示AutoreleasePage为空,当next==end()时表示AutoreleasePage已满。每一个AutoreleasePage的大小都是4096字节
。我们打开 runtime 的源码工程,在 NSObject.mm 文件的第 438-932 行可以找到 autoreleasepool 的实现源码。
通过阅读源码,我们可以知道:
每一个线程的 autoreleasepool 其实就是一个指针的堆栈;
每一个指针代表一个需要 release 的对象或者 POOL_SENTINEL(哨兵对象,代表一个 autoreleasepool 的边界);
一个 pool token 就是这个 pool 所对应的 POOL_SENTINEL 的内存地址。当这个 pool 被 pop 的时候,所有内存地址在 pool token 之后的对象都会被 release ;
这个堆栈被划分成了一个以 page 为结点的双向链表。pages 会在必要的时候动态地增加或删除;
Thread-local storage(线程局部存储)指向 hot page ,即最新添加的 autoreleased 对象所在的那个 page 。
双向链表:
push操作
其实是创建一个新的AutoreleasePool,对应AutoreleasePoolPage就是在AutoreleasePoolPage的next位置插入一个POOL_SENTINEL指针(哨兵指针)。next移动至下一个地址。POOL_SENTINEL作为push函数的返回值。
第一次调用push操作就会创建一个新的autorelease pool。即往AutoreleasePoolPage中插入一个POOL_SENTINEL,并且返回插入的POOL_SENTINEL的内存地址。
autorelease操作
其实就是将自己的地址插入AutoreleasePoolPage。
[obj autorelease],NSObject中-autorelease方法的实现:
- (id)autorelease {
return ((id)self)->rootAutorelease();
}
通过查看((id)self)->rootAutorelease()的方法调用,发现调用的其实是AutoreleasePoolPage的autorelease函数
__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 == obj);
return obj;
}
Pop操作
同理,前面提到的objc_autoreleasePoolPop(void *) 函数本质上也是调用的AutoreleasePoolPage的pop函数。
void objc_autoreleasePoolPop(void *ctxt)
{
if (UseGC) return;
// fixme rdar://9167170
if (!ctxt) return;
AutoreleasePoolPage::pop(ctxt);
}
pop函数的参数就是push的返回值,也就是POOL_SENTINEL。当执行pop操作时,内存地址POOL_SENTINEL之后的对象都会被release。直到POOL_SENTINEL所在page的next指向POOL_SENTINEL为止。
下图是某个线程的AutoreleasePool堆栈的内存结构图,在这个堆栈中有两个POOL_SENTINEL,也就是表示有两个autoreleasepool。该堆栈由三个AutoreleasePoolPage节点组成,第一个AutoreleasePoolPage为ColdPage(),最后一个为HotPage()。其中前两个AutoreleasePoolPage已经满了,最后一个结点保存了最新添加的autoreleased对象objr3的内存地址。
如果执行pop(token1)操作,那么该堆栈的内存结构将会变成如下图所示:
第二个POOL_SENTINEL指针之后的对象调用release方法,内存得到释放。
什么情况下需要手动添加AutoreleasePool:
Each thread in a Cocoa application maintains its own stack of autorelease pool blocks.
If you are writing a Foundation-only program or if you detach a thread, you need to create your own autorelease pool block.
If your application or thread is long-lived and potentially generates a lot of autoreleased objects, you should use autorelease pool blocks (like AppKit and UIKit do on the main thread); otherwise, autoreleased objects accumulate and your memory footprint grows. If your detached thread does not make Cocoa calls, you do not need to use an autorelease pool block.
1.有大量的零时对象,例如编写的循环中创建了大量的零时对象
2.编写的的程序不是基于UI框架的,比如说命令行工具
3.创建一个辅助线程
AutoreleasePool的创建和销毁
即将进入Runloop的时候创建AutoreleasePool。
RunLoop即将进入休眠的时候释放并创建新的Runloop。
即将退出Runloop的时候释放自动释放池。
App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。
第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush()创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠)时调用_objc_autoreleasePoolPop()和 _objc_autoreleasePoolPush()释放旧的池并创建新池;Exit(即将退出Loop)时调用_objc_autoreleasePoolPop()来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。