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 对象,这个说明了两点:
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 中。