美文网首页iOS 杂谈
Objective-C 小记(8)autorelease

Objective-C 小记(8)autorelease

作者: KylinRoc | 来源:发表于2017-03-03 22:13 被阅读355次

    本文使用的 runtime 版本为 objc4-706

    对于 autorelease 的研究需要先从 @autoreleasepool { ... } 着手。首先对有 @autoreleasepool { ... } 的代码使用 clang -rewrite-objc 进行转换,在转换后的文件中,可以看到 @autoreleasepool { ... } 变成了这样:

    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
        ...
    }
    

    当然还可以找到 __AtAutoreleasePool 的定义:

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

    可以看到,代码利用了变量声明和自动变量在代码块结束后自动销毁的特性,在构造函数和析构函数中调用了 objc_autoreleasePoolPushobjc_autoreleasePoolPop 函数。在 NSObject.mm 文件中可以找到这两个函数的实现:

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

    可以发现,这两个函数只是对 AutoreleasePoolPage 这个 C++ 类的两个类方法 pushpop 的简单封装。

    AutoreleasePoolPage

    NSObject.mm 中可以找到 AutoreleasePoolPage 类的实现,先可以看一下它的成员变量:

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

    一个一个过一下这些成员变量:

    1. magic:这个变量的类型是 magic_t,是用来检查 AutoreleasePoolPage 的内存没有被修改的,放在第一个也就是这个原因,防止前面地址有内容溢过来。
    2. next:类型是 id *,存放的是下一个被 autorelease 的对象指针存放的地址。
    3. thread:对应的线程,这说明了自动释放池是对应线程的。
    4. parentchild:用来保存前一个 AutoreleasePoolPage 和后一个 AutoreleasePoolPage,就是一个双向链表,毕竟一个 AutoreleasePoolPage 能存放的对象是有限的。
    5. depth:很明显是这个链表有多深。
    6. hiwat:一个在 DEBUG 时才有用的参数,表示最高有记录过多少对象(hi-water)。

    可以注意到,这些成员变量并没有指示出对象记录在哪里,继续在 AutoreleasePoolPage 的实现里看一看,能发现一些有趣的东西:

        static size_t const SIZE = 
            PAGE_MAX_SIZE;  // size and alignment, power of 2
        
        static void * operator new(size_t size) {
            return malloc_zone_memalign(malloc_default_zone(), SIZE, SIZE);
        }
    

    AutoreleasePoolPage 重载了 new 操作符,这样一个新的对象需要 SIZE 这么多的内存空间,SIZE 的值 PAGE_MAX_SIZE 是一个根据机器不同的大小,在写这篇文章的机器上(i386)上是 4096。AutoreleasePoolPage 的成员变量大小加在一起也只有 56 字节,但是 new 它一个居然要 4096 字节,这剩下的 4040 字节肯定就是存放被 autorelease 的对象的地方了。AutoreleasePoolPage 的实现中有这些个函数:

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

    可以看到,begin 就是成员变量结束的地址(this+sizeof(*this)),end 就是整个申请的内存结束的地方了,其余的函数很好看懂。对于成员变量 next 来说,可以看一下构造函数:

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

    可以看到 next(begin())next 的初始值就是 begin。结合上面对 next 的描述,就能理解这个初始值的意义了。

    autorelease

    现在来研究一下 autorelease 是怎么实现的,autorelease 的入口是 objc_autorelease 函数:

    __attribute__((aligned(16)))
    id
    objc_autorelease(id obj)
    {
        if (!obj) return obj;
        if (obj->isTaggedPointer()) return obj;
        return obj->autorelease();
    }
    

    就是很简单的进行了判空和判断 tagged pointer 后,就将实现交给了 objc_object 结构体的 autorelease 函数:

    // Equivalent to [this autorelease], with shortcuts if there is no override
    inline id 
    objc_object::autorelease()
    {
        if (isTaggedPointer()) return (id)this;
        if (fastpath(!ISA()->hasCustomRR())) return rootAutorelease();
    
        return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_autorelease);
    }
    

    走的也是 reatinrelease 的老套路,如果没有自定义的实现,就走默认实现 rootAutorelease,否则直接给自定义实现发消息。继续查看默认实现:

    // Base autorelease implementation, ignoring overrides.
    inline id 
    objc_object::rootAutorelease()
    {
        if (isTaggedPointer()) return (id)this;
        if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;
    
        return rootAutorelease2();
    }
    

    其中 prepareOptimizedReturn 函数是 ARC 对 autorelease 的优化,本篇文章不做研究,继续查看 rootAutorelease2

    __attribute__((noinline,used))
    id 
    objc_object::rootAutorelease2()
    {
        assert(!isTaggedPointer());
        return AutoreleasePoolPage::autorelease((id)this);
    }
    

    果不其然,是调用了 AutoreleasePoolPage 里的实现(这不废话吗前面还讲了那么多关于 AutoreleasePoolPage 手动捂脸)。

    继续追查 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

    结合文章最开始的分析,push 函数就是往 AutoreleasePoolPage 这一整个内存空间里压入一个自动释放池,看一下 push 的实现:

    #   define POOL_BOUNDARY nil
    
        static inline void *push() 
        {
            id *dest = autoreleaseFast(POOL_BOUNDARY);
            assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
            return dest;
        }
    

    push 的实现里很有意思的往 autoreleaseFast 函数里传入了一个叫 POOL_BOUNDARY(池边界)的东西,可以看到它其实就是 nil。新建一个自动释放池为什么要和 autorelease 调用一样的函数呢?接下来分析一下 autoreleaseFast 函数。

    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 中,首先需要拿到一个 hot page,这个其实就是所在线程正在使用的 AutoreleasePoolPagehotPage 的实现有一点需要注意的地方:

        // EMPTY_POOL_PLACEHOLDER is stored in TLS when exactly one pool is 
        // pushed and it has never contained any objects. This saves memory 
        // when the top level (i.e. libdispatch) pushes and pops pools but 
        // never uses them.
    #   define EMPTY_POOL_PLACEHOLDER ((id*)1)
    
        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_get_direct 获得线程(TLS, Thread-local storage)的 AutoreleasePoolPage 对象,fastcheck 是对 magic 的检查,但是如果发现结果是 EMPTY_POOL_PLACEHOLDER 也就是 1 的话,也返回 nil

    EMPTY_POOL_PLACEHOLDER 从注释的说明可以知道,是对当只有一个自动释放池创建了(push 了)并且没有任何对象被 autorelease 时的优化。现在只需要知道它的存在就好。

    回到 autoreleaseFast 函数,在拿到 page 后,需要对 page 的不同情况进行不同的处理。

    先看最简单的情况,也就是有 hot page 并且它没有满的情况,这个时候调用了 pageadd 方法:

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

    可以看到,这就是将 obj 存到 next 的位置,并将 next 加 1,典型的入栈操作。如果 obj 是一个对象(autorelease 方法的调用),这就是将对象保存在自动释放池了,如果 objPOOL_BOUNDARY 也就是 nilpush 方法的调用)则这里便是自动释放池的分界。

    继续看 pagenil 的情况,也就是对 autoreleaseNoPage 函数的调用:

        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) {
                // 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);
        }
    

    进入到这个函数,会有两种情况,注释里也已经说明了,也是刚才 hotPage 函数实现注意的地方。hot page 是 EMPTY_POOL_PLACEHOLDER 也会被当作是 no page,进入这个函数。

    我们先假设一种情况,是第一个自动释放池创建时,首先对 haveEmptyPoolPlaceholder 函数的结果进行判断:

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

    这个函数其实就是判断 hot page 是不是 EMPTY_POOL_PLACEHOLDER,因为我们现在假设为第一次创建自动释放池,所以这个函数的返回值便是 false,并且 obj 参数的值是 POOL_BOUNDARY,因此 autoreleaseNoPage 会调用 setEmptyPoolPlaceholder 并返回,而 setEmptyPoolPlaceholder 的实现:

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

    就是将 hot page 设置为 EMPTY_POOL_PLACEHOLDER。这样,在第一次创建(push)一个自动释放池时,并没有生成 AutoreleasePoolPage 对象,而是使用了一个占位符。

    现在进入第二种情况,在上面的情况发生完之后,有一个对象被 autorelease 了,流程也会进入 autoreleaseNoPage,但是现在 haveEmptyPoolPlaceholder 返回的是 true 了,将会把 pushExtraBoundary 也设置为 true

    这样在接下来的代码中,会创建新的对象 page 并将它设置为 hot page,因为发现 pushExtraBoundaryture,因此还需要 add 一个 POOL_BOUNDARY。最后再将对象也加入,就完事了。

    最后看到 page 满了的情况,也就是对 autoreleaseFullPage 函数的调用:

        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 有没有还没满的 child(顺链表往下查),没有的话就新建一个,再使用 add 函数将 obj 记录。

    pop

    其实现在大概能感觉到,自动释放池其实就是个用链表实现的一个栈。继续看 pop 的实现也就一个自动释放池结束的时候:

        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);
                }
            }
    
            page->releaseUntil(stop);
    
            // memory: delete empty children
            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,传入的是 push 返回值,其实就是 push 函数插入 POOL_BOUNDARY 的地址(指针),在 pop 里表示要一直释放到 token 指向的地址为止。

    pop 函数一开始会检查 token 是不是 EMPTY_POOL_PLACEHOLDER。当 tokenEMPTY_POOL_PLACEHOLDER 时,会继续检查是否有 hot page(理论上来说不应该会有 hot page,一个疑问),如果没有 hot page,则直接将 hot page 设置为 nil,如果有 hot page,则重新调用 pop,传入的 tokencoldPage()-begin()coldPage 的实现如下:

        static inline AutoreleasePoolPage *coldPage() 
        {
            AutoreleasePoolPage *result = hotPage();
            if (result) {
                while (result->parent) {
                    result = result->parent;
                    result->fastcheck();
                }
            }
            return result;
        }
    

    很明显,所谓的 cold page 就是线程的第一个 AutoreleasePoolPage

    pop 函数的 token 不是 EMPTY_POOL_PLACEHOLDER 时,进入正常的 pop 流程,首先要获取到 token 也就是一个内存地址的所在 page,也就是 pageForPointer 函数的工作:

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

    因为 AutoreleasePoolPage 对象是根据 SIZE 的大小来对齐的,所以使用地址 p 的值对 SIZE 取余就能获取到 p 和所在 page 地址的偏移值(offset),从而得到所在 page,最后会对所在 page 的 magic 进行检查,也就是 fastcheck 所做的工作。

    获得了 page 以后,pop 函数还会检查在 token 这个地址存储的内容是否是 POOL_BOUNDRAY

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

    正常来说,在这个地方 token 就应该得是 POOL_BOUNDARY,因为 push 函数每次都是添加的 POOL_BOUNDARY。但这个地方进行了判断,其中如果如果 token 就是 pagebegin,并且 page 是第一个的话,则认为是正常情况(这其实是没有 push 就直接 autorelease 了)。否则进入 badPop 流程,这个流程会在最新的 SDK (10.12, 10.0, 10.0, 3.0)上会直接产生 fatal。

    接下来的正常流程,也就是 token 所指向的地址存储的内容为 POOL_BOUNDARY 时,调用 releaseUntil 函数:

        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);
                }
    
                id obj = *--page->next;
                memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
    
                if (obj != POOL_BOUNDARY) {
                    objc_release(obj);
                }
            }
    
            setHotPage(this);
        }
    

    实现很容易看懂,就是循环直到到 stop 给每个对象调用 objc_release 也就等同于发送 release 消息。其中每次都从 hot page 开始的原因注释里进行了说明,是怕 release 方法里又 autorelease 了对象。

    最后,pop 函数还要删除不需要的空的 page:

            // memory: delete empty children
            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();
                }
            }
    

    做了点小优化,如果现在这个 page 只剩下不到一半的空间了,则多留一个 childkill 的实现如下:

        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->child = nil;
                }
                delete deathptr;
            } while (deathptr != this);
        }
    

    其实就是沿着链表删除。

    总结

    总的来看,自动释放池的实现思想是很简单的:

    1. 对每个线程来说,用一个由 AutoreleasePoolPage 的组成的双向链表维护一个栈,被 autorelease 的对象记录在这个栈中;
    2. 使用 POOL_BOUNDARY 也就是 nil 来对自动释放池进行分隔。

    当然,其中实现还是有着各种有趣的细节的。

    本文原始地址:Objective-C 小记(8)autorelease

    相关文章

      网友评论

        本文标题:Objective-C 小记(8)autorelease

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