美文网首页Rumtime
Objective-C runtime机制(5.2)——iOS

Objective-C runtime机制(5.2)——iOS

作者: 无忘无往 | 来源:发表于2019-01-23 11:51 被阅读5次

    autoreleasepool

    在iOS中,除了需要手动retain,release(现在已经交给了ARC自动生成)外,我们还可以将对象扔到自动释放池中,由自动释放池来自动管理这些对象。我们可以这样使用autoreleasepool:

    int main(int argc, char * argv[]) {
        @autoreleasepool {
            NSString *a =  [NSString stringWithFormat:@"%d", 1];
        }
    }
    

    clang -rewrite-objc 重写后,得到:

    int main(int argc, char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
            NSString *a = ((NSString * _Nonnull (*)(id, SEL, NSString * _Nonnull, ...))(void *)objc_msgSend)((id)objc_getClass("NSString"), sel_registerName("stringWithFormat:"), (NSString *)&__NSConstantStringImpl__var_folders_8k_3pbszhls2czcmz0w335cvc0w0000gn_T_main_1a8fc0_mi_1, 1);
    
        }
    }
    

    这时会发现, @autoreleasepool 被改写为了 __AtAutoreleasePool __autoreleasepool这样一个对象。__AtAutoreleasePool的定义为:

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

    于是,关于@autoreleasepool的代码可以被改写为:

    objc_autoreleasePoolPush();
    // Do your code
    objc_autoreleasePoolPop(atautoreleasepoolobj);
    

    置于@autoreleasepool的{}中的代码实际上是被一个push和pop操作所包裹。当push时,会压栈一个autoreleasepage,在{}中的所有的autorelease对象都会放到这个page中。当pop时,会出栈一个autoreleasepage,同时,所有存储于这个page的对象都会做release操作。这就是autoreleasepool的实现原理。

    objc_autoreleasePoolPush()objc_autoreleasePoolPop(atautoreleasepoolobj)的实现如下:

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

    它们都分别调用了AutoreleasePoolPage类的静态方法push和pop。AutoreleasePoolPage 是runtime中autoreleasepool的核心实现,下面,我们就来了解一下它。

    AutoreleasePoolPage

    AutoreleasePoolPage在runtime中的定义如下:

    class AutoreleasePoolPage 
    {
        magic_t const magic;                // 魔数,用于自身的完整性校验                                                         16字节
        id *next;                           // 指向autorelePool page中的下一个可用位置                                           8字节
        pthread_t const thread;             // 和autorelePool page中相关的线程                                                  8字节
        AutoreleasePoolPage * const parent; // autoreleasPool page双向链表的前向指针                                             8字节
        AutoreleasePoolPage *child;         // autoreleasPool page双向链表的后向指针                                             8字节
        uint32_t const depth;               // 当前autoreleasPool page在双向链表中的位置(深度)                                   4字节
        uint32_t hiwat;                     // high water mark. 最高水位,可用近似理解为autoreleasPool page双向链表中的元素个数       4字节
    
        // SIZE-sizeof(*this) bytes of contents follow
    }
    

    每个AutoreleasePoolPage的大小为一个SIZE,即内存管理中一个页的大小。这在Mac中是4KB,而在iOS中,这里没有相关代码,估计差不多。

    对象指针栈

    由源码可用看出,在AutoreleasePoolPage 类中共有7个成员属性,大小为56Bytes,按照一个Page是4KB计算,显然还有4040Bytes没有用。而这4040Bytes空间,就用来存储AutoreleasePoolPage所管理的对象指针。因此,一个AutoreleasePoolPage的内存布局如下图(摘自Draveness的博客):

    这里写图片描述

    在autoreleasepool中的对象指针是按照栈的形式存储的,栈低是一个POOL_BOUNDARY哨兵,之后对象指针依次入栈存储。

    POOL_BOUNDARY

    在图中可用看到,除了AutoreleasePoolPage 类中的7个成员之外,还有一个叫POOL_BOUNDARY, 其实这是一个nil指针,AutoreleasePoolPage中的next指针用来指向栈中下一个入栈位置。

    #   define POOL_BOUNDARY nil
    

    它作为一个哨兵,当需要将AutoreleasePoolPage 中存储的对象指针依次出栈时,会执行到POOL_BOUNDARY为止。

    双向链表

    在图中也可以看出,<font color=blue>单个的AutoreleasePoolPage是以栈的形式存储的。</font>
    当加入到autoreleasepool中的元素太多时,一个AutoreleasePoolPage 就不够用的了。这时候我们需要新创建一个AutoreleasePoolPage ,<font color=orange>多个AutoreleasePoolPage之间通过双向链表的形式串起来。</font>

    这里写图片描述

    成员parentchild就是用来构造双向链表的。

    下面我们就结合具体的代码,来看一下AutoreleasePoolPage是如何在系统中发挥作用的。

    Push

    当用户调用@autoreleasepool{}的时候,系统首先会调用AutoreleasePoolPage::push()方法,来创建或获取当前的hotPage,并向对象栈中插入一个POOL_BOUNDARY

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

    我们也可以调用autorelease(id obj)方法将某个特定的对象指针插入到AutoreleasePoolPage中:

    static inline id autorelease(id obj)
        {
            assert(obj);
            assert(!obj->isTaggedPointer());  // 注意这个assert,由于tagged pointer不遵循引用计数规则,所以也不会有autorelease操作。
            id *dest __unused = autoreleaseFast(obj);
            assert(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);
            return obj;
        }
    

    可以看到,无论是push还是autorelease方法,最后都是调用了autoreleaseFast(obj),该方法会将一个id放入到autoreleasePage中。:

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

    可以看到方法实现逻辑也很简单:

    1. 首先取出当前的hotPage,所谓hotPage,就是在autoreleasePage链表中正在使用的autoreleasePage节点。
    2. 如果有hotPage,且hotPage还没满,这将obj加入到page中。
    3. 如果有hotPage,但是已经满了,则进入page full逻辑(autoreleaseFullPage)。
    4. 如果没有hotPage,进入no page逻辑autoreleaseNoPage

    hotPage

    hotPage是autoreleasePage链表中正在使用的autoreleasePage节点。实质上是指向autoreleasepage的指针,并存储于线程的TSD(线程私有数据:Thread-specific Data)中:

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

    从这段代码可以看出,
    <font color=orange>autoreleasepool是和线程绑定的,一个线程对应一个autoreleasepool。而autoreleasepool虽然叫做自动释放池,其实质上是一个双向链表。</font>

    在介绍runloop的时候,我们也曾提到过,runloop和线程也是一一对应的,并且在当前线程的runloop指针,也会存储到线程的TSD中。这是runtime对于TSD的一个应用。

    add object

    如果有hot page,先判断page 是否已经full了,判断逻辑是next*是否等于end()

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

    关于begin()end(),定义如下,结合page的图示,应该比较容易理解:

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

    如果page没有满,这调用page的add方法:

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

    逻辑比较简单,就是将obj置于next的位置,next++,然后返回obj的位置。

    autoreleaseFullPage

    如果hot page满了,就需要在链表中‘加页’,同时将新页置为hot 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);
        }
    

    这一段代码重点需要关注的是寻找可用pagedo while逻辑。
    其实注释中已经写得很清楚,系统会首先尝试在hot pagechild pages中挑出第一个没有满的page,如果没有符合要求的child page,则只能创建一个新的new AutoreleasePoolPage(page)

    最后,将挑选出的page作为当前线程的hot page (实际上存储到了TSD中),并将obj存到新的hot page中。

    autoreleaseNoPage

    若当前线程没有hot Page,则说明当前的线程还未建立起autorelease pool 。这时,就会调用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()) { // 如果当前线程只有一个虚拟的空池,则这次需要真正创建一个page
                // 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  &&  !DebugPoolAllocation) { // 如果obj == POOL_BOUNDARY,这里苹果有个小心机,它不会真正创建page,而是在线程的TSD中做了一个空池的标志
                // 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.
    
            // 创建线程的第一个page,并置为hot page。
            AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
            setHotPage(page);
            
            // 如果之前只是做了空池标记,这里还需要在栈中补上POOL_BOUNDARY,作为栈底哨兵
            if (pushExtraBoundary) {
                page->add(POOL_BOUNDARY);
            }
            
            // Push the requested object or pool.  注意,这里的注释,进入page的不光可以有object,还可以是pool。
            return page->add(obj);
        }
    

    当系统发现当前线程没有对应的autoreleasepool时,我们自然的想到需要为线程创建一个page。但是苹果其实在这里是耍了一个小心机的,当在创建第一个page时,苹果并不会真正创建一个page,因为它害怕创建了page后,并没有真正的object需要插入page,这样就造成了无谓的内存浪费。

    在没有第一个真正的object入栈之前,苹果是这样做的:仅仅在线程的TSD中做了一个EMPTY_POOL_PLACEHOLDER标记,并返回它。这里没有真正的new 一个AutoreleasePoolPage

    Pop

    当autoreleasepool需要被释放时,会调用Pop方法。而Pop方法需要接受一个void *token参数,来告诉池子,需要一直释放到token对应的那个page:

    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 {
                    // 这是为了兼容旧的SDK,看来在新的SDK里面,token 可能的取值只有两个:POOL_BOUNDARY, page->begin() && !page->parent
                    // Error. For bincompat purposes this is not
                    // fatal in executables built with old SDKs.
                    return badPop(token);
                }
            }
            // 对page中的object做objc_release操作,一直到stop
            page->releaseUntil(stop);
    
            // memory: delete empty children 删除多余的child,节约内存
             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();
                }
            }
        }
    

    何时需要autoreleasePool

    OK,以上就是autoreleasepool的内容。那么在ARC的环境下,我们何时需要用@autoreleasepool呢?

    一般的,有下面两种情况:

    1. 创建子线程。当我们创建子线程的时候,需要将子线程的runloop用@autoreleasepool包裹起来,进而达到自动释放内存的效果。因为系统并不会为子线程自动包裹一个@autoreleasepool,这样加入到autoreleasepage中的元素就得不到释放。
    2. 在大循环中创建autorelease对象。当我们在一个循环中创建autorelease对象(不是用alloc创建的对象),该对象会加入到autoreleasepage中,而这个page中的对象,会等到外部池子结束才会释放。在主线程的runloop中,会将所有的对象的释放权都交给了RunLoop 的释放池,而RunLoop的释放池会等待这个事件处理之后才会释放,因此就会使对象无法及时释放,堆积在内存造成内存泄露。关于这一点,可以参考博客RunLoop和autorelease的一道面试题

    相关文章

      网友评论

        本文标题:Objective-C runtime机制(5.2)——iOS

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