zoukankan      html  css  js  c++  java
  • AutoreleasePool 的总结

    1.创建和释放时机问题

    App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()

    第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

    第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647(2^31-1
    ),优先级最低,保证其释放池子发生在其他所有回调之后。

    在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。

    2.AutorealeasePool内部结构

    是一条双向链表,每个节点都是一个AutoreleasePoolPage

    3.AutoreleasePoolPage的结构

    AutoreleasePoolPage 是一个 C++ 中的类:

    class AutoreleasePoolPage {
        magic_t const magic; //用于对当前 AutoreleasePoolPage 完整性的校验
        id *next;  // 下一个可插入对象的可用地址,如果 next 指向的地址加入一个 object,它就会如下图所示移动到下一个为空的内存地址中:
        pthread_t const thread; // 当前的线程
        AutoreleasePoolPage * const parent; // 父指针
        AutoreleasePoolPage *child; // 子指针
        uint32_t const depth;
        uint32_t hiwat;
    };

    每一个自动释放池都是由一系列的 AutoreleasePoolPage 组成的,并且每一个 AutoreleasePoolPage 的大小都是 4096 字节(16 进制 0x1000)

    4.POOL_BOUNDARY(哨兵对象)

    也有人称作 POOL_SENTINEL

    POOL_BOUNDARY 只是 nil 的别名,

    - 在每个自动释放池初始化调用 objc_autoreleasePoolPush 的时候,都会把一个 POOL_BOUNDARY push 到自动释放池的栈顶,并且返回这个 POOL_BOUNDARY 哨兵对象。

    5.objc_autoreleasePoolPush 方法

    void *objc_autoreleasePoolPush(void) {
        return AutoreleasePoolPage::push();
    }
    
    static inline void *push() {
       return autoreleaseFast(objc);
    }

    在这里会进入一个比较关键的方法 autoreleaseFast,第一次传入哨兵对象 POOL_SENTINEL

    static inline id *autoreleaseFast(id obj)
    {
       AutoreleasePoolPage *page = hotPage();
       if (page && !page->full()) {//page没有满,就把obj对象加到page  
    return page->add(obj);

    } else if (page) {
    return autoreleaseFullPage(obj, page);
       } else {
           return autoreleaseNoPage(obj);
       }
    }
    //有 hotPage 并且当前 page 已满
    static
    id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) { do { if (page->child) page = page->child; else page = new AutoreleasePoolPage(page); } while (page->full()); setHotPage(page); return page->add(obj); }

    //无 hotPage
    static id *autoreleaseNoPage(id obj) { AutoreleasePoolPage *page = new AutoreleasePoolPage(nil); setHotPage(page); if (obj != POOL_SENTINEL) { page->add(POOL_SENTINEL); } return page->add(obj);

    上述方法分三种情况选择不同的代码执行:

    • 有 hotPage 并且当前 page 不满
      • 调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中
    • 有 hotPage 并且当前 page 已满
      • 1.它会从传入的 page 开始遍历整个双向链表,查找到一个未满的 AutoreleasePoolPage,
      • 2.调用 autoreleaseFullPage 初始化一个新的页
      • 调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中
    • 无 hotPage
      • 调用 autoreleaseNoPage 创建一个 hotPage

      • 如果obj == POOL_SENTINEL,则直接page->add(obj) 将对象添加至 AutoreleasePoolPage 的栈中

      • 如果obj != POOL_SENTINEL,则先加入POOL_SENTINEL哨兵对象,再调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中

    最后的都会调用 page->add(obj) 将对象添加到自动释放池中。

    hotPage 可以理解为当前正在使用的 AutoreleasePoolPage

    6.objc_autoreleasePoolPop 方法

    static inline void pop(void *token) {
        AutoreleasePoolPage *page = pageForPointer(token);
        id *stop = (id *)token;
    
        page->releaseUntil(stop);
    
        if (page->child) {
            if (page->lessThanHalfFull()) {
                page->child->kill();
            } else if (page->child->child) {
                page->child->child->kill();
            }
        }
    }

    该静态方法总共做了三件事情:

    1. 使用 pageForPointer 获取当前 token(哨兵对象), 所在的 AutoreleasePoolPage
    2. 调用 releaseUntil 方法释放栈中的对象,直到 stop,
    3. 调用 child 的 kill 方法

    - pageForPointer 获取 AutoreleasePoolPage

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

    将指针与页面的大小,也就是 4096 取模,得到当前指针的偏移量,因为所有的 AutoreleasePoolPage 在内存中都是对齐的:

    p = 0x100816048
    p % SIZE = 0x48
    result = 0x100816000
    

    而最后调用的方法 fastCheck() 用来检查当前的 result 是不是一个 AutoreleasePoolPage

    - releaseUntil 释放对象

    它的实现还是很容易的,用一个 while 循环持续释放 AutoreleasePoolPage 中的内容,直到 next 指向了 stop(哨兵对象) 。

    使用 memset 将内存的内容设置成 SCRIBBLE,然后使用 objc_release 释放对象

    -  kill() 方法

    函数releaseUntil ,它在释放的时候其实会一直顺着parent往前释放,只到找到参数stop传入的地址,也就是说可能一次性释放好几个page

    1. 当前page为空,直接kill掉当前page,然后把parent设置为hotpage
    2. 当前page为空,而且没有parent,kill掉当前pagehotpage置为空;
    3. 当前page不为空,但是有child,如果当前page的空间占用不到一半,释放child,如果当前page的空间占用超过一半,且child还有child,直接释放这个孙子辈的page。(对于第三步注释中的解释是:keep one empty child if page is more than half fully)

    注意:

    1. releaseUntil 做的工作是擦除AutoreleasePoolPage所占用的内存区域的内容(通过memset),但是并没有置空child或者parent指针

    2. kill做的工作是置空已经被releaseUntil是放过的page的child的,也就是释放child所指向的AutoreleasePoolPage占用空间

    if (page->lessThanHalfFull()) {   
    
          page->child->kill();
    }
     
    当前page的空间占用少于一半,释放掉child page占用的空间
    else if (page->child->child) {
        page->child->child->kill();
    }
    
    当前page的空间占用超过一半的,因为很可能会满了,就不释放child page的占用空间了,为了预留一个child page,满了的话我们就不用创建一个page再置为自己的child了,但是只要预留一页就够了,所以释放掉child的child,也就是只保留page和page+1页


    补充:
    系统 C函数: 使用extern 可以调用其他类的c函数
     
    extern void _objc_autoreleasePoolPrint(void);
     
    可以使用,用于打印autoreleasepool 存放的对象


    参考博客:https://draveness.me/autoreleasepool/#AutoreleasePoolPage

  • 相关阅读:
    牛客网 剑指Offer JZ19 顺时针打印矩阵
    牛客网 剑指Offer Z18 二叉树的镜像
    牛客网 剑指Offer JZ17 树的子结构
    一键安装脚本(MySQL双主、EMQX集群、FastDFS集群)
    API和拦截器相关的设计简记
    C# 获取今天,昨天,上周,下周,上月,下月等等一些日期格式
    Element.scrollIntoView()
    自主学习(1)--matlab
    四则运算java
    学习计划
  • 原文地址:https://www.cnblogs.com/liuwenqiang/p/13220536.html
Copyright © 2011-2022 走看看