探究自动释放池的实现

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

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 块:

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,843评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,538评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,187评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,264评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,289评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,231评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,116评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,945评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,367评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,581评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,754评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,458评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,068评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,692评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,842评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,797评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,654评论 2 354

推荐阅读更多精彩内容