iOS自动释放池--autoreleasepool

作者: 风紧扯呼 | 来源:发表于2020-04-18 18:24 被阅读0次

    在上一篇文章中,详细分析了IOS内存管理的内存布局、内存管理方案、引用计数等内容,本篇文章将继续上篇文章的内容探索自动释放池autoreleasepool的相关知识。

    1、autoreleasepool初探

    熟悉OC开发的都知道,在main函数中就有@autoreleasepool这样一个东西,其实这就是自动释放池。那么@autoreleasepool的底层实现是什么样的呢?我们在命令行中使用 clang -rewrite-objc main.m -o main.cpp 让编译器重新改写这个文件,讲得到一个main.cpp文件,打开该文件,找到其中的main函数。

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

    我们可以看到@autoreleasepool转化成了__AtAutoreleasePool这样一个结构体,那么意味着@autoreleasepool的本质就是__AtAutoreleasePool结构体。

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

    这个结构体会在初始化时调用objc_autoreleasePoolPush() 方法,会在析构时调用
    objc_autoreleasePoolPop 方法。

    这就说明了main函数在实际工作的时候是这样的:

    int main(int argc, const char *argv[])
    {
        void * atautoreleasepoolobj = objc_autoreleasePoolPush();
    
        // do whatever you want
    
        objc_autoreleasePoolPop(atautoreleasepoolobj);
        return 0;
    }
    

    似乎一切都是围绕着objc_autoreleasePoolPush()objc_autoreleasePoolPop这两个方法展开。那么我们来看下这两个方法的源码实现:

    void *
    objc_autoreleasePoolPush(void)
    {
        // 调用了AutoreleasePoolPage中的push方法
        return AutoreleasePoolPage::push();
    }
    
    void
    objc_autoreleasePoolPop(void *ctxt)
    {
        // 调用了AutoreleasePoolPage中的pop方法
        AutoreleasePoolPage::pop(ctxt);
    }
    

    上面的两个方法看上去是对AutoreleasePoolPage对应静态方法pushpop的封装。

    2、AutoreleasePoolPage

    在runtime中的源码(objc4-756.2版本)中找到了一段注释,这段注释对我们理解AutoreleasePoolPage的底层结构会有所帮助。

    • 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组成的,而AutoreleasePoolPage是以双向链表的形式连接起来。

    image

    2.1、AutoreleasePoolPage结构

    来看一下AutoreleasePoolPage的代码定义(只列出了关键代码,部分代码省略)。

    class AutoreleasePoolPage 
    {
    #   define EMPTY_POOL_PLACEHOLDER ((id*)1)
    #   define POOL_BOUNDARY nil
        static pthread_key_t const key = AUTORELEASE_POOL_KEY;
        static uint8_t const SCRIBBLE = 0xA3;  // 0xA3A3A3A3 after releasing
        // AutoreleasePoolPage的大小,通过宏定义,可以看到是4096字节
        static size_t const SIZE =
     #if PROTECT_AUTORELEASEPOOL
            PAGE_MAX_SIZE;  // must be multiple of vm page size
     #else
            PAGE_MAX_SIZE;  // size and alignment, power of 2
     #endif
        static size_t const COUNT = SIZE / sizeof(id);
    
        magic_t const magic;//16字节
        id *next;//8字节
        pthread_t const thread;//8字节
        AutoreleasePoolPage * const parent;//8字节
        AutoreleasePoolPage *child;//8字节
        uint32_t const depth;//4字节
        uint32_t hiwat;//4字节
    }
    
    • magic:用来校验AutoreleasePoolPage的结构是否完整。
    • *next:next指向的是下一个AutoreleasePoolPage中下一个为空的内存地址(新来的对象会存储到next处),初始化时指向begin()
    • thread:保存了当前页所在的线程(一个AutoreleasePoolPage属于一个线程,一个线程中可以有多个AutoreleasePoolPage)。
    • *parent:指向父节点,第一个parent节点为nil
    • *child:指向子节点,最后一个child节点为nil
    • depth:代表深度,从0开始,递增+1。
    • hiwat:代表high water Mark最大入栈数。
    • SIZEAutoreleasePoolPage的大小,值为PAGE_MAX_SIZE,4096个字节。
    • POOL_BOUNDARY:只是nil的别名。在每个自动释放池初始化调用 objc_autoreleasePoolPush 的时候,都会把一个POOL_SENTINEL push到自动释放池的栈顶,并且返回这个POOL_SENTINEL自动释放池边界。而当方法 objc_autoreleasePoolPop调用时,就会向自动释放池中的对象发送release消息,直到第一个 POOL_SENTINEL

    AutoreleasePoolPage中的第一个对象是存储在next后面,那么就形成如下图所示这样一个结构。

    image

    其中的56个字节存储的AutoreleasePoolPage的成员变量,其他的区域存储加载到自动释放池的对象。
    next==begin()时表示AutoreleasePoolPage为空,当next==end()的时表示AutoreleasePoolPage已满。

    2.2、AutoreleasePoolPage容量

    在上一个小节的内容中我们分析了AutoreleasePoolPage的结构,了解到每一个AutoreleasePoolPage的大小是4096字节,其中56字节用于存储成员变量,剩下的区域存储加载到自动释放池的对象,那么似乎答案呼之欲出,一个AutoreleasePoolPage可以存储(4096-56)/8=505个对象。但是有一个注意的点,第一个page存放的需要释放的对象的容量应该是504个,因为在创建page的时候会在next的位置插入1POOL_SENTINEL

    image

    2.3、push方法

    通过前面小节的分析,我们知道objc_autoreleasePoolPush的本质就是调用push方法。我们先来看下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对象插入到栈顶。slowpath表示小概率发生。

    2.3.1、autoreleaseFast方法

    如下是autoreleaseFast方法的源码

     static inline id *autoreleaseFast(id obj)
    {
        // hotPage就是当前正在使用的AutoreleasePoolPage
        AutoreleasePoolPage *page = hotPage();
        if (page && !page->full()) {
            // 有hotPage且hotPage不满,将对象添加到hotPage中
            return page->add(obj);
        } else if (page) {
            // 有hotPage但是hotPage已满
            // 使用autoreleaseFullPage初始化一个新页,并将对象添加到新的AutoreleasePoolPage中
            return autoreleaseFullPage(obj, page);
        } else {
            // 无hotPage
            // 使用autoreleaseNoPage创建一个hotPage,并将对象添加到新创建的page中
            return autoreleaseNoPage(obj);
        }
    }
    

    autoreleaseFast方法的代码很简单,只要是三个判断分支。

    1. 如果有hotPage且没有满,则调用add方法将对象添加到hotPage中。否则执行第2步。
    2. 如果有hotPage但是已经满了,则调用autoreleaseFullPage方法初始化一个新页,并将对象添加到新的AutoreleasePoolPage中。否则执行第3步。
    3. 如果没有hotPage,则调用autoreleaseNoPage方法创建一个hotPage,并将对象添加到新创建的page
    hotPage 可以理解为当前正在使用的 AutoreleasePoolPage。
    

    2.3.2、add 添加对象

    add方法将对象添加到AutoreleasePoolPage中。

    id *add(id obj)
    {
        ASSERT(!full());
        unprotect();
        id *ret = next;  // faster than `return next-1` because of aliasing
        *next++ = obj;//将obj存放在next处,并将next指向下一个位置
        protect();
        return ret;
    }
    

    这个方法其实就是一个压栈操作,将对象添加到AutoreleasePoolPage中,然后移动栈顶指针。

    2.3.3、autoreleaseFullPage

    autoreleaseFullPage方法会重新开辟一个新的AutoreleasePoolPage页,并将对象添加到其中。

    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 {
            // 如果page->child不为空,那么使用page->child
            if (page->child) page = page->child;
            // 否则的话,初始化一个新的AutoreleasePoolPage
            else page = new AutoreleasePoolPage(page);
        } while (page->full());
    
        // 将找到的合适的page设置成hotPage
        setHotPage(page);
        // 将对象添加到hotPage中
        return page->add(obj);
    }
    

    遍历找到未满的的page,如果没有找到则初始化一个新的page,并将page设置为hotPage,同时将对象添加到这个page中。

    2.3.4、autoreleaseNoPage

    如果当前内存中不存在hotPage,就会调用autoreleaseNoPage方法初始化一个AutoreleasePoolPage

    id *autoreleaseNoPage(id obj)
    {
        // Install the first page.
        // 初始化一个AutoreleasePoolPage
        // 当前内存中不存在AutoreleasePoolPage,则从头开始构建AutoreleasePoolPage,也就是其parent为nil
        AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
        // 将初始化的AutoreleasePoolPage设置成hotPage
        setHotPage(page);
        
        // Push a boundary on behalf of the previously-placeholder'd pool.
        // 添加一个边界对象(nil)
        if (pushExtraBoundary) {
            page->add(POOL_BOUNDARY);
        }
        
        // Push the requested object or pool.
        // 将对象添加到AutoreleasePoolPage中
        return page->add(obj);
    }
    

    当前内存中不存在AutoreleasePoolPage,则从头开始构建AutoreleasePoolPage,也就是其parentnil。初始化之后,将当前页标记为hotPage,然后会先向这个page中添加一个POOL_SENTINEL 对象,来确保在pop调用的时候,不会出现异常。最后,将对象添加到自动释放池中。

    2.4、pop方法

    上面小节我们探索了objc_autoreleasePoolPush,下面我们看看objc_autoreleasePoolPop
    objc_autoreleasePoolPop的本质就是调用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();
            }
        }
    }
    

    上面方法做了如下几件事:

    1. 调用pageForPointer获取当前token所在的page。
    2. 调用releaseUntil方法释放栈中的对象,直到stop
    3. 调用childkill方法。

    2.4.1、pageForPointer找到page

    pageForPointer方法主要是通过通过内存地址的操作,获取当前token所在页的首地址。

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

    将指针与页面的大小(4096)取模,可以得到当前指针的偏移量。然后将指针的地址减偏移量便可以得到首地址。

    2.4.2、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
        
        // 释放AutoreleasePoolPage中的对象,直到next指向stop
        while (this->next != stop) {
            // Restart from hotPage() every time, in case -release 
            // autoreleased more objects
            // hotPage可以理解为当前正在使用的page
            AutoreleasePoolPage *page = hotPage();
    
            // fixme I think this `while` can be `if`, but I can't prove it
            // 如果page为空的话,将page指向上一个page
            // 注释写到猜测这里可以使用if,我感觉也可以使用if
            // 因为根据AutoreleasePoolPage的结构,理论上不可能存在连续两个page都为空
            while (page->empty()) {
                page = page->parent;
                setHotPage(page);
            }
    
            page->unprotect();
            // obj = page->next; page->next--;
            id obj = *--page->next;
            memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
            page->protect();
    
            // POOL_BOUNDARY为nil,是哨兵对象
            if (obj != POOL_BOUNDARY) {
                // 释放obj对象
                objc_release(obj);
            }
        }
    
        // 重新设置hotPage
        setHotPage(this);
    
    #if DEBUG
        // we expect any children to be completely empty
        for (AutoreleasePoolPage *page = child; page; page = page->child) {
            assert(page->empty());
        }
    #endif
    }
    

    因为AutoreleasePool实际上就是由AutoreleasePoolPage组成的双向链表,因此,*stop可能不是在最新的AutoreleasePoolPage中,即hotPage,这时需要从hotPage开始,一直释放,直到stop,中间所经过的所有AutoreleasePoolPage里面的对象都要释放。
    对象的释放objc_release方法请移步前面的文章iOS内存管理一:Tagged Pointer&引用计数

    2.4.3、kill方法

    kill方法删除双向链表中的每一个page

    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;
        // 找到链表最末尾的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);
    }
    

    3、自动释放池和线程

    官方文档Using Autorelease Pool Blocks中关于自动释放池和线程的关系有如下一段描述。

    Each thread in a Cocoa application maintains its own stack of autorelease pool blocks. If you are writing a Foundation-only program or if you detach a thread, you need to create your own autorelease pool block.
    If your application or thread is long-lived and potentially generates a lot of autoreleased objects, you should use autorelease pool blocks (like AppKit and UIKit do on the main thread); otherwise, autoreleased objects accumulate and your memory footprint grows. If your detached thread does not make Cocoa calls, you do not need to use an autorelease pool block.

    翻译成中文如下:

    应用程序中的每个线程都维护自己的自动释放池块堆栈。如果您正在编写一个仅限基础的程序,或者正在分离一个线程,那么您需要创建自己的自动释放池块。
    如果您的应用程序或线程是长生命周期的,并且可能会生成大量的自动释放对象,那么您应该使用自动释放池块(如在主线程上使用AppKit和UIKit);否则,自动释放的对象会累积,内存占用会增加。如果分离的线程不进行Cocoa调用,则不需要使用自动释放池块。

    从上面这段秒速我们可以知道自动释放池和线程是紧密相关的,每一个自动释放池只对应一个线程。

    4、AutoreleasePool和RunLoop

    一般很少会将自动释放池和RunLoop联系起来,但是如果打印[NSRunLoop currentRunLoop]结果中会发现和自动释放池相关的回调。

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

    即App启动后,苹果会给RunLoop注册很多个observers,其中有两个是跟自动释放池相关的,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()。\

    • 第一个observer监听的是activities=0x1(kCFRunLoopEntry),也就是在即将进入loop时,其回调会调用_objc_autoreleasePoolPush() 创建自动释放池;
    • 第二个observer监听的是activities = 0xa0(kCFRunLoopBeforeWaiting | kCFRunLoopExit)
      即监听的是准备进入睡眠和即将退出loop两个事件。在准备进入睡眠之前,因为睡眠可能时间很长,所以为了不占用资源先调用_objc_autoreleasePoolPop()释放旧的释放池,并调用_objc_autoreleasePoolPush() 创建新建一个新的,用来装载被唤醒后要处理的事件对象;在最后即将退出loop时则会 _objc_autoreleasePoolPop()释放池子。

    5、总结

    1. 自动释放池是由AutoreleasePoolPage以双向链表的方式实现的。
    2. 当对象调用autorelease方法时,会将对象加入AutoreleasePoolPage的栈中。
    3. 调用AutoreleasePoolPage::pop方法会向栈中的对象发送release消息。
    4. 自动释放池和线程是紧密相关的,每一个自动释放池只对应一个线程

    参考资料

    相关文章

      网友评论

        本文标题:iOS自动释放池--autoreleasepool

        本文链接:https://www.haomeiwen.com/subject/apkeyhtx.html