OC源码 —— autoreleasepool

因为现在普遍使用ARC,所以项目中几乎看不到release这样的字眼了,但是在一个不起眼的地方 —— main.m,有一个@autoreleasepool,本文就是要研究一下这货啦。

// iOS项目的main.m
int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

一个疑问

在我对autoreleasepool有一点理解之后,我就发现了一个奇怪的问题:既然main函数并不会真的return,那么这个autoreleasepool究竟有什么用呢?

作为对比,在macOS的项目中,main函数中的return就没有被autoreleasepool包围,是这个样子的:

// macOS项目的main.m
int main(int argc, const char * argv[]) {
    return NSApplicationMain(argc, argv);
}

难不成iOS项目的@autoreleasepool真的无关紧要?

autoreleasepool

还是老老实实看一看@autoreleasepool究竟是什么吧。

新建一个iOS项目,用clang重写一下main.m(当然在macOS项目中手写一个@autoreleasepool也没问题)。在main.cpp文件的最后,看到main函数变成了这样:

int main(int argc, char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        return UIApplicationMain(argc, argv, __null, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class"))));
    }
}

原先的@autoreleasepool没有了,看起来__AtAutoreleasePool才是其真实面目,搜索一下__AtAutoreleasePool,它的定义是这样的:

struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

就一个构造和一个析构函数,分别调用了这两个方法:

objc_autoreleasePoolPush()
objc_autoreleasePoolPop(obj)

看到这个方法名,首先想到的是栈,难道autoreleasepool的数据结构就是栈吗?

先看看push方法的实现:

void *
objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}

内部调用了一个c++类的类方法,看看这个类是怎么定义的:

class AutoreleasePoolPage 
{
    ...
    static size_t const SIZE = PAGE_MAX_SIZE;
    ...
    magic_t const magic;                   // 16字节
    id *next;                              // 8字节
    pthread_t const thread;                // 8字节
    AutoreleasePoolPage * const parent;    // 8字节 
    AutoreleasePoolPage *child;            // 8字节
    uint32_t const depth;                  // 4字节
    uint32_t hiwat;                        // 4字节
    ...
}

#define PAGE_MAX_SIZE           PAGE_SIZE
#define PAGE_SIZE              I386_PGBYTES
#define I386_PGBYTES            4096

每个page大小是4096字节,固定字段占用了56字节。剩余的部分都可以用来存放加入到page中的对象地址。

注意这两个字段:

AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;

这两个字段说明了pool并不是栈结构,而是一个双向链表。

想要了解这些字段的意思,必须看看这个类的构造函数:

AutoreleasePoolPage(AutoreleasePoolPage *newParent) 
    : magic(), next(begin()), thread(pthread_self()),
      parent(newParent), child(nil), 
      depth(parent ? 1+parent->depth : 0), 
      hiwat(parent ? parent->hiwat : 0)
{ 
    if (parent) {
        parent->check();
        assert(!parent->child);
        parent->unprotect();
        parent->child = this;
        parent->protect();
     }
     protect();
}

除了parent和child以外,其他字段的含义如下:

  • magic
    用作校验
  • next
    通过begin()方法初始化:
id * begin() {
    return (id *) ((uint8_t *)this+sizeof(*this));
}

返回的是当前page空闲的首位置,即可以用于存放对象地址的第一个位置,即56个字节之后开始的地方。

  • thread
    当前pool所处的线程
  • depth
    page的深度,首次为0,以后每次初始化一个page都加1。
  • hiwat
    这个字段是high water的缩写,这个字段用来计算pool中最多存放的对象个数。在每次执行pop()的时候,会更新一下这个字段。
// Check and propagate high water mark
// Ignore high water marks under 256 to suppress noise.
AutoreleasePoolPage *p = hotPage();
uint32_t mark = p->depth*COUNT + (uint32_t)(p->next - p->begin());
if (mark > p->hiwat  &&  mark > 256) {
    for( ; p; p = p->parent) {
        p->unprotect();
        p->hiwat = mark;
        p->protect();
    }
    ...
}

push方法

类的结构已经清楚了,回过去看看push()方法的实现:

static inline void *push() 
{
    id *dest;
    if (DebugPoolAllocation) {
        // Each autorelease pool starts on a new pool page.
        dest = autoreleaseNewPage(POOL_BOUNDARY);
    } else {
        dest = autoreleaseFast(POOL_BOUNDARY);
    }
    assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
    return dest;
}

#   define POOL_BOUNDARY nil

因为目的是了解autoreleasepool的真实表现,所以我们只需要关心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);
    }
}

这个方法是autorelease的关键,分成4个部分来研究一下:

  • AutoreleasePoolPage *page = hotPage();
  • page->add(obj);
  • autoreleaseFullPage(obj, page);
  • autoreleaseNoPage(obj);

part1

AutoreleasePoolPage *page = hotPage();

static inline AutoreleasePoolPage *hotPage() 
{
    AutoreleasePoolPage *result = (AutoreleasePoolPage *) tls_get_direct(key);
    if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
    if (result) result->fastcheck();
    return result;
}

看看hotPage()方法的源码,这里使用了tls的方法来存取当前的page。

TLS

TLS是线程局部存储(Thread Local Storage)的缩写,在oc中通过这两个方法来使用:

static inline void *tls_get_direct(tls_key_t k) 
{ 
    assert(is_valid_direct_key(k));

    if (_pthread_has_direct_tsd()) {
        return _pthread_getspecific_direct(k);
    } else {
        return pthread_getspecific(k);
    }
}

static inline void tls_set_direct(tls_key_t k, void *value) 
{ 
    assert(is_valid_direct_key(k));

    if (_pthread_has_direct_tsd()) {
        _pthread_setspecific_direct(k, value);
    } else {
        pthread_setspecific(k, value);
    }
}

其实就是通过键值对来存取信息,但是很快我发现了一个问题:这个key是定义在AutoreleasePoolPage类中的:

static pthread_key_t const key = AUTORELEASE_POOL_KEY;
#define AUTORELEASE_POOL_KEY  ((tls_key_t)__PTK_FRAMEWORK_OBJC_KEY3)
#define __PTK_FRAMEWORK_OBJC_KEY3   43

但是如果key是全局的,那么对于不同的线程来说,通过相同的key不是会取到相同的page吗,那么不同线程的autoreleasepool之间不是乱套了吗?

当然这个担心是多余的,我用c++测试了一下,即使key是全局的,不同线程通过这个key操作的也是不同的内存区域,不会互相影响。

回过去看hotPage(),就可以理解成当前使用的这个page,第一次调用push的时候,当然还没有page,所以通过hotPage()是获取不到的。

所以我们先看一看最后那个部分。

part2

autoreleaseNoPage(obj)

static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
    ...
    bool pushExtraBoundary = false;
    if (haveEmptyPoolPlaceholder()) {
        pushExtraBoundary = true;
    }
    else if (obj != POOL_BOUNDARY  &&  DebugMissingPools) {
        ...
    }
    else if (obj == POOL_BOUNDARY  &&  !DebugPoolAllocation) {
        return setEmptyPoolPlaceholder();
    }
    
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    setHotPage(page);
    
    if (pushExtraBoundary) {
        page->add(POOL_BOUNDARY);
    }
    
    return page->add(obj);
}

static inline bool haveEmptyPoolPlaceholder()
{
    id *tls = (id *)tls_get_direct(key);
    return (tls == EMPTY_POOL_PLACEHOLDER);
}

第一次调用这个方法时,haveEmptyPoolPlaceholder()返回的是false,所以会进入最后一个if判断中,调用setEmptyPoolPlaceholder():

static inline id* setEmptyPoolPlaceholder()
{
    assert(tls_get_direct(key) == nil);
    tls_set_direct(key, (void *)EMPTY_POOL_PLACEHOLDER);
    return EMPTY_POOL_PLACEHOLDER;
}

这个方法调用完之后,再次进入autoreleaseNoPage()时,就会进入第一个if判断中了。接着就会走到这个方法的最后那部分,创建一个新page,设置为hotpage。给page中添加一个边界标记,最后把将要autorelease的对象添加进来。

关于整个调用流程,最后会做一下汇总。

part3

page->add(obj);

id *add(id obj)
{
    assert(!full());
    unprotect();
    id *ret = next;  // faster than `return next-1` because of aliasing
    *next++ = obj;
    protect();
    return ret;
}

这个方法很简单,就是给把obj添加到page中,然后把next指针向后移动一步。

part4

autoreleaseFullPage(obj, page);

static __attribute__((noinline))
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
    // The hot page is full. 
    // Step to the next non-full page, adding a new page if necessary.
    // Then add the object to that page.
    assert(page == hotPage());
    assert(page->full()  ||  DebugPoolAllocation);

    do {
        if (page->child) page = page->child;
        else page = new AutoreleasePoolPage(page);
    } while (page->full());

    setHotPage(page);
    return page->add(obj);
}

如果一个page满了,那么就需要新建一个页,然后把链表的链条接上。最后同样把新建的页设为hotpage,并把obj添加进来。

autorelease方法

static inline id autorelease(id obj)
{
    assert(obj);
    assert(!obj->isTaggedPointer());
    id *dest __unused = autoreleaseFast(obj);
    assert(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);
    return obj;
}

这个方法也很简单,就调用了autoreleaseFast(),只不过这个时候参数是调用这个方法的对象,而不是push方法中的边界标识。

所以autorelease的过程并不是对象真正释放的过程,这也是autorelease的作用,说白了就是延迟释放的时机。真正释放的时机是在@autoreleasepool块结束的时候,那个时候会调用pop()方法。

pop方法

static inline void pop(void *token) 
{
    AutoreleasePoolPage *page;
    id *stop;
(*******part1*******)
    if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
        if (hotPage()) {
            pop(coldPage()->begin());
        } else {
            setHotPage(nil);
        }
        return;
    }

    page = pageForPointer(token);

(*******part2*******)
    stop = (id *)token;
    if (*stop != POOL_BOUNDARY) {
        if (stop == page->begin()  &&  !page->parent) {
        } else {
            return badPop(token);
        }
    }

(*******part3*******)
    ...
    page->releaseUntil(stop);
    ...
    if (page->child) {
        if (page->lessThanHalfFull()) {
            page->child->kill();
        }
        else if (page->child->child) {
            page->child->child->kill();
        }
    }
}

这个方法大致可以分为3个部分,

  • part1部分,判断token是否是EMPTY_POOL_PLACEHOLDER,这个东西是autoreleasepool首次push的时候返回的,也就是最顶层的pop会执行这一部分
  • part2部分,这部分不太理解,主要是不清楚什么时候token会不等于POOL_BOUNDARY,先略去。

@KylinRoc提示,在非ARC情况下,在新创建的线程中不使用autoreleasepool,直接调用autorelease方法时会出现这个情况。此时没有pool,直接进行autorelease。详细情况请参考评论。

  • part3部分,多数情况下,都会进入到这一部分。重点说一下这个部分。

我们可以先把autoreleasepool运行的过程大概表示一下:

token1 = push()
...
    token2 = push()
    ...
        token3 = push()
        ...
        pop(token3)
    ...
    pop(token2)
...
pop(token1)

每次pop,实际上都会把最近一次push之后添加进去的对象全部release掉,所以autoreleasepool是nest结构完全没问题。

在part3开始前,先通过token获取了一下当前的page:

page = pageForPointer(token);

static AutoreleasePoolPage *pageForPointer(const void *p) 
{
    return pageForPointer((uintptr_t)p);
}

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;
}

这个方法可以缩写为以下几步:

uintptr_t offset = p % SIZE;
result = (AutoreleasePoolPage *)(p - offset);

例如:
p = 0x100623bc2
offset = p % 4096 = 0xbc2
result = p - offset = 0x100623000

获取到page的地址之后,就可以调用release方法了:

page->releaseUntil(stop);

void releaseUntil(id *stop) 
{
    while (this->next != stop) {
        AutoreleasePoolPage *page = hotPage();

        while (page->empty()) {
            page = page->parent;
            setHotPage(page);
        }

        page->unprotect();
        id obj = *--page->next;
        memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
        page->protect();

        if (obj != POOL_BOUNDARY) {
            objc_release(obj);
        }
    }

    setHotPage(this);
}

从next指针开始,一个一个向前调用release方法,直到碰到push时压入的token为止。

最后就是kill child的过程:

if (page->child) {
    if (page->lessThanHalfFull()) {
        page->child->kill();
    }
    else if (page->child->child) {
        page->child->child->kill();
    }
}

如果当前page小于一半满,则把当前页的所有孩子都杀掉,否则,留下一个孩子,从孙子开始杀。正是因为这一步,在autoreleaseFullPage()方法中才会有这两步:

if (page->child) page = page->child;
else page = new AutoreleasePoolPage(page);

而不是直接new一个新的page出来。

这么做是不是出于性能的考虑?

总结

汇总一下autoreleasepool的调用流程:

  1. 使用@autoreleasepool标记,调用push()方法;
  2. 没有hotpage,调用autoreleaseNoPage()方法,设置EMPTY_POOL_PLACEHOLDER;
  3. 在@autoreleasepool块内部,第一次有对象调用autorelease,进而调用autoreleaseFast(),此时依然没有hotpage,进入autoreleaseNoPage();

这么说是不准确的,通常在我们的代码第一次调用autorelease的时候,page已经被创建出来了,但没关系,意思还是这么个意思。

  1. 因为在2设置了EMPTY_POOL_PLACEHOLDER,所以会设置本页为hotpage,添加边界标记POOL_BOUNDARY,最后添加obj;
  2. 继续有对象调用autorelease,此时已经有了page,调用page->add(obj);
  3. 如果page满了,调用autoreleaseFullPage()创建新page,重复步骤5;
  4. 到达autoreleasepool边界,调用pop方法,通常情况下会释放掉POOL_BOUNDARY之后的所有对象。

autoreleasepool和runloop关系挺大,后面写runloop的时候再分析用法吧。

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

推荐阅读更多精彩内容