探究自动释放池的实现

上一篇依靠 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 对象作为节点,构成的双向链表这样的数据结构。用一张图来快速过一下它吧:

探究自动释放池的实现_第1张图片
page_dlist.png

图中有两个 AutoreleasePoolPage 对象(以下简称 page 对象),每一个都是虚拟内存页面的大小,除去底部(低地址)为 page 对象的成员变量所占的空间之外,剩余的内存空间看作一个栈,每个帧用来存储将要被释放的对象或者哨兵对象(用于区分 Pool 的边界)。next 其实也是 page 对象的成员变量,单独画出来是为了描述它作用:next 总是指向下一个可放入 id 对象的地址,直到栈被堆满后指向栈顶。而 hotPage 不是成员变量,它是通过 TLS (Thread Local Storage)与线程绑定的处于活跃状态的 page 对象,这个说明了两点:

  1. 多个线程直接不共享 page 对象,在多线程中使用 MRR 时要注意这个问题,免得对象多次被释放或未能完成释放;
  2. 凡是增加或删除对象都从这个活跃的 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 块:

探究自动释放池的实现_第2张图片
embed_pool.png

接着是一个 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多线程和内存管理》 这本书,虽然里面有些内容过时了,但是里面的探究原理的思路非常地清晰,结合实际去学习还是很有趣味的。 :)

你可能感兴趣的:(探究自动释放池的实现)