Objective-C runtime机制(5.2)——iOS 内存管理

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的博客):

4824593-fb905e7d612b7e80
这里写图片描述

在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>

4824593-098e3b8750fbf8a5
这里写图片描述

成员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的一道面试题

猜你喜欢

转载自blog.csdn.net/weixin_34268610/article/details/87077193