iOS底层学习 - 内存管理之Autoreleasepool

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

传送门☞iOS底层学习 - 内存管理以内存管理方案app

传送门☞iOS底层学习 - 内存管理之weak原理探究框架

初探Autoreleasepool

Autoreleasepool做用

经过以前章节的学习,咱们知道在ARC下,LLVM编译器会自动帮咱们生产retainreleaseautorelease等代码,减小了在MRC下的工做量。调用autorelease会将该对象添加进自动释放池中,它会在一个恰当的时刻自动给对象调用release,因此autorelease至关于延迟了对象的释放。less

可是在ARC下,autorelease方法已被禁用,咱们可使用__autoreleasing修饰符修饰对象将对象注册到自动释放池中。ide

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.函数

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

Autoreleasepool原理探究

Autoreleasepool底层结构

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

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

咱们仍是使用clang -rewrite-objc main.m命令,转换为C++代码查看。经过如下代码,咱们能够发现转换后@autoreleasepool主要作了如下几点:post

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

/***********************************************************************

Autorelease pool implementation

A thread's autorelease pool is a stack of pointers.

Each pointer is either an object to release, or POOL_BOUNDARY which is an autorelease pool boundary.

A pool token is a pointer to the POOL_BOUNDARY for that pool. When the pool is popped, every object hotter than the sentinel is released.

The stack is divided into a doubly-linked list of pages. Pages are added and deleted as necessary.

Thread-local storage points to the hot page, where newly autoreleased objects are stored.

**********************************************************************/

翻译以下

一个线程的自动释放池是一个指针的堆栈结构。

每一个指针表明一个须要释放的对象或者POOL_BOUNDARY(自动释放池边界)

一个 pool token 就是这个 pool 所对应的 POOL_BOUNDARY 的内存地址。当这个 pool 被 pop 的时候,全部内存地址在 pool token 以后的对象都会被 release。 这个堆栈被划分红了一个以 page 为结点的双向链表。pages 会在必要的时候动态地增长或删除。

Thread-local storage(线程局部存储)指向 hot page ,即最新添加的 autoreleased 对象所在的那个 page 。

经过上面对成员变量的解析和上方官方的注释,咱们能够知道AutoreleasePoolPage底层结构以下:

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

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

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

AutoreleasePoolPage::push()原理

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

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,找到当前pagechild 方向尾部 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的相关知识,能够查看文章☞iOS底层学习 - 深刻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{}的,可是如下几种状况可使用

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

使用__autorelease修饰的对象,会被系统自动加入RunLoop建立的自动释放池中,随RunLoop生命周期释放。

总结

  • Autoreleasepool目前经过@autoreleasepool{}来建立,能够再适当的时机对对象调用release,保证了对象的延迟释放
  • 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 - 聊聊 autorelease 和 @autoreleasepool

iOS内存管理二:自动释放池autoreleasepool

iOS 底层拾遗:AutoreleasePool

相关文章
相关标签/搜索