• 【iOS】—— autoreleasePool以及总结


    1. 什么是autoreleasePool

    AutoreleasePool(自动释放池)是在Objective-CSwift中用于管理内存释放的机制。通过创建自动释放池,可以将需要延迟释放的对象放入其中,在自动释放池被销毁时,其中的对象会被释放,从而帮助避免内存泄漏并优化内存管理。

    大概的意思就是:

    • 自动释放池是栈结构,存储的是指针。
    • 指针指向需要自动释放的对象或者 POOL_BOUNDARY 边界值。以 POOL_BOUNDARY 为边界,当释放池释放时,在边界之内的对象会被释放。
    • page 以双向链表的形式构成 pool,page 会自动创建或释放。
    • 线程本地存储指向当前最新的 page。

    2. autoreleasePoolPage

    每个autoreleasePool都是由一系列autoreleasePoolPage组成的,并且每个autoreleasePoolPage大小都为4096字节,相关源码如下:

    #define I386_PGBYTES 4096
    #define PAGE_SIZE I386_PGBYTES
    
    class AutoreleasePoolPage {
        magic_t const magic;//AutoreleasePoolPage 完整性校验
        id *next;//下一个存放autorelease对象的地址
        pthread_t const thread; //AutoreleasePoolPage 所在的线程
        AutoreleasePoolPage * const parent;//父节点
        AutoreleasePoolPage *child;//子节点
        uint32_t const depth;//深度,也可以理解为当前page在链表中的位置
        uint32_t hiwat;
    }
    
    

    自动释放池其实就是一个由AutoreleasePoolPage构成的双向链表,其结构中的childparent分别指向其前趋和后继。
    在这里插入图片描述
    单个AutoreleasePoolPage结构如下:
    在这里插入图片描述

    其中有 56 bit 用于存储AutoreleasePoolPage的成员变量。

    • 该结构体的第一个成员变量是magic,我们在isa中也学习过,isa中是分判对象是否未完成初始化,在这里也一样,用来检查这个节点是否已经被初始化了。
    • begin()和end()这两个类的实例方法帮助我们快速获取 0x100816038 ~ 0x100817000 这一范围的边界地址。
    • next:指向下一个为空的内存地址,如果next指向的地址加入一个object,它就会如下图所示移动到下一个为空的内存地址中,就像栈顶指针一样。
    • thread:保存了当前页所在的线程。
    • depth:表示page的深度,首次为0,每个page的大小都是4096字节(16进制0x1000),每次初始化一个page,depth都加一。
    • POOL_BOUNDARY:就是哨兵对象,它只是nil的别名,用于分隔Autoreleasepool。POOL_BOUNDARY直译过来就是POOL的边界。它的作用是隔开page中的对象。因为并不是每次push与pop之间存进的对象都刚好占满一个page,可能会不满,可能会超过,因此这个POOL_BOUNDARY帮助我们分隔每个@autoreleasepool块之间的对象。也就是说这个page可能存储很多个@autoreleasepool块的对象,使用POOL_BOUNDARY来隔开每个@autoreleasepool块的对象。
     #define POOL_BOUNDARY nil
    
    
    objc_autoreleasePoolPush方法:
     void *objc_autoreleasePoolPush(void) {
        return AutoreleasePoolPage::push();
    }
    
    

    这里调用了AutoreleasePoolPage::push()方法:

     static inline void *push() 
    {
        id *dest;
        // POOL_BOUNDARY就是nil
        // 首先将一个哨兵对象插入到栈顶
        if (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指的是当前正在使用的AutoreleasePoolPage

    static inline id *autoreleaseFast(id obj)
    {
       AutoreleasePoolPage *page = hotPage();
       if (page && !page->full()) {//有 hotPage 并且当前 page 不满,将object加入当前栈中
           return page->add(obj);
       } else if (page) {//有hotPage 但当前page已满,找未满页或创建新页,将object添加到新页中
           return autoreleaseFullPage(obj, page);
       } else {//无hotPage,创建hotPage,加入其中
           return autoreleaseNoPage(obj);
       }
    }
     
    

    有hotPage但当前page未满,直接调用page->add(obj)方法将对象添加到自动释放池中。

    // 这其实就是一个压栈操作,将对象加入AutoreleasePoolPage,然后移动栈顶指针
    id *add(id obj) {
        id *ret = next;
        *next = obj;
        next++;
        return ret;
    }
     
    

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

    有hotPage但当前page已满,(找到未满页或者创建新页,将object添加到新页中autoreleaseFullPage当前page满的时候调用)

    static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) {
    //一直遍历,直到找到一个未满的 AutoreleasePoolPage,如果找到最后还没找到,就新建一个 AutoreleasePoolPage
        do {
            if (page->child) 
            	page = page->child;
            else page = new AutoreleasePoolPage(page);
        } while (page->full());
    	
    	//将找到的,或者构建的page作为hotPage,然后将obj加入
        setHotPage(page);
        return page->add(obj);
    }
    
    

    从传入的 page 开始遍历整个双向链表,直到查找到一个未满的 AutoreleasePoolPage

    如果找到最后还是没找到创建一个新的 AutoreleasePoolPage

    将找到的或者构建的page标记成 hotPage,然后调动上面分析过的 page->add 方法添加对象。

    无hotPage,创建hotPage,加入其中:

    这个时候,由于内存中没AutoreleasePoolPage,就要从头开始构建这个自动释放池的双向链表,那么当前页表作为第一张页表,是没有parent指针的。并且我们在第一次创建page时其首位都是要加POOL_SENTINEL标识的,方便让page知道在哪就结束了。

     static id *autoreleaseNoPage(id obj) {
        AutoreleasePoolPage *page = new AutoreleasePoolPage(nil); // 创建AutoreleasePoolPage
        setHotPage(page); // 设置page为当前页
     
        if (obj != POOL_SENTINEL) { // 加POOL_SENTINEL哨兵
            page->add(POOL_SENTINEL);
        }
     
        return page->add(obj); // 将obj加入
    }
    
    
    objc_autoreleasePoolPop方法:
     void objc_autoreleasePoolPop(void *ctxt) {
        AutoreleasePoolPage::pop(ctxt);
    }
    

    其调用的pop方法如下:

     static inline void pop(void *token) {
        AutoreleasePoolPage *page = pageForPointer(token);//使用 pageForPointer 获取当前 token 所在的 AutoreleasePoolPage
        id *stop = (id *)token;
    
        page->releaseUntil(stop);//调用 releaseUntil 方法释放栈中的对象,直到 stop 位置,stop就是传递的参数,一般为哨兵对象
    
    	//调用 child 的 kill 方法,系统根据当前页的不同状态kill掉不同child的页面
    	//releaseUntil把page里的对象进行了释放,但是page本身也会占据很多空间,所以要通过kill()来处理,释放空间
        if (page->child) {
            if (page->lessThanHalfFull()) { // 当前page小于一半满
                page->child->kill(); // 把当前页的孩子杀掉
            } else if (page->child->child) { // 否则,留下一个孩子,从孙子开始杀
                page->child->child->kill();
            }
        }
    }
    
    

    假设当前page一半都没满,说明剩余的page空间已经暂时够了,把多余的儿子page就可以kill掉,如果超过一半页,就认为下一半page还有存在的必要,所以kill孙子page,保留一个儿子page。

    token
    • token是指向该pool的POOL_BOUNDARY指针
    • token的本质就是指向哨兵对象的指针,存储着每次push时插入的POOL_BOUNDARY的地址
    • 只有第一次push的时候会在page中插入一个POOL_BOUNDARY【或者page满了,或者没有hotPage需要使用新的page了】,并不是page的开头都一定是POOL_BOUNDARY
    kill()方法
    void kill() {
        AutoreleasePoolPage *page = this; // 获取当前页
        while (page->child) page = page->child; // child存在就一直往下找,直到找到一个不存在的
    
        AutoreleasePoolPage *deathptr;
        do {
            deathptr = page;
            page = page->parent;
            if (page) {
                page->unprotect();
                page->child = nil; // 将其child指向置nil,防止出现悬垂指针
                page->protect();
            }
            delete deathptr; // 删除
        } while (deathptr != this); // 直到this处停止
    }
     
    

    3. 总结

    3.1 autoreleasePool的原理

    自动释放池本质是autoreleasePoolPage结构体对象,是一个以栈结构存储的页,每一个autoreleasePoolPage都是以双向链表的形式来连接起来。

    大小可根据宏定义查得数值为:4096字节

    自动释放池的出栈和入栈主要通过objc_autoreleasePoolPushobjc_autoreleasePoolPop,实际上调用的是autoreleasePoolPagepushpop方法。

    **push操作:**每次调用push操作都会创建一个新的autoreleasePoolPage,而autoreleasePoolPagePush的具体操作就是插入一个POOL BOUNDARY,并且返回插入POOL BOUNDARY的内存地址。在push中的操作,需要调用autoreleaseFast方法处理,具体情况分下面三个:

    • page存在并且未满,直接调用add方法将对象添加到page的next指针处,next指针递增。
    • page存在并且已满,需要调用autoreleaseFullPage,初始化一个新的page,然后通过add方法添加。
    • page不存在,需要先调用autoreleaseNoPage,创建一个hotPage,然后调用add方法添加对象到栈中。

    **pop操作:**当执行pop操作的时候,会传入一个参数,这个参数是push操作的返回值,也就是POOLBOUNDARY的内存地址token,因此pop操作,就是根据token找到哨兵对象的位置,然后objc_release释放token之前的对象,把next指针指到正确的位置。

    POOL_BOUNDARY是一种特殊标记,用于标记池的边界,就是说这个page可能存储很多个@autoreleasepool块的对象,在早期可能是一个特殊的指针或者整数值;在现代时,POOL_BOUNDARY可能是一个特殊的结构体或者无效指针。

    整体思路:

    通过push创建一个autoreleasePoolPage对象,会在开始的位置存放POOL BOUNDARY哨兵对象,然后将注册了autorelease的对象添加到存储到里面,调用pop的时候,会根据push返回的token的位置,释放到token之前的位置。

    3.2 autoreleasePool的问题

    3.2.1 autoreleasepool的嵌套操作

    可以使用嵌套操作,其目的是控制应用程序的内存峰值,是其不要太高。

    嵌套原因:自动释放池是以栈为节点,通过双向链表的形式链接,且与线程一一对应。

    3.2.2 autoreleasepool的释放时机

    在没有手动添加autoreleasePool的情况下,autorelease对象是在当前的runloop迭代结束的时候释放。

    1. 在准备进入runloop的时候,创建一个autoreleasePool(优先级最高)。
    2. runloop准备休眠的时候,会先释放掉autoreleasePool(优先级最低),然后创建一个新的autoreleasePool
    3. 在退出runloop的时候,就释放掉autoreleasePool
    3.2.3 那些对象可以加入到autoreleasePool中
    1. 在MRC情况下,使用new,copy,alloc关键字修饰的对象或者retain持有的对象,不能加入到自动释放池中。
    2. MRC中设置为autorelease的对象,不需要手动,自动加入到autoreleasePool中。
    3. 所有autorelease的对象,在出了作用域之后,会被自动添加到最近创建的自动释放池中。
    3.2.4 关于哨兵对象和next指针

    next指针只有一个,永远指向下一个能存放autoreleasepool的地址,而哨兵对象有很多个,每个autoreleasepool都对应一个哨兵对象,标示这个autoreleasepool对象从哪里开始存。

    3.2.5 next和child:

    next指向下一个能存放autoreleasepool对象的地址,child是autoreleasePoolPage的参数,指向下一个page。

    3.2.6 thread 和 AutoreleasePool的关系

    每个线程都有与之关联的自动释放池堆栈结构,新的pool在创建时会被压栈到栈顶,pool销毁时,会被出栈,对于当前线程来说,释放对象会被压栈到栈顶,线程停止时,会自动释放与之关联的自动释放池

    3.2.7 RunLoop 和 AutoreleasePool的关系

    主程序的RunLoop在每次事件循环之前之前,会自动创建一个 autoreleasePool 并且会在事件循环结束时,执行drain操作,释放其中的对象

  • 相关阅读:
    申请流量卡时,运营商到底审核什么?
    基于Radon滤波反投影算法的CT图像重建matlab仿真
    点云从入门到精通技术详解100篇-三维文物点云去噪与精简方法研究与应用(下)
    有点无聊,来试试用Python采集下载漫画
    RabbitMQ核心总结
    微信小程序实现音乐播放器(5)
    Git学习笔记
    Leetcode算法入门与数组丨5. 数组二分查找
    C. Color the Picture(贪心/构造)
    【C++入门到精通】C++入门 ——搜索二叉树(二叉树进阶)
  • 原文地址:https://blog.csdn.net/qq_72437394/article/details/140966859