美文网首页
ios自动释放池

ios自动释放池

作者: lth123 | 来源:发表于2021-07-11 17:10 被阅读0次
    一. 自动释放池源码解析
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
    
            NSObject *obj = [[NSObject alloc] init];
    
        }
        return 0;
    }
    

    在main函数中使用如下命令重写成c++文件

    clang -rewrite-objc main.m

    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
    
            NSObject *obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
    
        }
        return 0;
    }
    

    通过上面的c++代码可以看到:@autoreleasepool变成了__AtAutoreleasePool __autoreleasepool。也就是说 @autoreleasepool {} 被转换为一个 __AtAutoreleasePool 结构体。

    __AtAutoreleasePool结构
    struct __AtAutoreleasePool {
        //构造函数
      // 内部调用objc_autoreleasePoolPush函数。创建一个atautoreleasepoolobj
      __AtAutoreleasePool() {
          atautoreleasepoolobj = objc_autoreleasePoolPush();
          
      }
    // 析构函数
    // 内部调用objc_autoreleasePoolPop函数
      ~__AtAutoreleasePool()
        {
          objc_autoreleasePoolPop(atautoreleasepoolobj);
        }
      void * atautoreleasepoolobj;
    };
    
    

    __AtAutoreleasePool是一个结构体,内部实现了一个构造函数objc_autoreleasePoolPush()创建了一个自动释放池对象atautoreleasepoolobj和一个析构函数objc_autoreleasePoolPop(atautoreleasepoolobj).

    由此说明自动释放池的使用就是将作用域中的代码包含在__AtAutoreleasePool的构造函数和析构函数中,由atautoreleasepoolobj对象对作用域的内存进行管理.

    这个结构体会在初始化时调用 objc_autoreleasePoolPush() 方法,会在析构时调用 objc_autoreleasePoolPop 方法。
    这表明,我们的 main 函数在实际工作时其实是这样的:

    int main(int argc, const char * argv[]) {
        {
            void * atautoreleasepoolobj = objc_autoreleasePoolPush();
    
            // 加入自动释放池中的对象
    
            objc_autoreleasePoolPop(atautoreleasepoolobj);
        }
        return 0;
    }
    

    二.objc_autoreleasePoolPush的实现

    在runtime源码中可以看到该函数的实现如下:

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

    在objc_autoreleasePoolPush函数内部调用了AutoreleasePoolPage对象的push函数。
    我们先来看下AutoreleasePoolPage 的结构

    AutoreleasePoolPage结构

    AutoreleasePoolPage集成自AutoreleasePoolPageData 源码如下

    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)
        {
        }
    };
    
    • magic:用来校验AutoreleasePoolPage结构是否完整;
    • next:指向最新添加的autoreleased对象的下一个位置,初始化时指向begin;
    • thread:指向当前线程;
    • parent:指向父结点,第一个AutoreleasePoolPage结点的父结点为nil;
    • child:指向子结点,最后一个AutoreleasePoolPage结点的子结点为nil;
    • depth:当前结点的深度,从0开始,往后递增;
    • hiwat:代表hige water mark最大入栈数量标记;
    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
    }
    
    #define PROTECT_AUTORELEASEPOOL 0 // 说明目前版本 SIZE = PAGE_MIN_SIZE;
    
    #define I386_PGBYTES            4096            /* bytes per 80386 page */
    #define PAGE_SIZE               I386_PGBYTES
    #define PAGE_MIN_SIZE           PAGE_SIZE
    // 可以看到PAGE_MIN_SIZE的值是4096
    
    
    • 每一个自动释放池都是由一系列的 AutoreleasePoolPage 组成的,并且每一个 AutoreleasePoolPage 的大小都是 4096 字节(16 进制 0x1000)

    • 自动释放池中的 AutoreleasePoolPage 是以双向链表的形式连接起来的,如下图


      01.png

    如果我们的一个AutoreleasePoolPage 被初始化在内存的 0x100816000 ~ 0x100817000 中,它在内存中的结构如下

    01.png

    其中有 56 bit 用于存储 AutoreleasePoolPage 的成员变量,剩下的 0x100816038 ~ 0x100817000 都是用来存储加入到自动释放池中的对象。

        id * begin() {
            return (id *) ((uint8_t *)this+sizeof(*this));
        }
    
        id * end() {
            return (id *) ((uint8_t *)this+SIZE);
        }
    
    • begin() 和 end() 这两个实例方法帮助我们快速获取 0x100816038 ~ 0x100817000 这一范围的边界地址。

    • next 指向了下一个为空的内存地址。

    从AutoreleasePoolPage 的结构总可以看到POOL_SENTINEL对象。称之为哨兵对象。

    #define POOL_SENTINEL nil
    
    POOL_SENTINEL 哨兵对象的作用

    每个自动释放池初始化在调用objc_autoreleasePoolPush的时候,都会把一个POOL_SENTINEL push到自动释放池的栈顶,并且返回这个POOL_SENTINEL的地址。

    int main(int argc, const char * argv[]) {
        {
          // atautoreleasepoolobj指向的是哨兵对象
            void * atautoreleasepoolobj = objc_autoreleasePoolPush();
            
            // do whatever you want
            // 传入哨兵对象地址
            objc_autoreleasePoolPop(atautoreleasepoolobj);
        }
        return 0;
    }
    

    上面这个atautoreleasepoolobj就是一个POOL_SENTINEL。
    可以看到在调用objc_autoreleasePoolPop时,会传进去这个地址:

    • 根据传入的哨兵对象地址找到哨兵对象所处的page
    • 从最新加入的对象一直向前清理,可以向前跨越若干个page,直到哨兵所在的page,在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次release消息,并向回移动next指针到正确位置。

    回到objc_autoreleasePoolPush的实现上来,可以看到objc_autoreleasePoolPush内部调用的是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;
        }
    

    DebugPoolAllocation是调试模式,所以我们只要看autoreleaseFast函数。
    可以看到调用autoreleaseFast函数时传入了 POOL_BOUNDARY哨兵对象。

    autoreleaseFast函数的实现如下:
        static inline id *autoreleaseFast(id obj)
        {
            //1 获取hotPage
            AutoreleasePoolPage *page = hotPage();
    
            if (page && !page->full()) {
                // 2.1 有 hotPage 并且当前 page 不满
                // 调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中
                return page->add(obj);
                
            } else if (page) {
                // 2.2  有 hotPage 并且当前 page 已满
                // 调用 autoreleaseFullPage 初始化一个新的页
                // 调用 page->add(obj) 方法将对象添加至新的 AutoreleasePoolPage 的栈中
                return autoreleaseFullPage(obj, page);
            } else {
                //2.3  无 hotPage
                // 调用 autoreleaseNoPage 创建一个 hotPage
                // 调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中
                return autoreleaseNoPage(obj);
            }
        }
    
    

    从上面的注释可以看到autoreleaseFast跟根据hotpage()执行不同的流程

    • 1.有 hotPage 并且当前 page 不满

      调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中

      1. 有 hotPage 并且当前 page 已满

      调用 autoreleaseFullPage 初始化一个新的页
      调用 page->add(obj) 方法将对象添加至新的 AutoreleasePoolPage 的栈中

    • 3.无 hotPage

      调用 autoreleaseNoPage 创建一个 hotPage
      调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中

    当hotpPage 装满时,会调用autoreleaseFullPage函数,实现如下:

        // 如果当前的hotpage已经装满
        static __attribute__((noinline))
        id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
        {
            // 对传入的page对象条件检查,必须是hotpage,必须full
            // 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.
            // page 为hotpage
            ASSERT(page == hotPage());
            //  page full
            ASSERT(page->full()  ||  DebugPoolAllocation);
            // 通过page的 child指针,获取下一个page对象,如果下一个page对象为空,则调用AutoreleasePoolPage创建一个新的page对象
            // while 循环判断page,直到page 没有装满
            do {
                if (page->child) page = page->child;
                else page = new AutoreleasePoolPage(page);
            } while (page->full());
            // 将while循环得到的page对象设置为hotpage
            setHotPage(page);
            
            // 将obj对象加入到page中
            return page->add(obj);
        }
    
    

    从上面的源码中可以看出autoreleaseFullPage的实现逻辑:

      1. 传入的page必须是hotpage,必须full。
      1. 通过 child指针,while遍历,从当前page寻找,直到获取到一个没有装满的page,或者创建一个新的page。
    • 3.将while遍历获取到的page设置为hotPage,并且将obj加入到page中

    在上面的autoreleaseFast函数中我们通过hotPage()函数获取到了最上层的page。

    hotPage 实现
    static inline AutoreleasePoolPage *hotPage()
    {
        AutoreleasePoolPage *result = (AutoreleasePoolPage *)
            tls_get_direct(key); // 通过 tls 查询可用 AutoreleasePoolPage 对象
        if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil; // 如果查询结果为 EMPTY_POOL_PLACEHOLDER,返回 nil
        if (result) result->fastcheck(); // 如果 result 不为空,则调用 fastcheck 方法,这里面做了检测线程的工作
        return result;
    }
    
    自动释放池 full 实现
      bool full() { 
            return next == end();
        }
    
        id * end() {
            return (id *) ((uint8_t *)this+SIZE);
        }
    

    判断next指向是不是最后一个位置。

    page->add(obj)实现
    
        // 将对象添加到自动释放池中,压栈操作
        id *add(id obj)
        {
            // page没有装满
            ASSERT(!full());
            unprotect();
            // 获取当前的位置
            id *ret = next;  // faster than `return next-1` because of aliasing
            //将下一个位置指向obj
            *next++ = obj;
            protect();
            return ret;
        }
    
    
    autoreleaseNoPage实现
    //创建AutoreleasePoolPage
        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);
                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.
                
           // 初始化第一个AutoreleasePoolPage,parent为nil
            // Install the first page.
            AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
            //设置为hotpage
            setHotPage(page);
            
            // Push a boundary on behalf of the previously-placeholder'd pool.
            // 先添加POOL_BOUNDARY 哨兵对象
            if (pushExtraBoundary) {
                page->add(POOL_BOUNDARY);
            }
            
            // 再将对象加入到自动释放池
            // Push the requested object or pool.
            return page->add(obj);
        }
    

    既然当前内存中不存在 AutoreleasePoolPage,就要从头开始构建这个自动释放池的双向链表,也就是说,新的 AutoreleasePoolPage 是没有 parent 指针的。

    初始化之后,将当前页标记为 hotPage,然后会先向这个 page 中添加一个 POOL_SENTINEL 对象,来确保在 pop 调用的时候,不会出现异常。

    最后,将 obj 添加到自动释放池中。

    三. objc_autoreleasePoolPop 方法

    自动释放池结束时,会调用析构函数objc_autoreleasePoolPop。objc_autoreleasePoolPop实现如下:

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

    通过runtime源码发现,最后调用的是AutoreleasePoolPage的pop函数,传入的参数是POOL_SENTINEL哨兵对象

    pop函数实现
     // 参数token:POOL_BOUNDARY的地址
        static inline void
        pop(void *token)
        {
            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.
                    // page  为空,自动释放池还未使用,清空placeholder
                    return setHotPage(nil);
                }
                // Pool was used. Pop its contents normally.
                // Pool pages remain allocated for re-use as usual.
                // 将page设置为cold page
                page = coldPage();
                // token指向起始位置
                token = page->begin();
            } else {
                //通过POOL_BOUNDARY找到对应的page
                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);
            }
            // 调用popPage
            return popPage<false>(token, page, stop);
        }
    

    在 pageForPointer函数中,通过传入的token(哨兵对象的地址),获取到对应的page,然后执行popPage函数释放池中的对象

    如何通过token找到当前page

    在上面说到通过pageForPointer函数能获取当前page,实现如下:

        // 通过哨兵对象的地址找到对应的page
        static AutoreleasePoolPage *pageForPointer(const void *p) 
        {
            return pageForPointer((uintptr_t)p);
        }
    
        static AutoreleasePoolPage *pageForPointer(uintptr_t p) 
        {
            // p :POOL_BOUNDARY 在对应page中的地址
            AutoreleasePoolPage *result;
            
            //SIZE: 4096,每页page的大小
            // p % SIZE : 获取p 在page中的偏移地址
            uintptr_t offset = p % SIZE;
    
            // 边界检测: 偏移地址 <= page 的内存大小
            ASSERT(offset >= sizeof(AutoreleasePoolPage));
            
            //获取p所在page的起始地址
            result = (AutoreleasePoolPage *)(p - offset);
            result->fastcheck();
    
            return result;
        }
    

    从源码中可以看到 将指针与页面的大小,也就是 4096 取模,得到当前指针的偏移量,然后用指针地址减去偏移量就是page的起始地址

    我们获取到了当前的page,就要调用 popPage去释放自动释放池中的对象了。

    popPage实现
     template<bool allowDebug>
        static void
        popPage(void *token, AutoreleasePoolPage *page, id *stop)
        {
            if (allowDebug && PrintPoolHiwat) printHiwat();
            
            //释放page中的对象
            page->releaseUntil(stop);
    
            // memory: delete empty children 删除空的子page
            // 1.当前page为空,直接kill掉当前page,然后把parent设置为hotpage
            if (allowDebug && DebugPoolAllocation  &&  page->empty()) {
                // special case: delete everything during page-per-pool debugging
                // 取出parent page
                AutoreleasePoolPage *parent = page->parent;
                // 释放当前page
                page->kill();
                // 设置parent page 为hotPage
                setHotPage(parent);
            } else if (allowDebug && DebugMissingPools  &&  page->empty()  &&  !page->parent) {
                // special case: delete everything for pop(top)
                // when debugging missing autorelease pools
                // 2. 当前page为空,而且没有parent,kill掉当前page,hotpage置为空;
                page->kill();
                setHotPage(nil);
            } else if (page->child) {
                // hysteresis: keep one empty child if page is more than half full
                //3.当前page不为空,但是有child
                
                // 如果当前page 的使用不超过一半
                if (page->lessThanHalfFull()) {
                    // 从子 page开始释放
                    page->child->kill();
                }
                else if (page->child->child) {
                    // 如果当前page的使用超过一半,并且还有 子子child(孙子 child),则从孙子child开始释放
                    // 这里如果当前的page使用已经超过一半,意味着当前page可能很快就要用完,这个时候就保留 child page,从孙子page开始释放。创建page需要时间,这里牺牲一个page的内存空间,提升了新能
                    page->child->child->kill();
                }
            }
        }
    

    1.当前page为空,直接kill掉当前page,然后把parent设置为hotpage;
    2.当前page为空,而且没有parent,kill掉当前page,hotpage置为空;
    3.当前page不为空,但是有child,如果当前page的空间占用不到一半,释放child,如果当前page的空间占用超过一半,且child还有child,直接释放这个孙子辈的page。这里牺牲一个page的内存空间,提升了新能

    releaseUntil 释放对象

    releaseUntil 方法的实现如下:

     // 循环释放对象,直到stop
        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循环,直到next == 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为空,利用parent指针找到一个不为空的page,并设置为hotpage
                while (page->empty()) {
                    page = page->parent;
                    setHotPage(page);
                }
                
                //将page所在的内存区域设置为可读可写
                page->unprotect();
                //通过next指针获取page中记录的对象,next -1 前移
                id obj = *--page->next;
                // void *memset(void *s, int ch, size_t n);
                // 将s中当前位置后面的n个字节 (typedef unsigned int size_t )用 ch 替换并返回 s 。
                // 将当前对象的 8个字节 用0xA3 替换
                memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
                // 修改完page中的值后,设置page所在的内存区域为只读
                page->protect();
                
                // 如果对象不是哨兵对象则释放
                if (obj != POOL_BOUNDARY) {
                    objc_release(obj);
                }
            }
            // 把当前 page 设置 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
        }
    

    使用while循环释放page中的内容,直到next == stop,通过memset底层函数,将释放的地方用0xA3常量填充替换。

    releaseUntil函数只是将page中的对象 释放了,并且对应的位置用0xA3填充,但是child/parent 指针没有清空,也就是说page还在内存中,没有释放。释放page的操作在kill函数中进行的

    kill实现

    kill 方法的实现如下:

    // 清空child指针,释放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;
            // 通过while获取尾部的page
            while (page->child) page = page->child;
    
            AutoreleasePoolPage *deathptr;
            do {
                deathptr = page;
                page = page->parent;
                if (page) {
                    page->unprotect();
                    // 清空child 指针,释放子page
                    page->child = nil;
                    page->protect();
                }
                delete deathptr;
            } while (deathptr != this);
        }
    

    kill会将当前页面以及子页面全部删除,释放AutoreleasePoolPage占用空间,是从最尾部的子page开始释放

    调试自动释放池

    可以通过_objc_autoreleasePooPrint()函数调试

    // 声明
    extern void  _objc_autoreleasePooPrint();
    // 执行函数
    _objc_autoreleasePoolPrint()
    
    参考

    自动释放池的前世今生 ---- 深入解析
    黑幕背后的Autorelease
    iOS 底层拾遗:AutoreleasePool

    相关文章

      网友评论

          本文标题:ios自动释放池

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