「转载」iOS:autoreleasepool

介绍

自动释放池是Objective-C/Swift中的一种内存自动回收机制,AutoreleasePool可以将其中的变量进行release的时机延迟。简单来说,就是当创建一个对象,在正常情况下,变量会在超出其作用域的时立即release。如果将对象加入到了自动释放池中,这个对象并不会立即释放,会等到runloop休眠/超出@autoreleasepool作用域{}之后才会被释放。

官方文档
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对象,则手动创建自动释放池来管理这些对象可以很大程度地减少内存峰值。

创建一个自动释放池

在MRC下,可以使用NSAutoreleasePool或者@autoreleasepool。建议使用@autoreleasepool,苹果说它NSAutoreleasePool效率更高,并且ARC下只能用autoreleasepool。

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// Code benefitting from a local autorelease pool.
[pool release];

@autoreleasepool {
    // Code benefitting from a local autorelease pool.
}

@autoreleasepool blocks are more efficient than using an instance of NSAutoreleasePool directly; 
you can also use them even if you do not use ARC.

问:释放NSAutoreleasePool对象,使用[pool release]与[pool drain]的区别?

Objective-C 语言本身是支持 GC 机制的,但有平台局限性,仅限于 MacOS 开发中,iOS 开发用的是 RC 机制。在 iOS 的 RC 环境下[pool release]和[pool drain]效果一样,但在 GC 环境下drain会触发 GC 而release不做任何操作。使用[pool drain]更佳,一是它的功能对系统兼容性更强,二是这样可以跟普通对象的release区别开。(注意:苹果在引入ARC时称,已在 OS X Mountain Lion v10.8 中弃用GC机制,而使用ARC替代)

原理分析

通过macOS工程来分析@autoreleasepool的底层原理。 macOS工程中的main()函数什么都没做,只是放了一个@autoreleasepool。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
    }
    return NSApplicationMain(argc, argv);
}

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

__AtAutoreleasePool

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

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

@autoreleasepool底层是创建了一个__AtAutoreleasePool结构体对象;

在创建__AtAutoreleasePool结构体时会在构造函数中调用objc_autoreleasePoolPush()函数,并返回一个atautoreleasepoolobj。

在释放__AtAutoreleasePool结构体时会在析构函数中调用objc_autoreleasePoolPop()函数,并将atautoreleasepoolobj传入。

进入Runtime objc4源码查看以上提到的两个函数的实现

AutoreleasePoolPage

// 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类来实现的。

// NSObject.mm
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:
    // 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 ((AutoreleasePoolPage*)1)

#   define POOL_BOUNDARY nil
   ......
    AutoreleasePoolPage(AutoreleasePoolPage *newParent) :
        AutoreleasePoolPageData(begin(),//开始存储的位置
                                objc_thread_self(),//当前线程,通过tls获取
                                newParent,
                                newParent ? 1+newParent->depth : 0,//如果第一页深度为0,往后是前一个的深度+1
                                newParent ? newParent->hiwat : 0)
    {
      ......
    }

    ~AutoreleasePoolPage() 
    {
        ......
    }
    ......
}

// NSObject-internal.h
class AutoreleasePoolPage;
struct AutoreleasePoolPageData
{
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
    struct AutoreleasePoolEntry {
        uintptr_t ptr: 48;
        uintptr_t count: 16;

        static const uintptr_t maxCount = 65535; // 2^16 - 1
    };
    static_assert((AutoreleasePoolEntry){ .ptr = OBJC_VM_MAX_ADDRESS }.ptr == OBJC_VM_MAX_ADDRESS, "OBJC_VM_MAX_ADDRESS doesn't fit into AutoreleasePoolEntry::ptr!");
#endif

    magic_t const magic;
    __unsafe_unretained id *next;
    objc_thread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;

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

AutoreleasePoolPageData内的属性介绍:

magic_t const magic; // 校验AutoreleasePoolPage结构是否完整

__unsafe_unretained id *next; // 指向最新添加的进池的对象的下一个位置,初始时指向begin()

pthread_t const thread; // 指向当前线程

AutoreleasePoolPage * const parent; // 指向父节点,第一个节点的parent值为nil

AutoreleasePoolPage *child; // 指向子节点,最后一个节点的child值为nil

uint32_t const depth; // 代表深度,从0开始,往后递增1

uint32_t hiwat; // 最大入栈数量标记

整个程序运行过程中,可能会有多个AutoreleasePoolPage对象,再结合源码可知:

自动释放池(即所有的AutoreleasePoolPage对象)是以栈为结点通过双向链表的形式组合而成的一种数据结构;

自动释放池与线程一一对应;

每个AutoreleasePoolPage对象占用4096字节内存,其中56个字节用来存放它内部的成员变量,剩下的空间(4040个字节)用来存放autorelease对象的地址。

备注:

56个字节存储的是AutoreleasePoolPageData相关属性。

内存分布图如下:

继续通过源码分析push()、pop()以及autorelease方法的实现。

先来了解一下POOL_BOUNDARY

POOL_BOUNDARY称为哨兵对象或者边界对象;

POOL_BOUNDARY用来区分不同的自动释放池,以解决自动释放池嵌套的问题;

每当创建一个自动释放池,就会调用push()方法将一个POOL_BOUNDARY入栈,并返回其存放的内存地址;

当往自动释放池中添加autorelease对象时,将autorelease对象的内存地址入栈,它们前面至少有一个POOL_BOUNDARY;

当销毁一个自动释放池时,会调用pop()方法并传入一个POOL_BOUNDARY,会从自动释放池中最后一个对象开始,依次给它们发送release消息,直到遇到这个POOL_BOUNDARY。

push

static inline void *push() 
    {
        ReturnAutoreleaseInfo info = getReturnAutoreleaseInfo();
        moveTLSAutoreleaseToPool(info);

        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 == (id *)EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);

        // dtrace probe
        OBJC_RUNTIME_AUTORELEASE_POOL_PUSH(dest);

        return dest;
    }

判断是否有pool page(第一次来肯定是没有的):

如果没有,则通过autoreleaseNewPage创建;
如果有,则通过autoreleaseFast压栈哨兵对象。

autoreleaseFast()

   static inline id *autoreleaseFast(id obj)
    {
        AutoreleasePoolPage *page = hotPage();//获取当前page
        if (page && !page->full()) {//如果当前page存在且未满,则存入当前page
            return page->add(obj);
        } else if (page) {//如果当前page存在,但满了,则创建新的page,然后存入
            return autoreleaseFullPage(obj, page);
        } else {//如果当前page不存在,则创建第一个page,并存入
            return autoreleaseNoPage(obj);
        }
    }

autoreleaseFast主要是通过hotPage()获取当前页,判断当前page是否存在:

如果当前page存在且未满,则通过add将autorelease对象入栈;
如果当前page存在且满了,则通过autoreleaseFullPage创建新的页面并存入autorelease对象;
如果当前page不存在,则通过autoreleaseNoPage方法创建新第一个page,并存入autorelease对象。

add

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

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

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

        // dtrace probe
        OBJC_RUNTIME_AUTORELEASE_POOL_GROW(page->depth);

        return page->add(obj);
    }

autoreleaseFullPage()方法中通过do...while循环,通过Page的child指针找到最后一个Page。

如果最后一个Page未满,就通过page->add(obj)将autorelease对象添加到最后一个Page中;
如果最后一个Page已满,就创建一个新的Page并将该Page设置为hotPage,通过page->add(obj)将autorelease对象添加进去。

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

            if (DebugMissingPools == Fatal)
                _objc_fatal("Missing pools are a fatal error");

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

        // dtrace probe
        OBJC_RUNTIME_AUTORELEASE_POOL_GROW(page->depth);

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

autorelease方法的函数调用栈如下:

// NSObject.mm
1、 objc_autorelease
// objc-object.h 
2、 objc_object::autorelease
// NSObject.mm
3、autorelease
4、_objc_rootAutorelease
// objc-object.h
5、objc_object::rootAutorelease
// NSObject.mm
6、objc_object::rootAutorelease2
7、AutoreleasePoolPage::autorelease

AutoreleasePoolPage类的autorelease方法实现如下:

static inline id autorelease(id obj)
    {
        ASSERT(!_objc_isTaggedPointerOrNil(obj));
        id *dest __unused = autoreleaseFast(obj);
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
        ASSERT(!dest  ||  dest == (id *)EMPTY_POOL_PLACEHOLDER  ||  (id)((AutoreleasePoolEntry *)dest)->ptr == obj);
#else
        ASSERT(!dest  ||  dest == (id *)EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);
#endif
        return obj;
    }

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

pop

static inline void
    pop(void *token)//
    {
        ......
        // 通过token找到所处的page
        AutoreleasePoolPage *page;
        id *stop;
        if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
            // Popping the top-level placeholder pool.
            page = hotPage();
            if (!page) {
                // Pool was never used. Clear the placeholder.
                //pool从未被使用,清除占位符。
                return setHotPage(nil);
            }
            // Pool was used. Pop its contents normally.
            // Pool pages remain allocated for re-use as usual.
            page = coldPage();
            token = page->begin();
        } else {
            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 (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) {
            return popPageDebug(token, page, stop);
        }

        return popPage<false>(token, page, stop);
    }

拿到哨兵对象所在page后,开始popPage

template<bool allowDebug>
    static void
    popPage(void *token, AutoreleasePoolPage *page, id *stop)
    {
        if (allowDebug && PrintPoolHiwat) printHiwat();

        page->releaseUntil(stop);//释放对象

        // memory: delete empty children
        if (allowDebug && DebugPoolAllocation  &&  page->empty()) {
            // special case: delete everything during page-per-pool debugging
            AutoreleasePoolPage *parent = page->parent;
            page->kill();
            setHotPage(parent);
        } else if (allowDebug && 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()方法的执行过程如下:
5、判断token是不是EMPTY_POOL_PLACEHOLDER,是的话就清空这个自动释放池;

2、如果不是的话,就通过pageForPointer(token)拿到token所在的Page(自动释放池的首个Page);

3、然后popPage方法内通过调用page->releaseUntil(stop)将自动释放池中的autorelease对象全部释放,传参stop即为POOL_BOUNDARY的地址;

4、判断当前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
//从最后一个开始往前
        do {
            while (this->next != stop) {//
                // Restart from hotPage() every time, in case -release
                // autoreleased more objects
                AutoreleasePoolPage *page = hotPage();//获取当前page

                // fixme I think this `while` can be `if`, but I can't prove it
                while (page->empty()) {//如果当前page空了,则往父节点并更新当前page
                    page = page->parent;
                    setHotPage(page);
                }

                page->unprotect();
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
                AutoreleasePoolEntry* entry = (AutoreleasePoolEntry*) --page->next;

                // create an obj with the zeroed out top byte and release that
                id obj = (id)entry->ptr;
                int count = (int)entry->count;  // grab these before memset
#else
                id obj = *--page->next;
#endif
                memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
                page->protect();

                if (obj != POOL_BOUNDARY) {
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
                    // release count+1 times since it is count of the additional
                    // autoreleases beyond the first one
                    for (int i = 0; i < count + 1; i++) {
                        objc_release(obj);
                    }
#else
                    objc_release(obj);
#endif
                }
            }

            // Stale return autorelease info is conceptually autoreleased. If
            // there is any, release the object in the info. If stale info is
            // present, we have to loop in case it autoreleased more objects
            // when it was released.
        } while (releaseReturnAutoreleaseTLS());

        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()方法其实就是通过一个do...while循环,从最后一个入栈的autorelease对象开始,依次给它们发送一条release消息,直到遇到这个POOL_BOUNDARY。

AutoreleasePoolPage()创建page

AutoreleasePoolPage(AutoreleasePoolPage *newParent) :
        AutoreleasePoolPageData(begin(),//开始存储的位置
                                objc_thread_self(),//当前线程
                                newParent,
                                newParent ? 1+newParent->depth : 0,//如果是第一页深度为0,往后是前一个的深度+1
                                newParent ? newParent->hiwat : 0)
    {
        if (objc::PageCountWarning != -1) {
            checkTooMuchAutorelease();
        }

        if (parent) {
            parent->check();
            ASSERT(!parent->child);
            parent->unprotect();
            parent->child = this;
            parent->protect();
        }
        protect();
    }

AutoreleasePoolPage()方法的参数为newParent,新创建的Page的depth加1,next指针的初始位置指向begin,将新创建的Page的parent指针指向newParent。将newParent的child指针指向自己,这就形成了双向链表的结构。

begin、end、empty、full

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 。

查看自动释放池的情况

使用macOS工程示例分析

结合AutoreleasePoolPage的内存分布图以及_objc_autoreleasePoolPrint()私有函数,来帮助我们更好地理解@autoreleasepool的原理。

注意:

由于ARC环境下不能调用autorelease等方法,所以需要将工程切换为MRC环境。
如果使用ARC,则可以使用__autoreleasing所有权修饰符替代autorelease方法。

单个 @autoreleasepool

extern void _objc_autoreleasePoolPrint(void);
int main(int argc, const char * argv[]) {
    _objc_autoreleasePoolPrint();     // print1

    @autoreleasepool { 
        _objc_autoreleasePoolPrint(); // print2
        __autoreleasing HFPerson *p1 = [HFPerson new];
        __autoreleasing HFPerson *p2 = [HFPerson new];
        _objc_autoreleasePoolPrint();//print3
    
    }
    _objc_autoreleasePoolPrint();//print4

    return NSApplicationMain(argc, argv);
}

@interface HFPerson : NSObject
@end
//自动释放池的情况
objc[14787]: ############## (print1)
objc[14787]: AUTORELEASE POOLS for thread 0x116fbd600
objc[14787]: 0 releases pending. //当前自动释放池没有任何对象
objc[14787]: [0x7fd353009000]  ................  PAGE  (hot) (cold)
objc[14787]: ##############
objc[14787]: ############## (print2)
objc[14787]: AUTORELEASE POOLS for thread 0x116fbd600
objc[14787]: 1 releases pending. //当前自动释放池中有一个对象,这个对象为POOL_BOUNDARY
objc[14787]: [0x7fd353009000]  ................  PAGE  (hot) (cold)
objc[14787]: [0x7fd353009038]  ################  POOL 0x7fd353009038 //POOL_BOUNDARY
objc[14787]: ##############
objc[14787]: ############## (print3)
objc[14787]: AUTORELEASE POOLS for thread 0x116fbd600
objc[14787]: 3 releases pending.//当前自动释放池中有3个对象:POOL_BOUNDARY、p1、p2
objc[14787]: [0x7fd353009000]  ................  PAGE  (hot) (cold)
objc[14787]: [0x7fd353009038]  ################  POOL 0x7fd353009038 // POOL_BOUNDARY
objc[14787]: [0x7fd353009040]    0x6000020e8140  HFPerson //p1
objc[14787]: [0x7fd353009048]    0x6000020e8110  HFPerson// p2
objc[14787]: ##############
objc[14787]: ############## (print4)
objc[14787]: AUTORELEASE POOLS for thread 0x116fbd600
objc[14787]: 0 releases pending.//当前自动释放池中没有任何对象,因为@autoreleasepool作用域结束,调用pop方法释放了对象
objc[14787]: [0x7fd353009000]  ................  PAGE  (hot) (cold)
objc[14787]: ##############

嵌套 @autoreleasepool

extern void _objc_autoreleasePoolPrint(void);
int main(int argc, const char * argv[]) {
    _objc_autoreleasePoolPrint();     // print1

    @autoreleasepool { // r1 = push()
        _objc_autoreleasePoolPrint(); // print2
        __autoreleasing HFPerson *p1 = [HFPerson new];
        __autoreleasing HFPerson *p2 = [HFPerson new];
        _objc_autoreleasePoolPrint();//print3
        @autoreleasepool { // r2 = push
            __autoreleasing HFPerson *p3 = [HFPerson new];
            _objc_autoreleasePoolPrint();//print4
            @autoreleasepool { // r3 = push
                __autoreleasing HFPerson *p4 = [HFPerson new];
                _objc_autoreleasePoolPrint();//print5
            } // pop(r3)
            _objc_autoreleasePoolPrint();//print6
        }// pop(r2)
        _objc_autoreleasePoolPrint();//print7

    }//pop(r1)
    _objc_autoreleasePoolPrint();//print8

    return NSApplicationMain(argc, argv);
}
自动释放池情况
objc[15102]: ############## (print1)
objc[15102]: AUTORELEASE POOLS for thread 0x10d7a4600
objc[15102]: 0 releases pending. //当前自动释放池中没有任何对象
objc[15102]: [0x7fbe8a80a000]  ................  PAGE  (hot) (cold)
objc[15102]: ##############
objc[15102]: ############## (print2)
objc[15102]: AUTORELEASE POOLS for thread 0x10d7a4600
objc[15102]: 1 releases pending. //当前自动释放池中有1个对象,这个对象是POOL_BOUNDARY
objc[15102]: [0x7fbe8a80a000]  ................  PAGE  (hot) (cold)
objc[15102]: [0x7fbe8a80a038]  ################  POOL 0x7fbe8a80a038 //POOL_BOUNDARY
objc[15102]: ##############
objc[15102]: ############## (print3)
objc[15102]: AUTORELEASE POOLS for thread 0x10d7a4600
objc[15102]: 3 releases pending.//当前自动释放池中有3个对象(1个autoreleasepool)
objc[15102]: [0x7fbe8a80a000]  ................  PAGE  (hot) (cold)
objc[15102]: [0x7fbe8a80a038]  ################  POOL 0x7fbe8a80a038 // POOL_BOUNDARY
objc[15102]: [0x7fbe8a80a040]    0x6000010b81a0  HFPerson // p1
objc[15102]: [0x7fbe8a80a048]    0x6000010b81b0  HFPerson // p2
objc[15102]: ##############
objc[15102]: ############## (print4)
objc[15102]: AUTORELEASE POOLS for thread 0x10d7a4600
objc[15102]: 5 releases pending.//当前自动释放池中有5个对象(2个autoreleasepool)
objc[15102]: [0x7fbe8a80a000]  ................  PAGE  (hot) (cold)
objc[15102]: [0x7fbe8a80a038]  ################  POOL 0x7fbe8a80a038 // POOL_BOUNDARY
objc[15102]: [0x7fbe8a80a040]    0x6000010b81a0  HFPerson // p1
objc[15102]: [0x7fbe8a80a048]    0x6000010b81b0  HFPerson // p2
objc[15102]: [0x7fbe8a80a050]  ################  POOL 0x7fbe8a80a050 // POOL_BOUNDARY
objc[15102]: [0x7fbe8a80a058]    0x6000010b81c0  HFPerson // p3
objc[15102]: ##############
objc[15102]: ############## (print5)
objc[15102]: AUTORELEASE POOLS for thread 0x10d7a4600
objc[15102]: 7 releases pending.//当前自动释放池中有7个对象(3个autoreleasepool)
objc[15102]: [0x7fbe8a80a000]  ................  PAGE  (hot) (cold)
objc[15102]: [0x7fbe8a80a038]  ################  POOL 0x7fbe8a80a038 // POOL_BOUNDARY
objc[15102]: [0x7fbe8a80a040]    0x6000010b81a0  HFPerson // p1
objc[15102]: [0x7fbe8a80a048]    0x6000010b81b0  HFPerson // p2
objc[15102]: [0x7fbe8a80a050]  ################  POOL 0x7fbe8a80a050 // POOL_BOUNDARY
objc[15102]: [0x7fbe8a80a058]    0x6000010b81c0  HFPerson // p3
objc[15102]: [0x7fbe8a80a060]  ################  POOL 0x7fbe8a80a060 // POOL_BOUNDARY
objc[15102]: [0x7fbe8a80a068]    0x6000010a4010  HFPerson // p4
objc[15102]: ##############
objc[15102]: ############## (print6)
objc[15102]: AUTORELEASE POOLS for thread 0x10d7a4600
objc[15102]: 5 releases pending.//当前自动释放池中有5个对象(第3个@autoreleasepool已释放)
objc[15102]: [0x7fbe8a80a000]  ................  PAGE  (hot) (cold)
objc[15102]: [0x7fbe8a80a038]  ################  POOL 0x7fbe8a80a038
objc[15102]: [0x7fbe8a80a040]    0x6000010b81a0  HFPerson
objc[15102]: [0x7fbe8a80a048]    0x6000010b81b0  HFPerson
objc[15102]: [0x7fbe8a80a050]  ################  POOL 0x7fbe8a80a050
objc[15102]: [0x7fbe8a80a058]    0x6000010b81c0  HFPerson
objc[15102]: ##############
objc[15102]: ############## (print7)
objc[15102]: AUTORELEASE POOLS for thread 0x10d7a4600
objc[15102]: 3 releases pending.//当前自动释放池中有5个对象(第2、3个@autoreleasepool已释放)
objc[15102]: [0x7fbe8a80a000]  ................  PAGE  (hot) (cold)
objc[15102]: [0x7fbe8a80a038]  ################  POOL 0x7fbe8a80a038
objc[15102]: [0x7fbe8a80a040]    0x6000010b81a0  HFPerson
objc[15102]: [0x7fbe8a80a048]    0x6000010b81b0  HFPerson
objc[15102]: ##############
objc[15102]: ############## (print8)
objc[15102]: AUTORELEASE POOLS for thread 0x10d7a4600
objc[15102]: 0 releases pending.//当前自动释放池没有任何对象(3个@autoreleasepool都已释放)
objc[15102]: [0x7fbe8a80a000]  ................  PAGE  (hot) (cold)
objc[15102]: ##############

复杂情况 @autoreleasepool

由AutoreleasePoolPage类的定义可知,自动释放池(即所有的AutoreleasePoolPage对象)是以栈为结点通过双向链表的形式组合而成。

每当Page满了的时候,就会创建一个新的Page,并设置它为hotPage,而首个Page为coldPage。

接下来我们来看一下多个Page和多个@autoreleasepool嵌套的情况。

extern void _objc_autoreleasePoolPrint(void);
int main(int argc, const char * argv[]) {

    @autoreleasepool { // r1 = push()
        for (int i = 0; i < 600; i++) {
            __autoreleasing HFPerson *p = [HFPerson new];
        }
        @autoreleasepool { // r2 = push
            for (int i = 0; i < 500; i++) {
                __autoreleasing HFPerson *p = [HFPerson new];
            }
            @autoreleasepool { // r3 = push
                for (int i = 0; i < 200; i++) {
                    __autoreleasing HFPerson *p = [HFPerson new];
                }
                _objc_autoreleasePoolPrint();
            }// pop(r3)
        }// pop(r2)
    }//pop(r1)

    return NSApplicationMain(argc, argv);
}
自动释放池情况
objc[15261]: ##############
objc[15261]: AUTORELEASE POOLS for thread 0x10cdb5600
objc[15261]: 1303 releases pending.
objc[15261]: [0x7fda05012000]  ................  PAGE (full)  (cold)
objc[15261]: [0x7fda05012038]  ################  POOL 0x7fda05012038 //POOL_BOUNDARY
objc[15261]: [0x7fda05012040]    0x600002acc170  HFPerson //p1
objc[15261]: [0x7fda05012048]    .....................            //...
objc[15261]: [0x7fda05012ff8]    0x600002ace040  HFPerson // p504
objc[15261]: [0x7fda0500a000]  ................  PAGE (full)   //第二个page
objc[15261]: [0x7fda0500a038]    0x600002ace050  HFPerson // p505
objc[15261]: [0x7fda05012048]    .....................            //...
objc[15261]: [0x7fda0500a330]    0x600002ace640  HFPerson // p600
objc[15261]: [0x7fda0500a338]  ################  POOL 0x7fda0500a338 //POOL_BOUNDARY
objc[15261]: [0x7fda0500a340]    0x600002ace650  HFPerson // 601
objc[15261]: [0x7fda05012048]    .....................            //...
objc[15261]: [0x7fda0500aff8]    0x600002acffc0  HFPerson //p1008
objc[15261]: [0x7fda0500c000]  ................  PAGE  (hot)  //第三个page
objc[15261]: [0x7fda0500c038]    0x600002acffd0  HFPerson // p1009
objc[15261]: [0x7fda05012048]    .....................            //...
objc[15261]: [0x7fda0500c310]    0x600002adc580  HFPerson //p1100
objc[15261]: [0x7fda0500c318]  ################  POOL 0x7fda0500c318 //POOL_BOUNDARY
objc[15261]: [0x7fda0500c320]    0x600002adc590  HFPerson //p1101
objc[15261]: [0x7fda05012048]    .....................            //...
objc[15261]: [0x7fda0500c958]    0x600002add200  HFPerson // p1300

根据输出的自动释放池使用情况可知:

一个AutoreleasePoolPage对象的内存大小为4096个字节,它自身成员变量占用内存56个字节,所以剩下的4040个字节用来存储autorelease对象的内存地址。

又因为64bit下一个OC对象的指针所占内存为8个字节,所以一个Page可以存放505个对象的地址。

POOL_BOUNDARY也是一个对象,所以存放POOL_BOUNDARY后,此页可以放504个对象。

以上代码的自动释放池内存分布图如下所示:

从以上macOS工程示例可以得知:

在@autoreleasepool大括号结束的时候,就会调用Page的pop()方法,给@autoreleasepool中的autorelease对象发送release消息。

使用iOS工程示例分析

iOS工程中,方法里的autorelease对象是什么时候释放的呢?

有系统干预释放和手动干预释放两种情况。

系统干预释放是不指定@autoreleasepool,所有autorelease对象都由主线程的RunLoop创建的@autoreleasepool来管理。
手动干预释放就是将autorelease对象添加进我们手动创建的@autoreleasepool中。

系统干预释放

- (void)viewDidLoad {
    [super viewDidLoad];
    // MRC
    HFPerson *person = [[[HFPerson alloc] init] autorelease];
    //ARC
    //__autoreleasing HFPerson *p1 = [HFPerson new];
    NSLog(@"%s", __func__);
}

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

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

@implementation HFPerson
- (void)dealloc {
    NSLog(@"%s", __func__);

}
@end


打印输出

-[ViewController viewDidLoad]
-[ViewController viewWillAppear:]
-[HFPerson dealloc]
-[ViewController viewDidAppear:]

可以看到,调用了autorelease方法的person对象不是在viewDidLoad方法结束后释放,而是在viewWillAppear方法结束后释放,说明在viewWillAppear方法结束的时候,调用了pop()方法释放了person对象,这是由RunLoop控制的。

RunLoop和@autoreleasepool

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

第1个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush();

第2个Observer
1、 监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush();
2、监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()。

在iOS工程中系统干预释放的autorelease对象的释放时机是由RunLoop控制的,会在当前RunLoop每次循环结束时释放。

示例中person对象在viewWillAppear方法结束后释放,说明viewDidLoad和viewWillAppear方法在同一次循环里。

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

手动干预释放

- (void)viewDidLoad {
    [super viewDidLoad];
    @autoreleasepool {
       // MRC
       HFPerson *person = [[[HFPerson alloc] init] autorelease];
       //ARC
       //__autoreleasing HFPerson *p1 = [HFPerson new];
    }
    NSLog(@"%s", __func__);
}

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

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

打印输出

-[HFPerson dealloc]
-[ViewController viewDidLoad]
-[ViewController viewWillAppear:]
-[ViewController viewDidAppear:]

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

相关问题

1、ARC 环境下,autorelease 对象在什么时候释放?

系统干预释放:autorelease对象的释放时机是由RunLoop控制的,会在当前RunLoop每次循环结束时释放.
手动干预释放:添加到指定的@autoreleasepool中的autorelease对象,在@autoreleasepool大括号结束时就会释放,不受RunLoop控制。

2、ARC 环境下,需不需要手动添加 @autoreleasepool?

通常情况下不必自己创建@autoreleasepool,但是如果需要在循环中创建很多临时的autorelease对象时,需要手动添加@autoreleasepool。

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

1、如果编写的程序不是基于 UI 框架的,比如说命令行工具;
2、如果你编写的循环中创建了大量的临时对象;
可以在循环内使用@autoreleasepool在下一次迭代之前处理这些对象。在循环中使用@autoreleasepool有助于减少应用程序的最大内存占用。
3、 创建了辅助线程。
一旦线程开始执行,就必须创建自己的@autoreleasepool;否则,你的应用程序将存在内存泄漏。

3、如果对 NSAutoreleasePool 对象调用 autorelease 方法会发生什么情况?

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[pool autorelease];

//输出
Terminating app due to uncaught exception 'NSInvalidArgumentException', 
reason: '*** -[NSAutoreleasePool autorelease]: Cannot autorelease an autorelease pool' 

抛出异常NSInvalidArgumentException并导致程序Crash,异常原因:不能对NSAutoreleasePool对象调用autorelease。

4、AutoreleasePool的原理

自动释放池是以栈为结点通过双向链表的形式组合而成的一种数据结构。它的本质是一个AutoreleasePoolPage结构体对象。

自动释放池的进栈和出栈主要是通过结构体的构造函数和析构函数调用底层objc_autoreleasePoolPush和objc_autoreleasePoolPop,实际上是调用AutoreleasePoolPage的push和pop两个方法。

push操作就是获取到当前AutoreleasePoolPage,然后插入一个哨兵对象POOL_BOUNDARY,并返回哨兵对象POOL_BOUNDARY的内存地址。

关键操作是push内调用autoreleaseFast方法:
当page存在且不满时,调用add方法将对象添加至page的next指针处,并将next指针指向下一位。
当page存在,且已满时,调用autoreleaseFullPage初始化一个新的page,然后调用add方法将对象添加至这个page中。
若page不存在,调用autoreleaseNoPage,创建一个hotpage,然后调用add方法将对象添加至这个page中。

pop操作,需要一个参数,这个参数就是push操作返回的值,即哨兵对象POOL_BOUNDARY的内存地址token。pop根据token找到哨兵对象POOL_BOUNDARY所处的page,然后使用objc_release释放token之前的对象,并将next指到正确的位置。

5、一个自动释放池内有几个哨兵对象?

一个自动释放池内有一个哨兵对象POOL_BOUNDARY

6、AutoreleasePool能否嵌套使用?

可以嵌套使用,其目的是可以控制应用程序的内存峰值,使其不要太高;
可以嵌套的原因是因为自动释放池是以栈为节点,通过双向链表的形式连接的,且是和线程一一对应的;
自动释放池的多层嵌套其实就是不停的push哨兵对象,在pop时,会先释放里面的,再释放外面的。

7、哪些对象可以加入AutoreleasePool?alloc创建的对象可以吗?

使用alloc、new、copy、mutableCopy、allocWith...生成的对象和retain了的对象需要手动释放或arc自动管理,不会被添加到自动释放池中;

但是被__autoreleasing修饰/autorelease调用则会被添加到自动释放池中;

所有 autorelease的对象,在出了作用域之后,会被自动添加到最近创建的自动释放池中。

8、线程和@autoreleasepool的关系

官方文档中,提到:

Each thread (including the main thread) maintains its own stack of NSAutoreleasePool objects (see Threads). As new pools are created, they get added to the top of the stack. When pools are deallocated, they are removed from the stack. Autoreleased objects are placed into the top autorelease pool for the current thread. When a thread terminates, it automatically drains all of the autorelease pools associated with itself.

每个线程都有与之关联的自动释放池堆栈结构,新的pool在创建时会被压栈到栈顶,pool销毁时,会被出栈,对于当前线程来说,释放对象会被压栈到栈顶,线程停止时,会自动释放与之关联的自动释放池。

转载参考:
iOS - 聊聊 autorelease 和 @autoreleasepool
iOS 内存管理底层分析(二)- AutoreleasePool底层

其他相关链接
autoreasepool 官方文档

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

推荐阅读更多精彩内容