上一篇依靠 objc-runtime 的源码学习了引用计数的原理和具体实现,但并没有解释内存管理法则第二条中的“非自己生成的对象”是如何被释放的。要想回答这个问题,必须了解 AutoreleasePool 这个概念(讨论的环境还是 MRR 而非 ARC)。
Autorelease 概览
谈到内存管理的第二条法则时,出现了使用非 allow/new/copy/mutableCopy 开头的方法生成的对象,比如:
NSMutableArray *array = [NSMutableArray array];
我们并没有持有这个 array 对象,那我们也就没有权利释放它(当然你也可以释放它,只是会导致程序崩溃而已)。既然我们不能去释放它,那么我们就需要一套机制去做这个事情 —— Autorelease 就这种用于延迟释放对象的一种机制。简要地说,就是向对象发送 -autorelease
消息,将对象放到 AutoreleasePool 中,在某个时刻,向这个 Pool 中的所有对象发送 -release
消息。所以上面的 +array
方法的实现可能是这样的:
+(instancetype)array {
return [[NSMutableArray new] autorelease];
}
AutoreleasePoolPage 的结构
在谈到 AutoreleasePool 时,我们会想象它是一个类似 Array 或者 Set 这样的容器对象,其实不然。AutoreleasePool 的实现并不是建立在一个容器上的,而是依赖于由一个或多个的 AutoreleasePoolPage
对象作为节点,构成的双向链表这样的数据结构。用一张图来快速过一下它吧:
图中有两个 AutoreleasePoolPage
对象(以下简称 page 对象),每一个都是虚拟内存页面的大小,除去底部(低地址)为 page 对象的成员变量所占的空间之外,剩余的内存空间看作一个栈,每个帧用来存储将要被释放的对象或者哨兵对象(用于区分 Pool 的边界)。next
其实也是 page 对象的成员变量,单独画出来是为了描述它作用:next
总是指向下一个可放入 id 对象的地址,直到栈被堆满后指向栈顶。而 hotPage
不是成员变量,它是通过 TLS (Thread Local Storage)与线程绑定的处于活跃状态的 page 对象,这个说明了两点:
- 多个线程直接不共享 page 对象,在多线程中使用 MRR 时要注意这个问题,免得对象多次被释放或未能完成释放;
- 凡是增加或删除对象都从这个活跃的 page 对象开始操作。
AutoreleasePoolPage
这个类的完整定义也在 NSObject.mm
这个文件里面,下面列举主要的一些(静态)成员变量,有好些我还不知道其作用,望指教:
#define POOL_SENTINEL nil // 哨兵对象
static pthread_key_t const key = AUTORELEASE_POOL_KEY; // 用于 TLS 获得 hotPage 的 key
static uint8_t const SCRIBBLE = 0xA3; // 乱写的数据,用于填充被释放对象所占据的“帧”
static size_t const SIZE = PAGE_MAX_SIZE; // 该类重载了 new 操作符,为 page 对象分配 SIZE 这么大的内存空间
static size_t const COUNT = SIZE / sizeof(id);
magic_t const magic; // 应该是类似于魔数之类的东西,用于标记和判断什么?
id *next; // 能放置对象的下一个地址或栈顶
pthread_t const thread; // 与该 page 对象绑定的线程
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth; // page 的深度,或者说是这个里链表头部的距离,第一个结点为 0,第二个为 1,以此类推
uint32_t hiwat; // high water 高水位?不清楚其作用
首先是 POOL_SENTINEL
,也就是刚刚提到哨兵对象,实际只是 nil
的别名而已。使用过 NSAutoreleasePool
的人都知道,Pool 是可以嵌套使用的,而在实现上,由于每个 Pool 不是独立的结构,就要依靠这个哨兵来区分各个 Pool 块:
接着是一个 pthread_key_t const key
,这是用来获得与线程绑定的数据的键,结合 pthread_setspecific()
和 pthread_getspecific()
等函数,,让每个线程都能拥有属于自己的那一份看起来是全局变量的数据(比较典型的例子是 errno
,在某个线程出现的错误不会覆盖另一个线程的错误码)。不熟悉的话,换做 Objective-C 的实现可能会好理解一点:
NSString *key = @"Error_Key";
NSMutableDictionary * dic = [[NSThread currentThread] threadDictionary];
[dic setObject:@"error in main thread" forKey:key];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSMutableDictionary * bgDic = [[NSThread currentThread] threadDictionary];
[bgDic setObject:@"error in child thread" forKey:key];
NSLog(@"error: %@", [bgDic objectForKey:key]);
});
sleep(1);
NSLog(@"error: %@", [dic objectForKey:key]);
显然两个线程的 threadDictionary
是独立的。
AutoreleasePool 的工作流程
_objc_autoreleasePoolPush()
和 _objc_autoreleasePoolPop()
这两个函数,分别在 NSAutoreleasePool
对象实例化以及发送 -drain
消息时调用。前者的调用最终落实到 AutoreleasePool
的静态方法中:
static inline void *push()
{
id *dest;
if (DebugPoolAllocation) {
// Each autorelease pool starts on a new pool page.
dest = autoreleaseNewPage(POOL_SENTINEL);
} else {
dest = autoreleaseFast(POOL_SENTINEL);
}
assert(*dest == POOL_SENTINEL);
return dest;
}
通常会跳进 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()
的逻辑非常简单,没有 hotPage 就新建一个,hotPage 没满就直接 add()
进去,满了就续一个新 page 对象,没什么好说的。感兴趣的话可以去读一下源码。
前面说到插入哨兵之后会返回一个帧的地址,这个地址作为参数传递给 _objc_autoreleasePoolPop()
,表示释放的终点。但是光知道终点是不够的,你还得知道终点在哪个 page 对象上,才能让 page 对象调用成员函数 releaseUntil()
。所以就有了下面这个函数:
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;
}
pageForPointer()
通过哨兵的地址 p
对页面大小取余获得偏移量,再用 p
减去偏移量,就是哨兵所在 page 对象的地址了。pop()
函数完成释放工作(再啰嗦一下这个 token
是前面返回的哨兵所在的帧地址),当 page 对象调用 releaseUntil()
时,从 next
指针开始,往回释放每个对象,直到 stop
这个地址。
static inline void pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
page = pageForPointer(token);
stop = (id *)token;
// 这里省略了提前释放导致错误的代码
if (PrintPoolHiwat) printHiwat();
page->releaseUntil(stop);
// memory: delete empty children
// 这里省略了删除空子节点的代码
}
上面描述了 NSAutoreleasePool
在创建和倾倒时的具体工作过程,那么在给一个 Objective-C 对象发送 -autorelease
消息会是怎么样的呢?下面是其实现:
inline id
objc_object::rootAutorelease()
{
assert(!UseGC);
if (isTaggedPointer())
return (id)this;
if (prepareOptimizedReturn(ReturnAtPlus1))
return (id)this;
return rootAutorelease2();
}
上一篇讲过 tagged pointer object 了,这里不再赘叙。prepareOptimizedReturn()
是在 ARC 下有效的、用于在发送 -autorelease
消息快速返回的机制,编译器根据相关的信息,决定是否要把一个对象放到 pool 中,我应该会在探究 ARC 实现的时候写这个东西,现在感兴趣的话可以去 sunnyxx 的《黑幕背后的Autorelease》了解相关信息。
最后这个 rootAutorelease2()
(这随性的命名)会调用到前面说到过的 autoreleaseFast()
函数,将对象加入 pool 中。
最后的最后再推荐一下《Objective-C 高级编程 iOS与OS X多线程和内存管理》 这本书,虽然里面有些内容过时了,但是里面的探究原理的思路非常地清晰,结合实际去学习还是很有趣味的。 :)