iOS - 聊聊 autorelease 和 @autoreleasepool

前言: 做为 iOS 开发者,在面试过程当中常常会碰到这样一个问题:在 ARC 环境下,autorelease 对象在何时释放?这也是 iOS 内存管理的重要知识点,本文将针对这道面试题,讲解 autorelease 和 @autoreleasepool。html

网络配图.png

苹果在 iOS5 中引入了ARC(Automatic Reference Counting)自动引用计数,经过LLVM编译器和Runtime协做来进行自动管理内存。LLVM编译器会在编译时在合适的地方为 OC 对象插入retainreleaseautorelease代码,省去了在MRC(Manual Reference Counting)手动引用计数下手动插入这些代码的工做,减轻了开发者的工做量。面试

MRC下,当咱们不须要一个对象的时候,要调用releaseautorelease方法来释放它。调用release会当即让对象的引用计数减1,若是此时对象的引用计数为0,就会当即释放该对象的内存空间。调用autorelease会将该对象添加进自动释放池中,它会在一个恰当的时刻自动给对象调用release,因此autorelease至关于延迟了对象的释放。网络

1. 自动释放池

官方文档数据结构

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.app

以上是苹果对自动释放池的一段介绍,其意思为:在事件循环(RunLoop)的每次循环开始时,在主线程建立一个自动释放池,并在每次循环结束时销毁,在销毁时释放自动释放池中的全部autorelease对象。一般状况下咱们不须要手动建立自动释放池,可是若是咱们在循环中建立了不少临时的autorelease对象,则手动建立自动释放池来管理这些对象能够很大程度地减小内存峰值。框架

事件循环图

建立一个自动释放池

  • MRC下使用NSAutoreleasePool
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// Code benefitting from a local autorelease pool.
[pool release];
复制代码
  • ARC下使用@autoreleasepool
@autoreleasepool {
    // Code benefitting from a local autorelease pool.
}
复制代码

2. 原理分析

下面咱们先经过macOS工程来分析@autoreleasepool的底层原理。 macOS工程中的main()函数什么都没作,只是放了一个@autoreleasepoolless

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

__AtAutoreleasePool

经过 Clang clang -rewrite-objc main.m 将以上代码转换为 C++ 代码。ide

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

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

能够看到:函数

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

AutoreleasePoolPage

下面咱们进入Runtime objc4源码查看以上提到的两个函数的实现。工具

备注: 本文使用的是objc4-756.2源码进行分析。

// NSObject.mm
void * objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}

void objc_autoreleasePoolPop(void *ctxt)
{
    AutoreleasePoolPage::pop(ctxt);
}
复制代码

能够得知,objc_autoreleasePoolPush()objc_autoreleasePoolPop()两个函数实际上是调用了AutoreleasePoolPage类的两个类方法push()pop()。因此@autoreleasepool底层就是使用AutoreleasePoolPage类来实现的。

下面咱们来看一下AutoreleasePoolPage类的定义:

class AutoreleasePoolPage 
{
# define EMPTY_POOL_PLACEHOLDER ((id*)1) // EMPTY_POOL_PLACEHOLDER:表示一个空自动释放池的占位符
# define POOL_BOUNDARY nil // POOL_BOUNDARY:哨兵对象
    static pthread_key_t const key = AUTORELEASE_POOL_KEY;
    static uint8_t const SCRIBBLE = 0xA3;   // 用来标记已释放的对象
    static size_t const SIZE =              // 每一个 Page 对象占用 4096 个字节内存
#if PROTECT_AUTORELEASEPOOL // PAGE_MAX_SIZE = 4096
        PAGE_MAX_SIZE;  // must be muliple of vm page size
#else
        PAGE_MAX_SIZE;  // size and alignment, power of 2
#endif
    static size_t const COUNT = SIZE / sizeof(id);  // Page 的个数

    magic_t const magic;                // 用来校验 Page 的结构是否完整
    id *next;                           // 指向下一个可存放 autorelease 对象地址的位置,初始化指向 begin()
    pthread_t const thread;             // 指向当前线程
    AutoreleasePoolPage * const parent; // 指向父结点,首结点的 parent 为 nil
    AutoreleasePoolPage *child;         // 指向子结点,尾结点的 child 为 nil
    uint32_t const depth;               // Page 的深度,从 0 开始递增
    uint32_t hiwat;
    ......
}
复制代码

整个程序运行过程当中,可能会有多个AutoreleasePoolPage对象。从它的定义能够得知:

  • 自动释放池(即全部的AutoreleasePoolPage对象)是以为结点经过双向链表的形式组合而成;
  • 自动释放池与线程一一对应;
  • 每一个AutoreleasePoolPage对象占用4096字节内存,其中56个字节用来存放它内部的成员变量,剩下的空间(4040个字节)用来存放autorelease对象的地址。

其内存分布图以下:

AutoreleasePoolPage 双向链表结构

下面咱们经过源码来分析push()pop()以及autorelease方法的实现。

push

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);  // 传入 POOL_BOUNDARY 哨兵对象
        }
        assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
        return dest;
    }
复制代码

当建立一个自动释放池时,会调用push()方法。push()方法中调用了autoreleaseFast()方法并传入了POOL_BOUNDARY哨兵对象。

这里对POOL_BOUNDARY作一下介绍:

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

下面咱们来看一下autoreleaseFast()方法的实现:

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 对象添加进去
        }
    }
复制代码

autoreleaseFast()中先是调用了hotPage()方法得到未满的Page,从AutoreleasePoolPage类的定义可知,每一个Page的内存大小为4096个字节,每当Page满了的时候,就会建立一个新的PagehotPage()方法就是用来得到这个新建立的未满的PageautoreleaseFast()在执行过程当中有三种状况:

  • ① 当前Page存在且未满时,经过page->add(obj)autorelease对象入栈,即添加到当前Page中;
  • ② 当前Page存在但已满时,经过autoreleaseFullPage(obj, page)建立一个新的Page,并将autorelease对象添加进去;
  • ③ 当前Page不存在,即还没建立过Page,经过autoreleaseNoPage(obj)建立第一个Page,并将autorelease对象添加进去。

下面咱们来看一下以上提到的三个方法的实现:

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

page->add(obj)其实就是将autorelease对象添加到Page中的next指针所指向的位置,并将next指针指向这个对象的下一个位置,而后将该对象的位置返回。

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);
    }
复制代码

autoreleaseFullPage()方法中经过while循环,经过Pagechild指针找到最后一个Page

  • 若是最后一个Page未满,就经过page->add(obj)autorelease对象添加到最后一个Page中;
  • 若是最后一个Page已满,就建立一个新的Page并经过page->add(obj)autorelease对象添加进去,并将该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);
    }
复制代码

autoreleaseNoPage()方法中会建立第一个Page。该方法会判断是否有空的自动释放池存在,若是没有会经过setEmptyPoolPlaceholder()生成一个占位符,表示一个空的自动释放池。接着建立第一个Page,设置它为hotPage。最后将一个POOL_BOUNDARY添加进Page中,并返回POOL_BOUNDARY的下一个位置。

小结: 以上就是push操做的实现,往自动释放池中添加一个POOL_BOUNDARY,并返回它存放的内存地址。接着每有一个对象调用autorelease方法,会将它的内存地址添加进自动释放池中。

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;
    }
复制代码

能够看到,调用了autorelease方法的对象,也是经过以上解析的autoreleaseFast()方法添加进Page中。

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

        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();
            }
        }
    }
复制代码

pop()方法的传参token即为POOL_BOUNDARY对应在Page中的地址。当销毁自动释放池时,会调用pop()方法将自动释放池中的autorelease对象所有释放(其实是从自动释放池的中的最后一个入栈的autorelease对象开始,依次给它们发送一条release消息,直到遇到这个POOL_BOUNDARY)。pop()方法的执行过程以下:

  • ① 判断token是否是EMPTY_POOL_PLACEHOLDER,是的话就清空这个自动释放池;
  • ② 若是不是的话,就经过pageForPointer(token)拿到token所在的Page(自动释放池的首个Page);
  • ③ 经过page->releaseUntil(stop)将自动释放池中的autorelease对象所有释放,传参stop即为POOL_BOUNDARY的地址;
  • ④ 判断当前Page是否有子Page,有的话就销毁。

pop()方法中释放autorelease对象的过程在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);
            }

            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
    }
复制代码

releaseUntil()方法其实就是经过一个while循环,从最后一个入栈的autorelease对象开始,依次给它们发送一条release消息,直到遇到这个POOL_BOUNDARY

AutoreleasePoolPage()

咱们来看一下建立一个Page的过程。AutoreleasePoolPage()方法的参数为parentPage,新建立的Pagedepth加一,next指针的初始位置指向begin,将新建立的Pageparent指针指向parentPage。将parentPagechild指针指向本身,这就造成了双向链表的结构。

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();
    }
复制代码

begin、end、empty、full

下面再来看一下beginendemptyfull这些方法的实现。

  • begin的地址为:Page本身的地址+Page对象的大小56个字节;
  • end的地址为:Page本身的地址+4096个字节;
  • empty判断Page是否为空的条件是next地址是否是等于begin
  • full判断Page是否已满的条件是next地址是否是等于end(栈顶)。
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();
    }
复制代码

小结:

  • push操做是往自动释放池中添加一个POOL_BOUNDARY,并返回它存放的内存地址;
  • 接着每有一个对象调用autorelease方法,会将它的内存地址添加进自动释放池中。
  • pop操做是传入一个POOL_BOUNDARY的内存地址,从最后一个入栈的autorelease对象开始,将自动释放池中的autorelease对象所有释放(其实是给它们发送一条release消息),直到遇到这个POOL_BOUNDARY

3. 查看自动释放池的状况

能够经过如下私有函数来查看自动释放池的状况:

extern void _objc_autoreleasePoolPrint(void);
复制代码

4. macOS 工程示例分析

接下来咱们经过macOS工程代码示例,结合AutoreleasePoolPage的内存分布图以及_objc_autoreleasePoolPrint()私有函数,来帮助咱们更好地理解@autoreleasepool的原理。

注意: 因为ARC环境下不能调用autorelease等方法,因此须要将工程切换为MRC环境。

单个 @autoreleasepool

int main(int argc, const char * argv[]) {
    _objc_autoreleasePoolPrint();     // print1
    @autoreleasepool {
        _objc_autoreleasePoolPrint(); // print2
        HTPerson *p1 = [[[HTPerson alloc] init] autorelease];
        HTPerson *p2 = [[[HTPerson alloc] init] autorelease];
        _objc_autoreleasePoolPrint(); // print3
    }
    _objc_autoreleasePoolPrint();     // print4
    return 0;
}
复制代码

内存分布图

// 自动释放池的状况
objc[68122]: ############## (print1)
objc[68122]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68122]: 0 releases pending. //当前自动释放池中没有任何对象
objc[68122]: [0x102802000]  ................  PAGE  (hot) (cold)
objc[68122]: ##############

objc[68122]: ############## (print2)
objc[68122]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68122]: 1 releases pending. //当前自动释放池中有1个对象,这个对象为POOL_BOUNDARY
objc[68122]: [0x102802000]  ................  PAGE  (hot) (cold)
objc[68122]: [0x102802038]  ################ POOL 0x102802038 //POOL_BOUNDARY
objc[68122]: ##############

objc[68122]: ############## (print3)
objc[68122]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68122]: 3 releases pending. //当前自动释放池中有3个对象
objc[68122]: [0x102802000]  ................  PAGE  (hot) (cold)
objc[68122]: [0x102802038]  ################ POOL 0x102802038 //POOL_BOUNDARY
objc[68122]: [0x102802040]       0x100704a10  HTPerson          //p1
objc[68122]: [0x102802048]       0x10075cc30  HTPerson          //p2
objc[68122]: ##############

objc[68156]: ############## (print4)
objc[68156]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68156]: 0 releases pending. //当前自动释放池中没有任何对象,由于@autoreleasepool做用域结束,调用pop方法释放了对象
objc[68156]: [0x100810000]  ................  PAGE  (hot) (cold)
objc[68156]: ##############
复制代码

嵌套 @autoreleasepool

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

内存分布图

// 自动释放池的状况
objc[68285]: ############## (print1)
objc[68285]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68285]: 0 releases pending. //当前自动释放池中没有任何对象
objc[68285]: [0x102802000]  ................  PAGE  (hot) (cold)
objc[68285]: ##############

objc[68285]: ############## (print2)
objc[68285]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68285]: 1 releases pending. //当前自动释放池中有1个对象
objc[68285]: [0x102802000]  ................  PAGE  (hot) (cold)
objc[68285]: [0x102802038]  ################ POOL 0x102802038 //POOL_BOUNDARY
objc[68285]: ##############

objc[68285]: ############## (print3)
objc[68285]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68285]: 3 releases pending. //当前自动释放池中有3个对象(1个@autoreleasepool)
objc[68285]: [0x102802000]  ................  PAGE  (hot) (cold)
objc[68285]: [0x102802038]  ################ POOL 0x102802038 //POOL_BOUNDARY
objc[68285]: [0x102802040]       0x100707d80  HTPerson          //p1
objc[68285]: [0x102802048]       0x100707de0  HTPerson          //p2
objc[68285]: ##############

objc[68285]: ############## (print4)
objc[68285]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68285]: 5 releases pending. //当前自动释放池中有5个对象(2个@autoreleasepool)
objc[68285]: [0x102802000]  ................  PAGE  (hot) (cold)
objc[68285]: [0x102802038]  ################ POOL 0x102802038 //POOL_BOUNDARY
objc[68285]: [0x102802040]       0x100707d80  HTPerson          //p1
objc[68285]: [0x102802048]       0x100707de0  HTPerson          //p2
objc[68285]: [0x102802050]  ################ POOL 0x102802050 //POOL_BOUNDARY
objc[68285]: [0x102802058]       0x1005065b0  HTPerson          //p3
objc[68285]: ##############

objc[68285]: ############## (print5)
objc[68285]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68285]: 7 releases pending. //当前自动释放池中有7个对象(3个@autoreleasepool)
objc[68285]: [0x102802000]  ................  PAGE  (hot) (cold)
objc[68285]: [0x102802038]  ################ POOL 0x102802038 //POOL_BOUNDARY
objc[68285]: [0x102802040]       0x100707d80  HTPerson          //p1
objc[68285]: [0x102802048]       0x100707de0  HTPerson          //p2
objc[68285]: [0x102802050]  ################ POOL 0x102802050 //POOL_BOUNDARY
objc[68285]: [0x102802058]       0x1005065b0  HTPerson          //p3
objc[68285]: [0x102802060]  ################ POOL 0x102802060 //POOL_BOUNDARY
objc[68285]: [0x102802068]       0x100551880  HTPerson          //p4
objc[68285]: ##############

objc[68285]: ############## (print6)
objc[68285]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68285]: 5 releases pending. //当前自动释放池中有5个对象(第3个@autoreleasepool已释放)
objc[68285]: [0x102802000]  ................  PAGE  (hot) (cold)
objc[68285]: [0x102802038]  ################ POOL 0x102802038
objc[68285]: [0x102802040]       0x100707d80  HTPerson
objc[68285]: [0x102802048]       0x100707de0  HTPerson
objc[68285]: [0x102802050]  ################ POOL 0x102802050
objc[68285]: [0x102802058]       0x1005065b0  HTPerson
objc[68285]: ##############

objc[68285]: ############## (print7)
objc[68285]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68285]: 3 releases pending. //当前自动释放池中有3个对象(第二、3个@autoreleasepool已释放)
objc[68285]: [0x102802000]  ................  PAGE  (hot) (cold)
objc[68285]: [0x102802038]  ################ POOL 0x102802038
objc[68285]: [0x102802040]       0x100707d80  HTPerson
objc[68285]: [0x102802048]       0x100707de0  HTPerson
objc[68285]: ##############

objc[68285]: ############## (print8)
objc[68285]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68285]: 0 releases pending. //当前自动释放池没有任何对象(3个@autoreleasepool都已释放)
objc[68285]: [0x102802000]  ................  PAGE  (hot) (cold)
objc[68285]: ##############
复制代码

复杂状况 @autoreleasepool

AutoreleasePoolPage类的定义可知,自动释放池(即全部的AutoreleasePoolPage对象)是以为结点经过双向链表的形式组合而成。每当Page满了的时候,就会建立一个新的Page,并设置它为hotPage,而首个PagecoldPage。接下来咱们来看一下多个Page和多个@autoreleasepool嵌套的状况。

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

一个AutoreleasePoolPage对象的内存大小为4096个字节,它自身成员变量占用内存56个字节,因此剩下的4040个字节用来存储autorelease对象的内存地址。又由于64bit下一个OC对象的指针所占内存为8个字节,因此一个Page能够存放505个对象的地址。POOL_BOUNDARY也是一个对象,由于它的值为nil。因此以上代码的自动释放池内存分布图以下所示。

内存分布图

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

5. iOS 工程示例分析

从以上macOS工程示例能够得知,在@autoreleasepool大括号结束的时候,就会调用Pagepop()方法,给@autoreleasepool中的autorelease对象发送release消息。

那么在iOS工程中,方法里的autorelease对象是何时释放的呢?有系统干预释放和手动干预释放两种状况。系统干预释放是不指定@autoreleasepool,全部autorelease对象都由main函数中的@autoreleasepool管理。手动干预释放就是将autorelease对象添加进咱们手动建立的@autoreleasepool

下面仍是在MRC环境下进行分析。

系统干预释放

iOS工程的main()函数中也有一个@autoreleasepool,这个@autoreleasepool负责了应用程序全部autorelease对象的释放。

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}
复制代码
- (void)viewDidLoad {
    [super viewDidLoad];    
    HTPerson *person = [[[HTPerson alloc] init] autorelease];    
    NSLog(@"%s", __func__);
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];    
    NSLog(@"%s", __func__);
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];    
    NSLog(@"%s", __func__);
}

// -[ViewController viewDidLoad]
// -[ViewController viewWillAppear:]
// -[HTPerson dealloc]
// -[ViewController viewDidAppear:]
复制代码

能够看到,调用了autorelease方法的person对象不是在viewDidLoad方法结束后释放,而是在viewWillAppear方法结束后释放,说明在viewWillAppear方法结束的时候,调用了pop()方法释放了person对象。其实这是由RunLoop控制的,下面来说解一下RunLoop@autoreleasepool的关系。

RunLoop 与 @autoreleasepool

学习这个知识点以前,须要先搞懂RunLoop的事件循环机制以及它的6种活动状态,能够查看个人文章:
《深刻浅出 RunLoop(二):数据结构》
《深刻浅出 RunLoop(三):事件循环机制》

iOS在主线程的RunLoop中注册了两个Observer

  • 第1个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush()
  • 第2个Observer
    ① 监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()objc_autoreleasePoolPush()
    ② 监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()

image.png

因此,在iOS工程中系统干预释放的autorelease对象的释放时机是由RunLoop控制的,会在当前RunLoop每次循环结束时释放。以上person对象在viewWillAppear方法结束后释放,说明viewDidLoadviewWillAppear方法在同一次循环里。

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

手动干预释放

咱们再来看一下手动干预释放的状况。

- (void)viewDidLoad {
    [super viewDidLoad];    
    @autoreleasepool {
        HTPerson *person = [[[HTPerson alloc] init] autorelease];  
    }  
    NSLog(@"%s", __func__);
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];    
    NSLog(@"%s", __func__);
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];    
    NSLog(@"%s", __func__);
}

// -[HTPerson dealloc]
// -[ViewController viewDidLoad]
// -[ViewController viewWillAppear:]
// -[ViewController viewDidAppear:]
复制代码

能够看到,添加进手动指定的@autoreleasepool中的autorelease对象,在@autoreleasepool大括号结束时就会释放,不受RunLoop控制。

Q:ARC 环境下方法里的局部对象何时释放?

以上都是在MRC环境下分析,由于ARC下不能给对象调用retain releaseautorelease等方法。

ARC中方法里的局部对象何时释放?其实只要知道LLVM编译器在编译时给对象插入release仍是autorelease方法就知道了。

  • 经过alloc/new/copy/mutableCopy方法建立的对象,LLVM编译器在编译时会给对象插入release方法,因此这类局部对象,在方法结束时就会释放。
  • 经过其余方法(如类方法)建立的对象LLVM编译器在编译时会给对象插入autorelease方法,因此这类对象的释放时机由RunLoop控制。

Q:ARC 环境下,autorelease 对象在何时释放?

回到咱们最初的面试题,在ARC环境下,autorelease对象在何时释放?咱们就分系统干预释放手动干预释放两种状况回答。

Q:ARC 环境下需不须要手动添加 @autoreleasepool?

iOS工程在ARC环境下,main函数中的@autoreleasepool负责了应用程序全部autorelease对象的释放。一般状况下咱们不须要手动添加@autoreleasepool,可是若是咱们须要在循环中建立了不少临时的autorelease对象,则手动添加@autoreleasepool来管理这些对象能够很大程度地减小内存峰值。好比在for循环中alloc图片数据等内存消耗较大的场景,须要手动添加@autoreleasepool

苹果给出了三种须要手动添加@autoreleasepool的状况:

  • ① 若是你编写的程序不是基于 UI 框架的,好比说命令行工具;
  • ② 若是你编写的循环中建立了大量的临时对象;
    你能够在循环内使用@autoreleasepool在下一次迭代以前处理这些对象。在循环中使用@autoreleasepool有助于减小应用程序的最大内存占用。
  • ③ 若是你建立了辅助线程。
    一旦线程开始执行,就必须建立本身的@autoreleasepool;不然,你的应用程序将存在内存泄漏。(有关详细信息,请参见自动释放池块和线程。)

更多关于@autoreleasepool的使用能够查看苹果官方文档《Advanced Memory Management Programming Guide》