iOS--autoreleasepool

@autoreleasepool 编译(clang)后会被转换成:

__AtAutoreleasePool __autoreleasepool;

__AtAutoreleasePool是一个结构体,定义如下:

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

__AtAutoreleasePool中包含一个构造函数及一个析构函数:构造函数调用 objc_autoreleasePoolPush() 并返回一个 atautoreleasepoolobj 对象;而析构函数将atautoreleasepoolobj 对象作为 objc_autoreleasePoolPop() 的入参。

@autoreleasepool{}实际是下面两行的替换:

void * atautoreleasepoolobj = objc_autoreleasePoolPush(); 
         // do something
objc_autoreleasePoolPop(atautoreleasepoolobj);

先看objc_autoreleasePoolPush()和objc_autoreleasePoolPop()的实现,在runtime源码的NSObject.mm文件中:

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

void
objc_autoreleasePoolPop(void *ctxt)
{
    AutoreleasePoolPage::pop(ctxt);
}

objc_autoreleasePoolPush()和objc_autoreleasePoolPop()实际上分别调用的AutoreleasePoolPage的push()和pop(ctxt) 函数。

自动释放池的主要底层数据结构是 __AtAutoreleasePool 和 AutoreleasePoolPage

AutoreleasePoolPage

image

在 AutoreleasePoolPage 定义的上方有一大段注释自动释放池实现:

线程的自动释放池是指针的栈,即自动释放池是栈结构。

每个指针要么是要释放的对象,要么是自动释放池边界POOL_BOUNDARY(哨兵对象,表示一个 autorelease pool 的边界)。

释放池 token 是一个指向这个释放池 POOL_BOUNDARY的指针。当释放池被 pop 的时候,在这个哨兵对象后面添加的那些hotter对象都会被 release。

这个栈(即 autorelease pool)是一个以 page 为结点的双向链表,这些 page 根据需要来添加或者删除。

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

这里需要注意的是,栈上只存指针(就是对象的地址),对象本身是存在堆上的。

AutoreleasePoolPage定义

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

AutoreleasePoolPage 继承自 AutoreleasePoolPageData。

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

这些参数的意思如下:

magic_t const magic:16bytes,用来校验AutoreleasePoolPage结构的完整性

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

pthread_t const thread:8bytes,指向当前线程,保存了当前页所在的线程(一个AutoreleasePoolPage属于一个线程,一个线程中可以有多个AutoreleasePoolPage)。

AutoreleasePoolPage * const parent:8bytes,指向父page,第一个page的parent为nil

AutoreleasePoolPage child:8bytes,*指向子page,最后一个page的child为nil

uint32_t const depth:4bytes,表示深度,从0开始

uint32_t hiwat: 4bytes,代表 high water mark,表示最大入栈数量标记

COUNT :page里的对象个数

EMPTY_POOL_PLACEHOLDER :空池占位。

POOL_BOUNDARY :哨兵对象或边界对象, nil别名,用来区别每个 AutoreleasePool的边界,用来区分不同的自动释放池,以解决自动释放池嵌套的问题。

PAGE_MAX_SIZE :定义的大小(在下面定义中可以看到是4096bytes)

SIZE:AutoreleasePoolPage的大小

define BYTE_SIZE 8 /* byte size in bits */

#define I386_PGBYTES 4096 / bytes per 80386 page /

#define PAGE_SIZE I386_PGBYTES

#define PAGE_MAX_SIZE PAGE_SIZE

define PAGE_MIN_SIZE PAGE_SIZE

一个 AutoreleasePoolPage 会开辟 PAGE_MAX_SIZE 的内存(4096 bytes,可能会根据不同设备及系统分配不同的内存),除了 AutoreleasePoolPage 的成员变量所占空间(共 56 bytes),其余空间将会用来存储加入到自动释放池的对象,一个AutoreleasePoolPage能存放505个对象(即(4096-56)/8 = 505) 。

一个 AutoreleasePoolPage 初始状态结构如下:

image

begin() 和 end() 这两个函数帮助快速获取AutoreleasePoolPage存储对象地址的起至位置。

next 指向了最新下一个为空的内存地址,当 next == end() 时,表示当前 page 已经满了。

初始化一个AutoreleasePool后:

image

初始化一个AutoreleasePool会在AutoreleasePoolPage添加一个POOL_BOUNDARY,并将这个POOL_BOUNDARY返回,来确保在 pop 调用的时候,不会出现异常。POOL_BOUNDARY会存放在当前 next 指向的位置,当对象存放完成后,next 指针会指向下一个为空的地址。

向AutoreleasePool添加一个对象

image

next 原来指向的地址存入一个 obj(id*,指向autorelease对象的指针),而next 指针则指向下一个为空的地址。

下面去源码里验证一下

objc_autoreleasePoolPush 函数

首先自动释放池初始化,返回一个atautoreleasepoolobj哨兵对象:

** void * atautoreleasepoolobj = objc_autoreleasePoolPush();**

这里objc_autoreleasePoolPush()实现上面已经写出来了,里面返回的是AutoreleasePoolPage的push()函数:

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

push()函数通过调用 autoreleaseFast()函数(这里入参POOL_BOUNDARY哨兵对象)来执行具体的插入操作:

    static inline id *autoreleaseFast(id obj)
    {
        AutoreleasePoolPage *page = hotPage();
        if (page && !page->full()) {
            return page->add(obj);
        } else if (page) {
            return autoreleaseFullPage(obj, page);
        } else {
            return autoreleaseNoPage(obj);
        }
    }

hotPage()是正在使用的AutoreleasePoolPage。

autoreleaseFast() 函数里面分为三种情况:

【1】当前 hotPage存在且没有满时:

调用 page->add(obj) 函数将对象加入该 hotPage 中,即 next 指向的位置;

【2】当前hotpage 存在但是已满时:

调用 autoreleaseFullPage(obj, page) 函数,该函数会先查找 hotPage 的 child,如果有则将 child page 设置为 hotPage,如果没有则将创建一个新的 hotPage,之后在这个新的 hotPage 上执行 **page->add(obj) **操作;

【3】当前 page不存在时:

调用** autoreleaseNoPage(obj) 函数,该函数会创建一个 hotPage,然后执行 page->add(obj) **操作。

接下来看看 add() 函数的定义:

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

add() 函数会把传入的对象obj 存放在原本 next 所在的位置, 而next 指针移到下一个空位置。

再看看autoreleaseFullPage(obj, page),(当前 hotPage 已满):

   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的实现可以看出:

【1】当前hotPage 已满;

【2】从传入的 page 开始遍历整个双向链表,直到查找到一个未满的AutoreleasePoolPage;如果没有找到未满的AutoreleasePoolPage,则创建一个新的AutoreleasePoolPage。最后得到新的page;

【3】将上一步中的page设置为hotpage;

【4】调用add(obj),将obj对象添加的page中。

最后我们再来看看autoreleaseNoPage(obj),(没有 hotPage):

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

        if (obj == POOL_BOUNDARY) {
            // 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();
        }
        // 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);
    }

这里对函数实现进行了删减只留下正常流程:

【1】当前没有可用的AutoreleasePoolPage;

【2】创建一个AutoreleasePoolPage并设置为 hotPage;

【3】条件性给这个hotPage添加一个POOL_BOUNDARY哨兵对象(这里条件不影响流程认知,不做说明);

【4】然后执行** page->add(obj) **操作。

不存在 AutoreleasePoolPage也就意味着没有可用的自动释放池,就要从头开始构建这个自动释放池的双向链表,也就是说,新的 AutoreleasePoolPage 是没有 parent 指针的。

最后,看一下autorelease(id obj)实现,同样也调用了 autoreleaseFast(obj) 函数:

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

AutoreleasePoolPage 的autorelease 函数的和 push 操作的实现非常相似。

只不过push操作插入的是一个POOL_BOUNDARY ,而 autorelease操作插入的是一个具体的autoreleased 对象,向一个对象发送 autorelease 消息,就会把该对象 add 进 page 里。

objc_autoreleasePoolPop(void *ctxt)

在调用时 (atautoreleasepoolobj是要释放的释放池的哨兵对象)

objc_autoreleasePoolPop(atautoreleasepoolobj);

objc_autoreleasePoolPop的实现上面已经列出,实际调用AutoreleasePoolPage的pop()函数:

static inline void
    pop(void *token)
    {
        AutoreleasePoolPage *page;
        id *stop;
        if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
             //token对用的自动释放池是一个空的占位池
         }else{
             **page =pageForPointer(token);** 
         } 
         stop = (id*)token;
        return popPage(token, page, stop);
    }

page =pageForPointer(token); 和 return popPage(token, page, stop);

static AutoreleasePoolPage *pageForPointer(uintptr_tp)

{

    AutoreleasePoolPage *result;

    uintptr_toffset = p %SIZE;

    ASSERT(offset >=sizeof(AutoreleasePoolPage));

    result = (AutoreleasePoolPage*)(p - offset);

    result->fastcheck();

    returnresult;

}

这里 token 是一个指向这个释放池 POOL_BOUNDARY的指针。

pageForPointer(token) 会获取哨兵对象所在 AutoreleasePoolPage:主要是通过指针与 page 大小取模得到其偏移量(因为所有的 AutoreleasePoolPage 在内存中都是对齐的),最后通过 fastCheck() 函数检查得到的是不是一个 AutoreleasePoolPage。

  static void
    popPage(void *token, AutoreleasePoolPage *page, id *stop)
    {
        page->releaseUntil(stop);

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

popPage()中调用的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;
            memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
            page->protect();

            if (obj != POOL_BOUNDARY) {
                objc_release(obj);
            }
        }

        setHotPage(this);
    }

popPage()函数首先调用 releaseUntil()函数循环释放栈中的对象,直到 stop;releaseUntil()函数会先把 next 指针向前移动,取到将要释放的一个指针,之后调用 memset 擦除该指针所占内存,再调用 objc_release 函数释放该指针指向的对象,这样通过 next 指针循环往前查找去释放对象,期间可往前跨越多个 page,直到找到传进来的哨兵对象为止。当有嵌套的 autoreleasepool 时,会清除一层后再清除另一层,因为 pop 是会释放到上次 push 的位置为止,每次一层,互不影响。

popPage()函数中在releaseUntil()函数后面判断,如果传入的哨兵对象所在 page 有 child,有两种情况:一是当前 page 使用不满一半,从 child page 开始将后面所有 page 都 kill();二是当前 page 使用超过一半,从 child page 的 child page(即孙子,如果有的话)开始将后面所有的 page 都 kill()

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

    void kill() 
    {
        // Not recursive: we don't want to blow out the stack 
        // if a thread accumulates a stupendous amount of garbage
        AutoreleasePoolPage *page = this;
        while (page->child) page = page->child;

        AutoreleasePoolPage *deathptr;
        do {
            deathptr = page;
            page = page->parent;
            if (page) {
                page->unprotect();
                page->child = nil;
                page->protect();
            }
            delete deathptr;
        } while (deathptr != this);
    }

[NSObject autorelease]

  • (id)autorelease

—id _objc_rootAutorelease(self);

——obj->rootAutorelease();

——— id objc_object::rootAutorelease()

————id objc_object::rootAutorelease2()

————— AutoreleasePoolPage::autorelease((id)this);

到这里我们发现,一个对象autorelease,最终还是AutoreleasePoolPage的autorelease;

AutoreleasePoolPage的autorelease上面已经介绍过。

总结

一、自动释放池是一个双向链表,链表的每一个元素就是就是一个AutoreleasePoolPage。

image

二、释放池主要通过 push 及 pop 操作来管理:

每调用一次 objc_autoreleasepoolpush 操作就会创建一个新的autoreleasepool ,往 AutoreleasePoolPage 中插入一个POOL_BOUNDARY ,并返回这个POOL_BOUNDARY的内存地址。

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

三、向一个对象发送 autorelease 消息,会把该对象 add 进 AutoreleasePoolPage 里

四、从main函数中知道iOS项目默认是包裹在大的释放池中;RunLoop开始循环、休眠和退出时会对释放池进行objc_autoreleasepoolpush/objc_autoreleasepoolpop操作。

五、线程和释放池一一对应;

扩展

一、autoreleasepool的使用场景?

使用场景:多次创建临时变量导致内存上涨时,需要延迟释放

苹果的文档有说大意如下:
1、程序不是基于 UI 框架的,如命令行工具(因为它并没有内置的”autorelease pool”的支持)

2、你编写的循环创建了大量的临时对象(在循环体内创建一个@autoreloeasePool{}在循环中使用autorelease pool block可以降低内存峰值)

for (NSURL url in urls) {
@autoreleasepool {
/
Process the string, creating and autoreleasing more objects. */
}
}

3、如果创建了一个辅助线程。当线程开始执行的时候你必须立马创建一个autorelease pool block,否则可能造成autoreleased对象的内存泄漏。

4、使用容器的block版本的枚举器时,内部会自动添加一个AutoreleasePool:

[array enumerateObjectsUsingBlock:^(idobj,NSUIntegeridx,BOOL*stop) {

// 这里被一个局部@autoreleasepool包围着

}];

二、autorelease 对象会在什么时候释放?

分两种情况:

我们自己添加的 @autoreleasepool:会在大括号结束时释放(Any object (such as fileContents) sent an autorelease message inside the autorelease pool block is released at the end of the block.)

不使用 @autoreleasepool:不手动指定 autoreleasepool 的 autorelease 对象出了作用域之后,会被添加到最近一次创建的自动释放池中,并会在当前的 runloop 迭代结束时释放。(runloop循环系统会隐式创建一个新的autoreleasepool并在循环结束后释放)

一个线程创建的时候就会有一个autorelease pool的创建,并且在线程退出的时候,清空整个autorelease pool子线程中的autorelease变量

三、子线程中的autorelease变量什么时候释放?子线程里面,需要加autoreleasepool吗?

在子线程创建了 autoreleasepool 的话,产生的 autorelease 对象就会交给 autoreleasepool 去管理,在线程退出的时候,清空整个autoreleasepool。

如果没有创建 autoreleasepool ,但是产生了 autorelease 对象,就会调用AutoreleasePoolPage 的 autoreleaseNoPage 方法。该方法会自动创建一个 hotpage,并调用page->add(obj)将对象添加到 AutoreleasePoolPage 的栈中。

要弄清线程-runloop-autoreleasepool之间的关系。

主线程的runloop是默认创建的,系统会监听runloop两种事件(自动释放池的创建和释放,销毁的时机):
(1)kCFRunLoopEntry,即将进入runloop时,会创建一个autoreleasepool。
(2)kCFRunLoopBeforeWaiting,runloop即将休眠时,会释放autoreleasepool并创建一个新的autoreleasepool;
(3)kCFRunLoopExit,runloop即将退出时,会释放autoreleasepool。
(4)而autoreleasepool在释放时,会对插入到pool中的对象发送release消息。

所以,runloop每次迭代结束,autoreleasepool释放,aurelease对象释放。

runloop只可以获取,不可以手动创建。
子线程的runloop是手动获取的,在获取的时候系统会创建一个runloop并返回,所以手动获取到的runloop其实是系统刚创建好的。
子线程的autoreleasepool也需要手动获取,但区分情况,一般系统提供的block如usingBlock和GCD提供的block内部都会自动包裹一个autoreleasepool,不用手动加。但是你自己通过其他方式创建的子线程,在线程内部需要手动获取autoreleasepool,防止局部内存使用峰值过高或发生其他内存问题,最后,autoreleasepool释放时,也会对其管理的对象发送release消息。

四、那些对象会放入自动释放池?
在MRC中,调用 autorelease 的对象会被添加到自动释放池。
在ARC中,以类方法获取的对象(注意不是使用alloc/new/copy/mutablCopy获取的对象)不会马上被销毁,而是要等到超过autoreleasepool的作用域才会真实执行release,这是因为类方法里的编译器帮你返回了一个autoreleasing的对象。

    Animal *animal1 = [[Animal alloc] init];
    Animal *animal2 = [Animal animal];
    _objc_autoreleasePoolPrint();

如上代码打印,alloc创建的animal1并不会加入自动释放池,类方法创建的 animal2 会打印

objc[19507]: ##############
objc[19507]: AUTORELEASE POOLS for thread 0x1056cee00
objc[19507]: 1 releases pending.
objc[19507]: [0x7ff56080c000]  ................  PAGE  (hot) (cold)
objc[19507]: [0x7ff56080c038]    0x6000026e40f0  Animal
objc[19507]: ##############

你可能感兴趣的:(iOS--autoreleasepool)