iO内存管理之Autoreleasepool

有关内存管理的相关优化方案和引用计数的相关原理,我们已经了解,本章来讲解在内存管理中的另一个方案Autoreleasepool

初探Autoreleasepool

Autoreleasepool作用

通过之前章节的学习,我们知道在ARC下,LLVM编译器会自动帮我们生产retain、release和autorelease等代码,减少了在MRC下的工作量。调用autorelease会将该对象添加进自动释放池中,它会在一个恰当的时刻自动给对象调用release,所以autorelease相当于延迟了对象的释放。
但是在ARC下,autorelease方法已被禁用,我们可以使用__autoreleasing修饰符修饰对象将对象注册到自动释放池中。

Autoreleasepool创建

  • 在MRC下,可以使用NSAutoreleasePool或者@autoreleasepool。建议使用@autoreleasepool,苹果说它比NSAutoreleasePool快大约六倍。
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// Code benefitting from a local autorelease pool.
[pool release];
  • 而在ARC下,已经禁止使用NSAutoreleasePool类创建自动释放池,只能使用@autoreleasepool。
@autoreleasepool {
    // Code benefitting from a local autorelease pool.
}

The Application Kit creates an autorelease pool on the main thread at the beginning of every cycle of the event loop, and drains it at the end, thereby releasing any autoreleased objects generated while processing an event. If you use the Application Kit, you therefore typically don’t have to create your own pools. If your application creates a lot of temporary autoreleased objects within the event loop, however, it may be beneficial to create “local” autorelease pools to help to minimize the peak memory footprint.

以上是苹果对自动释放池的一段介绍,其意思为:AppKit 和 UIKit 框架在事件循环(RunLoop)的每次循环开始时,在主线程创建一个自动释放池,并在每次循环结束时销毁它,在销毁时释放自动释放池中的所有autorelease对象。通常情况下我们不需要手动创建自动释放池,但是如果我们在循环中创建了很多临时的autorelease对象,则手动创建自动释放池来管理这些对象可以很大程度地减少内存峰值。


Autoreleasepool原理探究

Autoreleasepool底层结构

我们知道在main函数中,会创建一个@autoreleasepool {}对象,那么其底层的结构是怎样的呢?

int main(int argc, const char * argv[]) {
    @autoreleasepool {}
    return 0;
}

我们还是使用clang -rewrite-objc main.m命令,转换为C++代码查看。通过以下代码,我们可以发现转换后@autoreleasepool主要做了以下几点:

  • @autoreleasepool底层是创建了一个__AtAutoreleasePool结构体对象;
  • 在创建__AtAutoreleasePool结构体时会在构造函数中调用objc_autoreleasePoolPush()函数,并返回一个atautoreleasepoolobj(POOL_BOUNDARY存放的内存地址,下面会讲到);
  • 在释放__AtAutoreleasePool结构体时会在析构函数中调用objc_autoreleasePoolPop()函数,并将atautoreleasepoolobj传入。
struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; }
    return 0;
}

AutoreleasePoolPage底层结构

首先来看AutoreleasePoolPage的相关源码,其几个成员变量的含义如下:

  • magic:用来校验AutoreleasePoolPage的结构是否完整。

  • next:next指向的是下一个AutoreleasePoolPage中下一个为空的内存地址(新来的对象会存储到next处),初始化时指向begin()。

  • thread:保存了当前页所在的线程(一个AutoreleasePoolPage属于一个线程,一个线程中可以有多个AutoreleasePoolPage)。

  • parent:指向父节点,第一个parent节点为nil。

  • child:指向子节点,最后一个child节点为nil。

  • depth:代表深度,从0开始,递增+1。

  • hiwat:代表high water Mark最大入栈数。

  • SIZE:AutoreleasePoolPage的大小,值为PAGE_MAX_SIZE,4096个字节,其中56个字节用来存储自己的变量,剩下的4040个字节用来存储要释放的对象,也就是最多505个对象。

  • POOL_BOUNDARY:

    只是nil的别名。前世叫做POOL_SENTINEL,称为哨兵对象或者边界对象;
    POOL_BOUNDARY用来区分不同的自动释放池,以解决自动释放池嵌套的问题
    每当创建一个自动释放池,就会调用push()方法将一个POOL_BOUNDARY入栈,并返回其存放的内存地址;
    当往自动释放池中添加autorelease对象时,将autorelease对象的内存地址入栈,它们前面至少有一个POOL_BOUNDARY;
    当销毁一个自动释放池时,会调用pop()方法并传入一个POOL_BOUNDARY,会从自动释放池中最后一个对象开始,依次给它们发送release消息,直到遇到这个POOL_BOUNDARY。

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

    AutoreleasePoolPageData(__unsafe_unretained id* _next, pthread_t _thread, AutoreleasePoolPage* _parent, uint32_t _depth, uint32_t _hiwat)
        : magic(), next(_next), thread(_thread),
          parent(_parent), child(nil),
          depth(_depth), hiwat(_hiwat)
    {
    }
};

----------------------------------------------------------------------------------

class AutoreleasePoolPage : private AutoreleasePoolPageData
{
    friend struct thread_data_t;

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

    // 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)

#   define POOL_BOUNDARY nil

    // SIZE-sizeof(*this) bytes of contents follow
    ......
}

翻译如下

一个线程的自动释放池是一个指针的堆栈结构。
每个指针代表一个需要释放的对象或者POOL_BOUNDARY(自动释放池边界)
一个 pool token 就是这个 pool 所对应的 POOL_BOUNDARY 的内存地址。当这个 pool 被 pop 的时候,所有内存地址在 pool token 之后的对象都会被 release。
这个堆栈被划分成了一个以 page 为结点的双向链表。pages 会在必要的时候动态地增加或删除。
Thread-local storage(线程局部存储)指向 hot page ,即最新添加的 autoreleased 对象所在的那个 page 。

通过上面对成员变量的解析和上方官方的注释,我们可以知道AutoreleasePoolPage底层结构如下:

  • AutoreleasePoolPage是以栈为结点通过双向链表的形式组合而成;遵循先进后出规则,整个自动释放池由一系列的AutoreleasePoolPage组成的,而AutoreleasePoolPage是以双向链表的形式连接起来。
  • 自动释放池与线程一一对应;
  • 每个AutoreleasePoolPage对象占用4096字节内存,其中56个字节用来存放它内部的成员变量,剩下的空间(4040个字节)用来存放autorelease对象的地址。要注意的是第一页只有504个对象,因为在创建page的时候会在next的位置插入1个POOL_SENTINEL。
  • POOL_BOUNDARY为哨兵对象,入栈时插入,出栈时释放对象到此传入的哨兵对象

该图表示AutoreleasePoolPage的双向列表结构



该图表示AutoreleasePoolPage的双向列表和栈结构


AutoreleasePoolPage::push()原理

首先我们看objc_autoreleasePoolPush的源码,发现其内部就是调用了AutoreleasePoolPage的push()方法。

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

来到AutoreleasePoolPage内部的push()方法,其中slowpath表示很少会走到,是底部的容错处理,所以最终会走到autoreleaseFast方法中

static inline void *push() 
    {
        id *dest;
        if (slowpath(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;
    }

查看autoreleaseFast源码,先是调用了hotPage(),hotPage()方法就是用来获得新创建的未满的Page。其内部主要是判断逻辑:

  • 如果当前 Page 存在且未满,走page->add(obj)将 autorelease 对象入栈,即添加到当前 Page 中
  • 如果当前 Page 存在但已满,走autoreleaseFullPage,创建一个新的 Page,并将 autorelease 对象添加进去
  • 如果当前 Page 不存在,即还没创建过 Page,创建第一个 Page,并将 autorelease 对象添加进去
    static inline id *autoreleaseFast(id obj)
    {
        AutoreleasePoolPage *page = hotPage(); // 双向链表中的最后一个 Page
        if (page && !page->full()) {// 如果当前 Page 存在且未满
            return page->add(obj);      // 将 autorelease 对象入栈,即添加到当前 Page 中;
        } else if (page) { // 如果当前 Page 存在但已满
            return autoreleaseFullPage(obj, page); // 创建一个新的 Page,并将 autorelease 对象添加进去
        } else {// 如果当前 Page 不存在,即还没创建过 Page
            return autoreleaseNoPage(obj);      // 创建第一个 Page,并将 autorelease 对象添加进去
        }
    }

page->full()

首先我们来看一下,如何判断当前page是否是满状态的。

  • begin的地址为:Page自己的地址+Page对象的大小56个字节;
  • end的地址为:Page自己的地址+4096个字节;
  • empty:判断Page是否为空的条件是next地址是不是等于begin;
  • full:判断Page是否已满的条件是next地址是不是等于end(栈顶)。

我们知道next指向的是下一个AutoreleasePoolPage中下一个为空的内存地址,新对象会存在next,如果此时next指向end则代表当前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();
    }

page->add(obj)

当page没有存满时,会调用此方法,内部的原理非常简单,就是一个压栈的操作,并将next指针指向这个对象的下一个位置,然后将该对象的位置返回。

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

autoreleaseFullPage(obj, page)

如果当前 Page 存在但已满,会调用此方法。其内部实现的主要方法就是一个do..while循环,主要实现了一下的逻辑

  • 由于page是链表结构,所以通过循环查找page->child
  • 一级级判断是否page->full()
  • 如果到最后一个page都是满的,那么就新new一个AutoreleasePoolPage
  • 如果有不满的,或者新创建的,调用setHotPage(page)将当前页设置为活跃
  • 最后将对象通过page->add压栈
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);
    }

autoreleaseNoPage(obj)

当没有page时,会走到此方法,其主要逻辑如下:

  • 先会判断是否有空的自动释放池存在,如果没有会通过setEmptyPoolPlaceholder()生成一个占位符,表示一个空的自动释放池
  • 创建第一个Page,设置它为hotPage
  • 将一个POOL_BOUNDARY添加进Page中,并返回POOL_BOUNDARY的下一个位置。
  • 插入第一个对象
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", 
                         objc_thread_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);
    }

AutoreleasePoolPage::pop(ctxt)原理

看完对象入栈的实现,我们再来看一下出栈的实现。
首先pop的入参token即为POOL_BOUNDARY对应在Page中的地址。当销毁自动释放池时,会从从自动释放池的中的最后一个入栈的autorelease对象开始,依次给它们发送一条release消息,直到遇到这个POOL_BOUNDARY,具体的步骤如下:

  • 判断token是不是EMPTY_POOL_PLACEHOLDER,是的话就清空这个自动释放池
  • 如果不是的话,就通过pageForPointer(token)拿到token所在的Page
  • 通过page->releaseUntil(stop)将自动释放池中的autorelease对象全部释放,传参stop即为POOL_BOUNDARY的地址
  • 判断当前Page是否有子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 {
                // 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();
            }
        }
    }

pageForPointer(token)

该方法,主要是通过token来拿到当前所在的page。主要实现原理是将指针token与页面的大小(4096)取模,可以得到当前指针的偏移量。然后将指针的地址减偏移量便可以得到首地址。即该page的地址

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->releaseUntil(stop)

pop()方法中释放autorelease对象的过程在releaseUntil()方法中,下面来看一下这个方法的实现:

  • releaseUntil()方法其实就是通过一个while循环
  • 从hotPage开始,一直释放,直到stop,即传入的POOL_BOUNDARY
  • 最后设置释放完的当前page为hotPage
    void releaseUntil(id *stop) 
    {
        // Not recursive: we donot 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 canot prove it
            while (page->empty()) {
                page = page->parent;
                setHotPage(page);
            }

            page->unprotect();
            id obj = *--page->next;  // next指针是指向最后一个对象的后一个位置,所以需要先减1
            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
    }

page->kill()

kill方法删除双向链表中的每一个的page,找到当前page的 child 方向尾部 page,然后反向挨着释放并且把其parent节点的 child 指针置空。

void kill() 
{
    // Not recursive: we donot want to blow out the stack 
    // if a thread accumulates a stupendous amount of garbage
    AutoreleasePoolPage *page = this;
    // 找到链表最末尾的page
    while (page->child) page = page->child;

    AutoreleasePoolPage *deathptr;
    // 循环删除每一个page
    do {
        deathptr = page;
        page = page->parent;
        if (page) {
            page->unprotect();
            page->child = nil;
            page->protect();
        }
        delete deathptr;
    } while (deathptr != this);
}

Autoreleasepool嵌套探究

准备:

  • 由于ARC环境下不能调用autorelease等方法,所以需要将工程切换为MRC环境。
  • 使用 extern void _objc_autoreleasePoolPrint(void);方法来打印autoreleasePool的相关信息

单个page嵌套

int main(int argc, const char * argv[]) {
    _objc_autoreleasePoolPrint();             // print1
    @autoreleasepool { //r1 = push()
        _objc_autoreleasePoolPrint();         // print2
        NSObject *p1 = [[[NSObject alloc] init] autorelease];
        NSObject *p2 = [[[NSObject alloc] init] autorelease];
        _objc_autoreleasePoolPrint();         // print3
        @autoreleasepool { //r2 = push()
            NSObject *p3 = [[[NSObject alloc] init] autorelease];
            _objc_autoreleasePoolPrint();     // print4
            @autoreleasepool { //r3 = push()
                NSObject *p4 = [[[NSObject alloc] init] autorelease];
                _objc_autoreleasePoolPrint(); // print5
            } //pop(r3)
            _objc_autoreleasePoolPrint();     // print6
        } //pop(r2)
        _objc_autoreleasePoolPrint();         // print7
    } //pop(r1)
    _objc_autoreleasePoolPrint();             // print8
    return 0;
}

打印结果过如下,通过打印结果,我们可以印证上面原理的探索,其主要的进出栈流程如下图所示,且作用域只在@autoreleasepool {}之间,超过之后就全部调用pop释放


objc[12943]: ##############
objc[12943]: AUTORELEASE POOLS for thread 0x11aee5dc0
objc[12943]: 0 releases pending.
objc[12943]: ##############
objc[12943]: ##############
objc[12943]: AUTORELEASE POOLS for thread 0x11aee5dc0
objc[12943]: 0 releases pending.
objc[12943]: [0x1]  ................  PAGE (placeholder)
objc[12943]: [0x1]  ################  POOL (placeholder)
objc[12943]: ##############
objc[12943]: ##############
objc[12943]: AUTORELEASE POOLS for thread 0x11aee5dc0
objc[12943]: 3 releases pending.
objc[12943]: [0x7f924480b000]  ................  PAGE  (hot) (cold)
objc[12943]: [0x7f924480b038]  ################  POOL 0x7f924480b038
objc[12943]: [0x7f924480b040]    0x600001b34070  NSObject
objc[12943]: [0x7f924480b048]    0x600001b34080  NSObject
objc[12943]: ##############
objc[12943]: ##############
objc[12943]: AUTORELEASE POOLS for thread 0x11aee5dc0
objc[12943]: 5 releases pending.
objc[12943]: [0x7f924480b000]  ................  PAGE  (hot) (cold)
objc[12943]: [0x7f924480b038]  ################  POOL 0x7f924480b038
objc[12943]: [0x7f924480b040]    0x600001b34070  NSObject
objc[12943]: [0x7f924480b048]    0x600001b34080  NSObject
objc[12943]: [0x7f924480b050]  ################  POOL 0x7f924480b050
objc[12943]: [0x7f924480b058]    0x600001b34090  NSObject
objc[12943]: ##############
objc[12943]: ##############
objc[12943]: AUTORELEASE POOLS for thread 0x11aee5dc0
objc[12943]: 7 releases pending.
objc[12943]: [0x7f924480b000]  ................  PAGE  (hot) (cold)
objc[12943]: [0x7f924480b038]  ################  POOL 0x7f924480b038
objc[12943]: [0x7f924480b040]    0x600001b34070  NSObject
objc[12943]: [0x7f924480b048]    0x600001b34080  NSObject
objc[12943]: [0x7f924480b050]  ################  POOL 0x7f924480b050
objc[12943]: [0x7f924480b058]    0x600001b34090  NSObject
objc[12943]: [0x7f924480b060]  ################  POOL 0x7f924480b060
objc[12943]: [0x7f924480b068]    0x600001b2c030  NSObject
objc[12943]: ##############
objc[12943]: ##############
objc[12943]: AUTORELEASE POOLS for thread 0x11aee5dc0
objc[12943]: 5 releases pending.
objc[12943]: [0x7f924480b000]  ................  PAGE  (hot) (cold)
objc[12943]: [0x7f924480b038]  ################  POOL 0x7f924480b038
objc[12943]: [0x7f924480b040]    0x600001b34070  NSObject
objc[12943]: [0x7f924480b048]    0x600001b34080  NSObject
objc[12943]: [0x7f924480b050]  ################  POOL 0x7f924480b050
objc[12943]: [0x7f924480b058]    0x600001b34090  NSObject
objc[12943]: ##############
objc[12943]: ##############
objc[12943]: AUTORELEASE POOLS for thread 0x11aee5dc0
objc[12943]: 3 releases pending.
objc[12943]: [0x7f924480b000]  ................  PAGE  (hot) (cold)
objc[12943]: [0x7f924480b038]  ################  POOL 0x7f924480b038
objc[12943]: [0x7f924480b040]    0x600001b34070  NSObject
objc[12943]: [0x7f924480b048]    0x600001b34080  NSObject
objc[12943]: ##############
objc[12943]: ##############
objc[12943]: AUTORELEASE POOLS for thread 0x11aee5dc0
objc[12943]: 0 releases pending.
objc[12943]: [0x7f924480b000]  ................  PAGE  (hot) (cold)
objc[12943]: ##############

多个page嵌套

int main(int argc, const char * argv[]) {
    @autoreleasepool { //r1 = push()
        for (int i = 0; i < 600; i++) {
            NSObject *p = [[[NSObject alloc] init] autorelease];
        }
        @autoreleasepool { //r2 = push()
            for (int i = 0; i < 500; i++) {
                NSObject *p = [[[NSObject alloc] init] autorelease];
            }
            @autoreleasepool { //r3 = push()
                for (int i = 0; i < 200; i++) {
                    NSObject *p = [[[NSObject alloc] init] autorelease];
                }
                _objc_autoreleasePoolPrint();
            } //pop(r3)
        } //pop(r2)
    } //pop(r1)
    return 0;
}

可以看到打印结果如下:根据原理的探究,我们知道每个page除了第一页是504个对象外,其他最多存储505个对象,当一个page满了时候,会创建一个新的page,并且每个page之间是以栈为结点通过双向链表的形式组合而成。其主要流程如下图所示


objc[69731]: ##############
objc[69731]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[69731]: 1303 releases pending. //当前自动释放池中有1303个对象(3个POOL_BOUNDARY和1300个NSObject实例)
objc[69731]: [0x100806000]  ................  PAGE (full)  (cold) /* 第一个PAGE,full代表已满,cold代表coldPage */
objc[69731]: [0x100806038]  ################  POOL 0x100806038    //POOL_BOUNDARY
objc[69731]: [0x100806040]       0x10182a040  NSObject            //p1
objc[69731]: [0x100806048]       .....................            //...
objc[69731]: [0x100806ff8]       0x101824e40  NSObject            //p504
objc[69731]: [0x102806000]  ................  PAGE (full)         /* 第二个PAGE */
objc[69731]: [0x102806038]       0x101824e50  NSObject            //p505
objc[69731]: [0x102806040]       .....................            //...
objc[69731]: [0x102806330]       0x101825440  NSObject            //p600
objc[69731]: [0x102806338]  ################  POOL 0x102806338    //POOL_BOUNDARY
objc[69731]: [0x102806340]       0x101825450  NSObject            //p601
objc[69731]: [0x102806348]       .....................            //...
objc[69731]: [0x1028067e0]       0x101825d90  NSObject            //p1008
objc[69731]: [0x102804000]  ................  PAGE  (hot)         /* 第三个PAGE,hot代表hotPage */
objc[69731]: [0x102804038]       0x101826dd0  NSObject            //p1009
objc[69731]: [0x102804040]       .....................            //...
objc[69731]: [0x102804310]       0x101827380  NSObject            //p1100
objc[69731]: [0x102804318]  ################  POOL 0x102804318    //POOL_BOUNDARY
objc[69731]: [0x102804320]       0x101827390  NSObject            //p1101
objc[69731]: [0x102804328]       .....................            //...
objc[69731]: [0x102804958]       0x10182b160  NSObject            //p1300
objc[69731]: ##############

@autorelease与RunLoop

@autorelease与RunLoop关系

其中主要的的RunLoop运行流程如下图所示


而且通过打印[NSRunLoop currentRunLoop],可以发现其中有_wrapRunLoopWithAutoreleasePoolHandler()代表的相关AutoreleasePool的回调。

<CFRunLoopObserver 0x6000024246e0 [0x7fff8062ce20]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff48c1235c), context = <CFArray 0x600001b7afd0 [0x7fff8062ce20]>{type = mutable-small, count = 1, values = (0 : <0x7fc18f80e038>)}}
<CFRunLoopObserver 0x600002424640 [0x7fff8062ce20]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff48c1235c), context = <CFArray 0x600001b7afd0 [0x7fff8062ce20]>{type = mutable-small, count = 1, values = (0 : <0x7fc18f80e038>)}}

那么,RunLoop和AutoreleasePool的主要关系如下

  • kCFRunLoopEntry:在即将进入RunLoop时,会自动创建一个__AtAutoreleasePool结构体对象,并调用objc_autoreleasePoolPush()函数。
  • kCFRunLoopBeforeWaiting:在RunLoop即将休眠时,会自动销毁一个__AtAutoreleasePool对象,调用objc_autoreleasePoolPop()。然后创建一个新的__AtAutoreleasePool对象,并调用objc_autoreleasePoolPush()。
  • kCFRunLoopBeforeExit,在即将退出RunLoop时,会自动销毁最后一个创建的__AtAutoreleasePool对象,并调用objc_autoreleasePoolPop()。

main函数变化分析

了解了他们之间的关系,我们可以通过main函数,来分析一下

// Xcode 11
int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
--------------------------------------------------------------------------------
// Xcode 旧版本
int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

我们知道@autoreleasepool {}的作用域只在其大括号之间,而且UIApplicationMain主线程会创建主RunLoop,通过上面的探究,我们知道在创建RunLoop的时候,也会对应的创建AutoreleasePool。其中使用autorelease修饰的对象都会添加到RunLoop创建的自动释放池中。

所以Xcode 11和之前版本的区别,主要就是Xcode 11将@autoreleasepool {}提前,这可以保证@autoreleasepool中的autorelease对象在程序启动后立即释放。而之前的版本是在主线程RunLoop创建的自动释放池的外层的,意味着程序结束后main函数中的@autoreleasepool中的autorelease对象才会释放。

总结

  • Autoreleasepool目前通过@autoreleasepool{}来创建,可以再适当的时机对对象调用rekease,保证了对象的延迟释放
  • AutoreleasePoolPage底层结构
    AutoreleasePoolPage是以栈为结点通过双向链表的形式组合而成;遵循先进后出规则,整个自动释放池由一系列的AutoreleasePoolPage组成的,而AutoreleasePoolPage是以双向链表的形式连接起来。
    自动释放池与线程一一对应;
    调用objc_autoreleasePoolPush()来入栈,调用objc_autoreleasePoolPop()来出栈
    使用POOL_BOUNDARY哨兵对象来作为出入栈的标志位
  • 只是nil的别名。前世叫做POOL_SENTINEL,称为哨兵对象或者边界对象;
  • POOL_BOUNDARY用来区分不同的自动释放池,以解决自动释放池嵌套的问题
  • 每当创建一个自动释放池,就会调用push()方法将一个POOL_BOUNDARY入栈,并返回其存放的内存地址;
  • 当往自动释放池中添加autorelease对象时,将autorelease对象的内存地址入栈,它们前面至少有一个POOL_BOUNDARY;
  • 当销毁一个自动释放池时,会调用pop()方法并传入一个POOL_BOUNDARY,会从自动释放池中最后一个对象开始,依次给它们发送release消息,直到遇到这个POOL_BOUNDARY。
    每个AutoreleasePoolPage对象占用4096字节内存,其中56个字节用来存放它内部的成员变量,剩下的空间(4040个字节)用来存放autorelease对象的地址。要注意的是第一页只有504个对象,因为在创建page的时候会在next的位置插入1个POOL_BOUNDARY。
  • push()原理
  1. 调用了hotPage()获得新创建的未满的Page
  2. 当前 Page 存在且未满,走page->add(obj),将 autorelease 对象入栈,并将next指针指向这个对象的下一个位置,然后将该对象的位置返回
  3. 当前 Page 存在但已满,走autoreleaseFullPage,循环查找page->child并判断是否已满,都已满则创建新的AutoreleasePoolPage,并将 autorelease 对象入栈,设置HotPage
  4. 当没有page时,走autoreleaseNoPage,先会判断是否有空的自动释放池存在并生成占位符,然后创建一个新page并设置HotPage,依次插入POOL_BOUNDARY和autorelease 对象入栈
  • pop()原理
  1. 判断token是不是EMPTY_POOL_PLACEHOLDER,是的话就清空这个自动释放池
  2. 如果不是的话,就通过pageForPointer(token)拿到token所在的Page
  3. 通过page->releaseUntil(stop)将自动释放池中的autorelease对象全部释放,传参stop即为POOL_BOUNDARY的地址
  4. 判断当前Page是否有子Page,有的话就销毁
  • @autorelease与RunLoop

kCFRunLoopEntry:在即将进入RunLoop时,会自动创建一个__AtAutoreleasePool结构体对象,并调用objc_autoreleasePoolPush()函数。
kCFRunLoopBeforeWaiting:在RunLoop即将休眠时,会自动销毁一个__AtAutoreleasePool对象,调用objc_autoreleasePoolPop()。然后创建一个新的__AtAutoreleasePool对象,并调用objc_autoreleasePoolPush()。
kCFRunLoopBeforeExit,在即将退出RunLoop时,会自动销毁最后一个创建的__AtAutoreleasePool对象,并调用objc_autoreleasePoolPop()。

点击这里加入群聊【iOS学习交流】,里面整理各种面试问题,欢迎大家进来学习跟交流

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