美文网首页iOS底层
OC内存管理-自动释放池

OC内存管理-自动释放池

作者: HotPotCat | 来源:发表于2021-09-15 22:51 被阅读0次

    一、自动释放池简介

    @autoreleasepool {
        NSLog(@"Hello, World!");
    }
    

    对于autoreleasepool是怎么实现的呢?直接xcrun查看下对应的c++实现:

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

    可以看到@autoreleasepool被转换成了__AtAutoreleasePool __autoreleasepool__AtAutoreleasePool定义如下:

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

    __AtAutoreleasePool是一个结构体,有一个成员变量atautoreleasepoolobj,在构造函数中调用objc_autoreleasePoolPush,在析构函数中调用objc_autoreleasePoolPop
    那么@autoreleasepool其实也就相当于:

    objc_autoreleasePoolPush()
    ......
    objc_autoreleasePoolPop()
    

    当然也可以通过汇编验证:


    image.png

    objc_autoreleasePoolPush下符号断点:

    image.png
    定位到了objc_autoreleasePoolPush的源码在objc库中。

    @autoreleasepool 底层实际调用了objc_autoreleasePoolPushobjc_autoreleasePoolPop

    二、自动释放池的结构

    objc_autoreleasePoolPushobjc_autoreleasePoolPop对应源码如下:

    void *
    objc_autoreleasePoolPush(void)
    {
        return AutoreleasePoolPage::push();
    }
    
    NEVER_INLINE
    void
    objc_autoreleasePoolPop(void *ctxt)
    {
        AutoreleasePoolPage::pop(ctxt);
    }
    

    可以看到是属于AutoreleasePoolPage命名空间的。
    AutoreleasePoolPage定义如下:

    /***********************************************************************
       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. 
    **********************************************************************/
    
    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);
        static size_t const MAX_FAULTS = 2;
    ......
    }
    

    AutoreleasePoolPage本身有一些静态成员变量,继承自AutoreleasePoolPageData。注释说明了自动释放池是一个线程的栈指针的集合。有一个边界对象(哨兵对象)。本身是一个双向链表,并且有hotpagecoldpage

    2.1 AutoreleasePoolPageData

    struct AutoreleasePoolPageData
    {
    ......
        magic_t const magic;//16 用来校验 AutoreleasePoolPage 的结构是否完整
        __unsafe_unretained id *next;//8 next 指向最新添加的 autoreleased 对象的下一个位置,初始化时指向 begin() ;
        pthread_t const thread;//8 指向当前线程
        AutoreleasePoolPage * const parent;//8 指向父结点,第一个结点的 parent 值为 nil
        AutoreleasePoolPage *child;//8 指向子结点,最后一个结点的 child 值为 nil
        uint32_t const depth;//4 代表深度,从 0 开始,往后递增 1
        uint32_t hiwat;//4 代表 high water mark 最大入栈数量标记
    
        //构造函数
        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:用来校验 AutoreleasePoolPage 的结构是否完整。
    • next:指向最新添加的 autoreleased 对象的下一个位置,初始化时指向 begin()
    • thread指向当前线程
    • parent:指向父结点,第一个结点的 parent 值为 nil
    • child:指向子结点,最后一个结点的 child 值为 nilparentchild说明了它是一个双向链表。
    • depth:代表深度,从 0 开始,往后递增 1。也就是有多少页。
    • hiwat:代表 high water mark 最大入栈数量标记。

    magic_t结构如下:

    struct magic_t {
        static const uint32_t M0 = 0xA1A1A1A1;
    #   define M1 "AUTORELEASE!"
        static const size_t M1_len = 12;
        uint32_t m[4];
    ......
    }
    

    M0M1_lenstatic变量,所以magic_t占用空间与m[4]相关,也就是16字节。整个AutoreleasePoolPageData占用的内存空间为56字节。

    有如下代码:

    extern void _objc_autoreleasePoolPrint(void);
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            NSObject *obj = [[NSObject alloc] autorelease];
            _objc_autoreleasePoolPrint();
        }
        return 0;
    }
    

    输出:

    objc[94411]: ##############
    objc[94411]: AUTORELEASE POOLS for thread 0x1000dedc0
    objc[94411]: 2 releases pending.
    objc[94411]: [0x10400b000]  ................  PAGE  (hot) (cold)
    objc[94411]: [0x10400b038]  ################  POOL 0x10400b038
    objc[94411]: [0x10400b040]       0x103366e90  NSObject
    objc[94411]: ##############
    

    可以看到直接输出了AutoreleasePoolPage中有2个对象,一个是obj另外一个是哨兵对象(POOL 0x10400b038)。

    1._objc_autoreleasePoolPrintobjc源码中用于调试的函数,打印了AutoreleasePoolPage中的内容:

    image.png
    2.可以通过单个文件配置-fno-objc-arc或者整个项目配置Objective-C Automatic Reference CountingNO关闭arc
    image.png

    自动释放池是一个双向链表的结构,由多个AutoreleasePoolPage组成,AutoreleasePoolPage本身占用56个字节空间大小。

    三、自动释放池的压栈与创建

    3.1 push

    static inline void *push() 
    {
        id *dest;
        if (slowpath(DebugPoolAllocation)) {
            // Each autorelease pool starts on a new pool page.
            dest = autoreleaseNewPage(POOL_BOUNDARY);
        } else {
            //非debug调用 autoreleaseFast
            dest = autoreleaseFast(POOL_BOUNDARY);
        }
        ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
        return dest;
    }
    

    直接调动autoreleaseFast,哨兵对象POOL_BOUNDARY传递的是nil

    3.2 autoreleaseFast

    static inline id *autoreleaseFast(id obj)
    {
        //获取 hotPage
        AutoreleasePoolPage *page = hotPage();
        //page 存在 并且没有满
        if (page && !page->full()) {
            //添加对象进入page
            return page->add(obj);
        } else if (page) {//page存在满了
            return autoreleaseFullPage(obj, page);
        } else { //page不存在创建,首次需要创建。
            return autoreleaseNoPage(obj);
        }
    }
    
    • page存在并且没有存满的情况下调用add添加对象。
    • page存在并且存满的情况调用autoreleaseFullPage创建新的page
    • page不存在则调用autoreleaseNoPage创建page

    3.3 autoreleaseNoPage

    #   define POOL_BOUNDARY nil
    
    static __attribute__((noinline))
    id *autoreleaseNoPage(id obj)
    {
        ......
        // We are pushing an object or a non-placeholder'd pool.
    
        // Install the first page.
        //创建新的page
        AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
        //设置page为hotPage
        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.
        //添加obj到page中
        return page->add(obj);
    }
    
    • 调用AutoreleasePoolPage构造方法创建page
    • 设置pagehotPage
    • 添加哨兵对象,哨兵对象是一个nil
    • page中添加obj

    3.3.1 AutoreleasePoolPage 创建 page

    AutoreleasePoolPage(AutoreleasePoolPage *newParent) :
        AutoreleasePoolPageData(begin(),//page存储的起始位置,也就是 _next 下一个元素的位置
                                objc_thread_self(),//线程
                                newParent,//parent,首次为nil
                                newParent ? 1+newParent->depth : 0,//depth 首次 0
                                newParent ? newParent->hiwat : 0) //hiwat 首次 0
    {
        if (objc::PageCountWarning != -1) {
            checkTooMuchAutorelease();
        }
    
        if (parent) {//首次没有parent
            parent->check();
            ASSERT(!parent->child);
            parent->unprotect();
            //设置parent的child为自己。
            parent->child = this;
            parent->protect();
        }
        protect();
    }
    

    在其中会判断parent从而设置它的child为当前pageAutoreleasePoolPage中调用了AutoreleasePoolPageData的构造函数。传递的第一个参数为begin

    image.png
    • begin初始给了next指针,偏移自身56字节。
    • thread设置为当前线程。
    • parent设置为前一个page,首次为nil
    • depthparent.depth + 1也就是加了1page,首次赋值为0,所以当前page = depth + 1
    • hiwat设置为parenthiwat,首次赋值为0

    thisAutoreleasePoolPage,返回值为加56,也就是AutoreleasePoolPageData本身占用的内存空间。说明每个page都有56字节存储自身内容,然后才存储自动释放的对象:

    image.png
    先存储自身的成员变量,首次创建page会先存哨兵对象(nil)再存储自动释放的对象。

    3.4 autoreleaseFullPage

    page满的情况下调用了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 {
            //找到最后一个page
            if (page->child) page = page->child;
            //创建新的page
            else page = new AutoreleasePoolPage(page);
        } while (page->full());
        //设置page为hotpage
        setHotPage(page);
        //obj添加进page
        return page->add(obj);
    }
    
    • 找到最后一个page然后创建新的page,参数是最后一个page
    • 设置创建的pagehotpage
    • obj加入page

    3.5 page->add

    id *add(id obj)
    {
        ASSERT(!full());
        unprotect();
        id *ret;
    //多次持有逻辑的处理,count++
    ......
        ret = next;  // faster than `return next-1` because of aliasing
        //内存平移 存储obj 然后next指向下一个空间。
        *next++ = obj;
     done:
        protect();
        return ret;
    }
    

    核心逻辑是存储obj指针并且next指针平移。如果对象已经在自动释放池中则会进行count++(从0开始计数,实际数量需要count +1)。

    四、自动释放池满页临界值

    首先为什么分页呢?
    如果不分页所有对象都会在一页,操作复杂,管理不便,并且需要分配一块很大的内存,如果分页则不需要连续的内存。并且开锁解锁只针对单个页面更安全。

    既然有full逻辑,那么什么情况下page会添加满呢?

    bool full() { 
        return next == end();
    }
    

    也就是page存储没有空间了,那么每个page占用多大内存呢?
    有如下验证代码:

    extern void _objc_autoreleasePoolPrint(void);
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            for(int i = 0; i < 505; i++) {
                NSObject *obj = [[NSObject alloc] autorelease];
            }
            _objc_autoreleasePoolPrint();
        }
        return 0;
    }
    

    输出:

    objc[13893]: ##############
    objc[13893]: AUTORELEASE POOLS for thread 0x1000dedc0
    objc[13893]: 506 releases pending.
    objc[13893]: [0x10680a000]  ................  PAGE (full)  (cold)
    objc[13893]: [0x10680a038]  ################  POOL 0x10680a038
    objc[13893]: [0x10680a040]       0x1010206f0  NSObject
    objc[13893]: [0x10680a048]       0x101705390  NSObject
    ......
    objc[13893]: [0x10680aff8]       0x1017072f0  NSObject
    objc[13893]: [0x105009000]  ................  PAGE  (hot) 
    objc[13893]: [0x105009038]       0x101707300  NSObject
    objc[13893]: ##############
    

    那也就是存储了504 + POOL_BOUNDARY一共505个对象指针,加上page本身56字节一共505 * 8 + 56 = 4K,也就是一页4K大小。接着第二页由于没有POOL_BOUNDARY那么就会存储505obj

    在源码中有PAGE_MIN_SIZE的定义(112 = 4K):

    image.png
    • AutoreleasePoolPage大小为4K,每一页都有本身的成员变量56kb,剩余空间存储对象指针。
    • 一个AutoreleasePoolPage只有一个哨兵对象。

    五、自动释放池出栈(objc_autoreleasePoolPop)

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

    ctxt值为0x1。最终调用了popPage

    static inline void
    pop(void *token)
    {
    ......
        //token 也为 page开始位置,page地址   stop 为page的 begin
        return popPage<false>(token, page, stop);
    }
    

    最终调用popPage释放page

    5.1 popPage

    template<bool allowDebug>
    static void
    popPage(void *token, AutoreleasePoolPage *page, id *stop)
    {
        if (allowDebug && PrintPoolHiwat) printHiwat();
        //调用对象的release,移动next指针
        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();//释放child
            }
            else if (page->child->child) {//否则释放child-child
                page->child->child->kill();
            }
        }
    }
    
    • 调用releaseUntil释放对象以及移动next指针。

    5.1.1 releaseUntil

    void releaseUntil(id *stop) 
    {
        while (this->next != stop) {
            // Restart from hotPage() every time, in case -release 
            // autoreleased more objects
            //获取hotPage
            AutoreleasePoolPage *page = hotPage();
    
            // fixme I think this `while` can be `if`, but I can't prove it
            //page为空往前找,并且设置hotPage
            while (page->empty()) {
                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
            //page中存储的地址设为 0xA3
            memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
            page->protect();
    
            //不为哨兵对象调用 objc_release
            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++) {
                    //release
                    objc_release(obj);
                }
    #else
                objc_release(obj);
    #endif
            }
        }
        //设置hotPage 为release后指向的page
        setHotPage(this);
    ......
    }
    
    • 循环调用对象的objc_release释放对象。
    • 这里count是因为对象可能被自动释放池多次持有。

    5.1.2 kill

    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;
        //找到最后一个child page
        while (page->child) page = page->child;
        //循环释放page
        AutoreleasePoolPage *deathptr;
        do {
            deathptr = page;
            page = page->parent;
            if (page) {
                page->unprotect();
                //child置为nil
                page->child = nil;
                page->protect();
            }
            //释放page空间
            delete deathptr;
        } while (deathptr != this);
    }
    
    • 根据 page 找到最后的child
    • child置空,释放page开辟的空间。

    AutoreleasePoolPage根据入栈顺序先调用objc_release释放对象,最后释放page对应的内存空间。
    childparent用于存储和释放过程中page的查找与操作。

    六、自动释放池扩展

    6.1 自动释放池嵌套

    既然自动释放池会开辟page存储自动释放的对象,并且自动释放池与线程时对应的关系,那么如果自动释放池嵌套的情况是如何存储的呢?

    @autoreleasepool {
        NSObject *obj = [[NSObject alloc] autorelease];
        @autoreleasepool {
            NSObject *obj1 = [[NSObject alloc] autorelease];
            //输出全部
            _objc_autoreleasePoolPrint();
        }
        //只输出外层的
        _objc_autoreleasePoolPrint();
    }
    

    输出:

    objc[86251]: ##############
    objc[86251]: AUTORELEASE POOLS for thread 0x1000dedc0
    objc[86251]: 4 releases pending.
    objc[86251]: [0x104810000]  ................  PAGE  (hot) (cold)
    objc[86251]: [0x104810038]  ################  POOL 0x104810038
    objc[86251]: [0x104810040]       0x10371cde0  NSObject
    objc[86251]: [0x104810048]  ################  POOL 0x104810048
    objc[86251]: [0x104810050]       0x1037119d0  NSObject
    objc[86251]: ##############
    

    可以看到自动释放池在page中存储是按顺序存储的。

    • 1page可能会对应多个自动释放池,1个自动释放池也可能存储在多个page中。
    • 1个自动释放池对应1个哨兵对象。

    6.2 autorelease 验证

    既然在MRC下主动调用autorelease的对象会加入自动释放池,那么在ARC下什么情况下会被加入自动释放池?

    @autoreleasepool {
        HPObject *hpObj = [HPObject alloc];
        NSLog(@"hpObj: %@",hpObj);
    
        __autoreleasing  HPObject *hpObj1 = [HPObject alloc];
        NSLog(@"hpObj1: %@",hpObj1);
    
        HPObject *hpObj2 = [HPObject object];
        NSLog(@"hpObj2: %@",hpObj2);
    
        _objc_autoreleasePoolPrint();
    }
    

    输出:

    hpObj: <HPObject: 0x10078f580>
    hpObj1: <HPObject: 0x10331ed10>
    hpObj2: <HPObject: 0x10331ffe0>
    objc[96721]: ##############
    objc[96721]: AUTORELEASE POOLS for thread 0x1000dedc0
    objc[96721]: 3 releases pending.
    objc[96721]: [0x101016000]  ................  PAGE  (hot) (cold)
    objc[96721]: [0x101016038]  ################  POOL 0x101016038
    objc[96721]: [0x101016040]       0x10331ed10  HPObject
    objc[96721]: [0x101016048]       0x10331ffe0  HPObject
    objc[96721]: ##############
    

    可以看到hpObj没有加入自动释放池。__autoreleasing修饰的对象会加入自动释放池。
    那么这块是怎么区分的呢?
    allocnewcopymutableCopy开头命名的方法返回的对象不会被加入自动释放池,其余方法返回的对象会被加入自动释放池。

    @autoreleasepool {
        HPObject *allocObj = [HPObject allocObject];
        NSLog(@"allocObj: %@",allocObj);
        
        HPObject *newObj = [HPObject newObject];
        NSLog(@"newObj: %@",newObj);
    
        HPObject *initObj = [HPObject initObject];
        NSLog(@"initObj: %@",initObj);
    
        HPObject *copyObj = [HPObject copyObject];
        NSLog(@"copyObj: %@",copyObj);
    
        HPObject *mutableCopyObj = [HPObject mutableCopyObject];
        NSLog(@"mutableCopyObj: %@",mutableCopyObj);
    
        HPObject *otherObj = [HPObject otherObject];
        NSLog(@"otherObj: %@",otherObj);
    
        _objc_autoreleasePoolPrint();
    }
    

    输出:

    image.png
    单独每个验证都与结论一致,一起验证只有第一个非copy/new/copy/mutableCopy开始命名的方法返回的对象才加入自动释放池。
    经过断点验证initObjotherObject都会走_objc_rootAutorelease的逻辑,但是在_objc_autoreleasePoolPrint调用之前,otherObject已经调用objc_autoreleasePoolPop释放了。所以才会不一致。

    对应的汇编还原代码:

    +(void *)allocObject {
        rax = objc_alloc_init(@class(HPObject), _cmd);
        return rax;
    }
    
    +(void *)newObject {
        rax = objc_alloc_init(@class(HPObject), _cmd);
        return rax;
    }
    
    +(void *)initObject {
        rax = objc_alloc_init(@class(HPObject), _cmd);
        rax = [rax autorelease];
        return rax;
    }
    
    +(void *)copyObject {
        rax = objc_alloc_init(@class(HPObject), _cmd);
        return rax;
    }
    
    +(void *)mutableCopyObject {
        rax = objc_alloc_init(@class(HPObject), _cmd);
        return rax;
    }
    
    +(void *)otherObject {
        rax = objc_alloc_init(@class(HPObject), _cmd);
        rax = [rax autorelease];
        return rax;
    }
    

    只有以非alloc/new/copy/mutableCopy开头的方法在编译阶段ARC才会调用autorelease。当然如果返回值以__autoreleasing修饰也会加入自动释放池。

    alloc/new/copy/mutableCopy开头并且同时__autoreleasing修饰的情况:

    image.png

    总结:
    自动释放池结构:


    自动释放池结构
    • @autoreleasepool底层调用的是objc_autoreleasePoolPushobjc_autoreleasePoolPop
    • 自动释放池的存储数据的是一以AutoreleasePoolPage为解除的双向链表结构。其中包含了parentchild指针连接AutoreleasePoolPagebeginnext标记自动释放池的开始和结束。
      • 第一个pageparent 值为 nil
      • 最后一个pagechild 值为 nil
      • begin标记page开始的位置,从56字节开始(56字节存储page本身的数据)。
    • next指向最新添加的 autoreleased 对象的下一个位置,初始化时指向 begin()
    • 每一个AutoreleasePoolPage都对应一个线程,一个线程可能对应多个page
    • 每个自动释放池第一个存储的对象是哨兵对象,并且每个自动释放池只有一个哨兵对象。POOL_BOUNDARY是一个为nil的空对象。
    • 多次添加同一个哨兵对象对应的count会进行+1
    • 每个page大小为4k
    • 自动释放池的释放会先遍历释放对象,然后释放对应page的空间。
    • __autoreleasing修饰的对象会被加入自动释放池。
    • alloc、new、copy、mutableCopy开头命名的方法返回的对象不会被加入自动释放池(编译期间就确定了)。

    相关文章

      网友评论

        本文标题:OC内存管理-自动释放池

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