美文网首页程序员
OC Runtime之Autorelease

OC Runtime之Autorelease

作者: 08a2d4bd26c9 | 来源:发表于2017-09-28 14:23 被阅读0次

先说一点题外话,我本来是想写一篇OC Runtime之内存管理的文章。转念一想,题目可能太大,不太好驾驭,很容易就写出个几万字或者和已经存在于网络上的各种博客不默契的重合。鉴于读者大部分会有MRC和ARC的基本概念和基础,所以我想从Autorelease入手,结合源码,详细介绍一下OC最基础的内存管理机制。

首先要提的是不管是MRC还是ARC,背后的本质都是引用计数的模型。区别在于在ARC环境下,编译器会自动的在合适的时候插入retain,release等指令,免去了开发人员手动管理内存的复杂之处。但ARC也不是万能的,如果使用不当也会存在内存泄露或者非法访问的情况。最常见的问题大概是循环引用,即两个对象各自持有对方的强引用,或者更多对象存在循环强引用的情况。这些在开发过程中需要注意。

在具体讲解代码之前,我想首先概括一下Autorelease的实现方式。首先,Autorelease机制的实现主要是通过AutoreleasePoolPage这个数据结构来实现的。需要注意的是,苹果并没有定义一个类似于名字是AutoreleasePool的数据结构,而是使用了AutoreleasePoolPage,通过双向链表的方式,来实现了实际意义上的AutoreleasePool,链表中的AutoreleasePoolPage根据需要来进行增删。AutoreleasePool可以当做一个概念来对待,对外基本表现为一个栈,栈中存储的是需要Autorelease的对象的引用,并且是可以嵌套存储的。

AutoreleasePoolPage结构中定义了如下变量:

#   define EMPTY_POOL_PLACEHOLDER ((id*)1)

#   define POOL_BOUNDARY nil
    static pthread_key_t const key = AUTORELEASE_POOL_KEY;
    static uint8_t const SCRIBBLE = 0xA3;  // 0xA3A3A3A3 after releasing
    static size_t const SIZE = 
#if PROTECT_AUTORELEASEPOOL
        PAGE_MAX_SIZE;  // must be multiple of vm page size
#else
        PAGE_MAX_SIZE;  // size and alignment, power of 2
#endif
    static size_t const COUNT = SIZE / sizeof(id);

    magic_t const magic;
    id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;

parent和child很好理解,分别指向双向链表中的前一个和后一个节点。
depth记录当前page距离双向链表头结点的距离,也就是当前page在双向链表中的深度。
hiwat是high water的缩写,记录着当前pool的“水位”,基本可以理解为pool目前的容量。
magic是一个特殊的数据结构,主要用于校验AutoreleasePoolPage的合法性。
thread记录当前AutoreleasePoolPage所在的线程。在OC的内存模型中,每个线程都需要有自己的AutoreleasePool。
SIZE表示当前AutoreleasePoolPage的容量,其值被设置为PAGE_MAX_SIZE。PAGE_MAX_SIZE的值被定义为1<<14,也就是2的14次方。
EMPTY_POOL_PLACEHOLDER是一个很神奇的东西,当线程只push了最外层的一个pool,并且其中没有存放任何对象的时候,EMPTY_POOL_PLACEHOLDER被存储到了TLS中,来表示这种情况。这个好处显而易见,即在并未真正使用AutoreleasePool的情况下,避免了一个空page的内存分配。可见苹果的底层代码,对于内存的优化真是无所不用其极。至于TLS,即线程局部存储,简单来说可以理解为每个线程私有的一块存储空间。TLS具体的技术细节此处不表,不影响对于Autorelease机制的理解。
POOL_BOUNDARY是嵌套的pool使用的分隔符,值为nil。

id * begin() {
    return (id *) ((uint8_t *)this+sizeof(*this));
}

id * end() {
    return (id *) ((uint8_t *)this+SIZE);
}

bool empty() {
    return next == begin();
}

bool full() { 
    return next == end();
}

bool lessThanHalfFull() {
    return (next - begin() < (end() - begin()) / 2);
}

id *add(id obj)
{
    assert(!full());
    unprotect();
    id *ret = next;  // faster than `return next-1` because of aliasing
    *next++ = obj;
    protect();
    return ret;
}
  • 这些是AutoreleasePoolPage的一些基本的函数和实现。begin函数用于拿到page实际的存储空间的起始地址。因为page本身定义的变量也会占用存储空间,所以page的地址要加上这一部分的空间才是实际的存储空间。end函数用于拿到page的结束地址。empty函数用于判断page是否为空,也就是内部没有存储任何对象。full函数用于判断page是否已满。lessThanHalfFull函数顾名思义用于判断page内存占用率是否已经过半。
  • add函数用于向page中添加一个对象。unprotect和protect函数调用了mprotect函数对于内存进行了一定的保护,不做详解。有兴趣的读者可以自行探究mprotect的机制和作用。苹果的注释里有个好玩的地方,就是目前的实现要比直接return next-1要快一些。我的理解是return next-1的话要比目前的实现多一个-1的运算过程,不知道内中还有没有别的奥秘。
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;
}

向一个对象发送autorelease消息时会调用此函数,不过这个函数只是一个简单的封装。除去一些assert语句,函数内部调用了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);
    }
}

hotPage可以理解为当前活跃的AutoreleasePoolPage。如果当前page未满,则直接调用add方法,将对象添加到当前page中。如果当前page恰好已满,则调用autoreleaseFullPage函数,创建一个新的page并且添加对象入内。如果当前不存在hotPage(实际上就是没有可用的page),则调用autoreleaseNoPage函数来处理。

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,或者到达链表尾端并创建一个新的page。而后将这个page设置为hotPage,并且添加对象入内。逻辑上没什么难以理解的地方。

static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
    // "No page" could mean no pool has been pushed
    // or an empty placeholder pool has been pushed and has no contents yet
    assert(!hotPage());

    bool pushExtraBoundary = false;
    if (haveEmptyPoolPlaceholder()) {
        // We are pushing a second pool over the empty placeholder pool
        // or pushing the first object into the empty placeholder pool.
        // Before doing that, push a pool boundary on behalf of the pool 
        // that is currently represented by the empty placeholder.
        pushExtraBoundary = true;
    }
    else if (obj != POOL_BOUNDARY  &&  DebugMissingPools) {
        // We are pushing an object with no pool in place, 
        // and no-pool debugging was requested by environment.
        _objc_inform("MISSING POOLS: (%p) Object %p of class %s "
                              "autoreleased with no pool in place - "
                              "just leaking - break on "
                              "objc_autoreleaseNoPool() to debug", 
                              pthread_self(), (void*)obj, object_getClassName(obj));
        objc_autoreleaseNoPool(obj);
        return nil;
    }
    else if (obj == POOL_BOUNDARY  &&  !DebugPoolAllocation) {
        // We are pushing a pool with no pool in place,
        // and alloc-per-pool debugging was not requested.
        // Install and return the empty pool placeholder.
        return setEmptyPoolPlaceholder();
    }

    // We are pushing an object or a non-placeholder'd pool.

    // Install the first page.
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    setHotPage(page);
        
    // Push a boundary on behalf of the previously-placeholder'd pool.
    if (pushExtraBoundary) {
        page->add(POOL_BOUNDARY);
    }
        
    // Push the requested object or pool.
    return page->add(obj);
}

haveEmptyPoolPlaceholder函数去检查TLS中是否存有EMPTY_POOL_PLACEHOLDER。如果是的话,需要在插入真实对象前首先插入POOL_BOUNDARY,也就是nil值,作为嵌套的pool之间的分隔。
如果不是,也即是说已经分配了内存的情况下,插入一个非POOL_BOUNDARY的对象,在DebugMissingPools选项打开的情况下,会导致失败。因为这个时候并没有任何pool。如果要插入的对象就是POOL_BOUNDARY,也就是nil,在DebugPoolAllocation没有开启的情况下,也并不会真正分配内存,而是设置TLS中的EmptyPoolPlaceholder。

而后创建一个AutoreleasePoolPage,设置其父节点为nil,并将其设置为hotPage。而后根据需要插入POOL_BOUNDARY和对象。

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

这两个函数用于根据某个指针的地址找到指针所在的page。因为所有的page都是内存对齐的,所以p%SIZE就是p在其page中的偏移量。p-offset自然就是当前page的内存起始点。

void kill() 
{
    // Not recursive: we don't want to blow out the stack 
    // if a thread accumulates a stupendous amount of garbage
    AutoreleasePoolPage *page = this;
    while (page->child) page = page->child;

    AutoreleasePoolPage *deathptr;
    do {
        deathptr = page;
        page = page->parent;
        if (page) {
            page->unprotect();
            page->child = nil;
            page->protect();
        }
        delete deathptr;
    } while (deathptr != this);
}

kill函数用于清除掉当前AutoreleasePool所占用的所有内存空间。
首先找到链表最尾端的节点,而后从尾到头,依次释放链表的各节点。

static __attribute__((noinline))
id *autoreleaseNewPage(id obj)
{
    AutoreleasePoolPage *page = hotPage();
    if (page) return autoreleaseFullPage(obj, page);
    else return autoreleaseNoPage(obj);
}

该函数用于在当前hotPage已满的时候,调用autoreleaseFullPage函数来创建一个新的page;或者在当前没有任何page的时候,调用autoreleaseNoPage函数来处理。

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

该函数用于向AutoreleasePool中push一个新的pool。其实本质上就是插入一个nil用于区分边界。

static inline void pop(void *token) 
{
    AutoreleasePoolPage *page;
    id *stop;

    if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
        // Popping the top-level placeholder pool.
        if (hotPage()) {
            // Pool was used. Pop its contents normally.
            // Pool pages remain allocated for re-use as usual.
            pop(coldPage()->begin());
        } else {
            // Pool was never used. Clear the placeholder.
            setHotPage(nil);
        }
        return;
    }

    page = pageForPointer(token);
    stop = (id *)token;
    if (*stop != POOL_BOUNDARY) {
        if (stop == page->begin()  &&  !page->parent) {
            // Start of coldest page may correctly not be POOL_BOUNDARY:
            // 1. top-level pool is popped, leaving the cold page in place
            // 2. an object is autoreleased with no pool
        } else {
            // Error. For bincompat purposes this is not 
            // fatal in executables built with old SDKs.
            return badPop(token);
        }
    }

    if (PrintPoolHiwat) printHiwat();

    page->releaseUntil(stop);

    // memory: delete empty children
    if (DebugPoolAllocation  &&  page->empty()) {
        // special case: delete everything during page-per-pool debugging
        AutoreleasePoolPage *parent = page->parent;
        page->kill();
        setHotPage(parent);
    } else if (DebugMissingPools  &&  page->empty()  &&  !page->parent) {
        // special case: delete everything for pop(top) 
        // when debugging missing autorelease pools
        page->kill();
        setHotPage(nil);
    } 
    else if (page->child) {
        // hysteresis: keep one empty child if page is more than half full
        if (page->lessThanHalfFull()) {
            page->child->kill();
        }
        else if (page->child->child) {
            page->child->child->kill();
        }
    }
}

参数中的token可以理解为要pop到的终点。如果token的值为EMPTY_POOL_PLACEHOLDER,则意味着将整个pool中所有的对象全部pop出来。如果当前存在hotPage,说明pool曾经被使用过,则递归调用pop函数,参数传入第一个page的第一个对象。如果当前不存在hotPage,则调用setHotPage方法清除掉TLS中的EMPTY_POOL_PLACEHOLDER标记。

如果token的值非EMPTY_POOL_PLACEHOLDER,则调用pageForPointer函数,根据token找到其对应的page。如果token的值不是POOL_BOUNDARY,除非值是pool中第一个page的第一个对象,不然就是异常情况。

而后调用releaseUntil函数,进行实际的pop操作。而后清除相应的内存。

void releaseUntil(id *stop) 
{
    // Not recursive: we don't want to blow out the stack 
    // if a thread accumulates a stupendous amount of garbage
        
    while (this->next != stop) {
        // Restart from hotPage() every time, in case -release 
        // autoreleased more objects
        AutoreleasePoolPage *page = hotPage();

        // fixme I think this `while` can be `if`, but I can't prove it
        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);

#if DEBUG
    // we expect any children to be completely empty
    for (AutoreleasePoolPage *page = child; page; page = page->child) {
        assert(page->empty());
    }
#endif
}

该函数根据stop的位置,release在其后面的全部对象。

需要注意的是,本文并没有列举出全部的代码实现,只是挑选了比较关键的部分进行讲解,有些地方讲解也并没有很具体,没有涉及到很细节的东西,大都关注逻辑上和流程上。因为本文的目的并不在于让读者了解一些底层的技术细节,比如TLS,mprotect等等,而是给大家一个基本的印象,大致对于Autorelease是什么和如何实现的有一个基本的了解,可以解决一些开发过程中遇到的问题,也可以避免一些错误的对于Autorelease的使用。有些问题没有讲清楚的,可以随时沟通交流。

相关文章

网友评论

    本文标题:OC Runtime之Autorelease

    本文链接:https://www.haomeiwen.com/subject/vjkpdxtx.html