美文网首页
Autorelease背后的机制

Autorelease背后的机制

作者: skogt | 来源:发表于2019-05-17 11:39 被阅读0次

    autorelease机制是iOS管理对象内存的好伙伴,在MRC时代,我们通过[obj autorelease]来延迟释放内存,到了ARC时代,我们甚至可以不用知道autorelease就可以管理好内存。那么,objc和编译器到底做了什么,帮助我们管理好内存呢?autorelease背后的机制到底是什么呢?

    在说autorelease之前,大家可能需要大概了解下runloop相关的知识,纳尼?autorelease还跟runloop有关联?答案是肯定的,iOS很多内容都跟runloop有千丝万缕的关联,谁也离不开谁。app启动之后,苹果在主线程runloop里注册了两个observer,第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 优先级最高,保证创建释放池发生在其他所有回调之前。
    第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的优先级最低,保证其释放池子发生在其他所有回调之后,正因为这套机制的存在,我们不需要关心autorelease的存在。有兴趣的可以拜读下深入理解runloop这篇文章,反正我是已经读了好几遍了...

    通过上述的简答,相信我们已经有了大致的了解,那么背后的机制到底是什么呢?下面我们来慢慢揭开。

    Autorelease原理

    AutoreleasePoolPage

    ARC下,我们使用@autoreleasepool{}来使用一个AutoreleasePool,

    @autoreleasepool {
        NSString *hello = @"hello world";
    }
    

    随后编译器将其改写成下面的样子:

    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
            NSString *hello = (NSString *)&__NSConstantStringImpl__var_folders_g7_bldz1yr55832skz1w_v3ygj40000gn_T_ViewController_69a444_mi_0;
        }
    

    其中的关键就在于__AtAutoreleasePool,然后我们找到相应的内容,这时我们发现了autorelease最核心的两个函数objc_autoreleasePoolPush,objc_autoreleasePoolPop

    extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void);
    extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *);
    
    struct __AtAutoreleasePool {
      __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
      ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
      void * atautoreleasepoolobj;
    };
    

    那么上述的两个函数干了什么呢,通过苹果开源的源码
    ,我们定位到这两个函数其实是在操作AutoreleasePoolPage这个类,其中AutoreleasePoolPage定义如下

    class AutoreleasePoolPage 
    {
        #if PROTECT_AUTORELEASEPOOL
            4096;  // must be multiple of vm page size
        #else
            4096;  // size and alignment, power of 2
        #endif
        magic_t const magic;
        id *next;
        pthread_t const thread;
        AutoreleasePoolPage * const parent;
        AutoreleasePoolPage *child;
        uint32_t const depth;
        uint32_t hiwat;
    }
    

    通过这个类,我们可以看到AutoreleasePool并没有单独的结构,而是由若干个AutoreleasePoolPage以双向链表
    的形式组合而成(分别对应结构中的parent指针和child指针)

    • AutoreleasePool是按线程一一对应的(结构中的thread指针指向当前线程)
    • AutoreleasePoolPage每个对象会开辟4096字节内存(也就是虚拟内存一页的大小),除了上面的实例变量所占空间,剩下的空间全部用来储存autorelease对象的地址
    • 上面的id *next指针作为游标指向栈顶最新add进来的autorelease对象的下一个位置
    • 一个AutoreleasePoolPage的空间被占满时,会新建一个AutoreleasePoolPage对象,连接链表,后来的autorelease对象在新的page加入

    所以,若当前线程中只有一个AutoreleasePoolPage对象,并记录了很多autorelease对象地址时内存如下图:

    图中的情况,这一页再加入一个autorelease对象就要满了(也就是next指针马上指向栈顶),这时就要执行上面说的操作,建立下一页page对象,与这一页链表连接完成后,新page的next指针被初始化在栈底(begin的位置),然后继续向栈顶添加新对象。

    static inline void *push() 
    {
        if (!hotPage()) {
            setHotPage(new AutoreleasePoolPage(NULL));
        } 
        id *dest = autoreleaseFast(POOL_SENTINEL);
        assert(*dest == POOL_SENTINEL);
        return dest;
    }
    
    static inline id *autoreleaseFast(id obj)
    {
        AutoreleasePoolPage *page = hotPage();
        if (page && !page->full()) {
            return page->add(obj);
        } else {
            return autoreleaseSlow(obj);
        }
    }
    
    id *add(id obj)
    {
        assert(!full());
        unprotect();
        *next++ = obj;
        protect();
        return next-1;
    }
    

    所以,向一个对象发送- autorelease消息,就是将这个对象加入到当前AutoreleasePoolPage的栈顶next指针指向的位置

    释放时刻

    每当进行一次objc_autoreleasePoolPush调用时,runtime向当前的AutoreleasePoolPage中add进一个哨兵对象,值为0

    static inline void *push() 
    {
        if (!hotPage()) {
            setHotPage(new AutoreleasePoolPage(NULL));
        } 
        id *dest = autoreleaseFast(POOL_SENTINEL); // POOL_SENTINEL = 0 返回哨兵对象的地址
        assert(*dest == POOL_SENTINEL);
        return dest;
    }
    

    那么这一个page就变成了下面的样子:

    objc_autoreleasePoolPush的返回值正是这个哨兵对象的地址,被objc_autoreleasePoolPop(哨兵对象)作为入参,于是:

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

    刚才的objc_autoreleasePoolPop执行后,最终变成了下面的样子:

    嵌套的AutoreleasePool

    知道了上面的原理,嵌套的AutoreleasePool就非常简单了,pop的时候总会释放到上次push的位置为止,多层的pool就是多个哨兵对象而已,就像剥洋葱一样,每次一层,互不影响。

    参考资料:
    https://blog.ibireme.com/2015/05/18/runloop/
    https://blog.sunnyxx.com/2014/10/15/behind-autorelease/
    https://opensource.apple.com/source/objc4/objc4-532/runtime/NSObject.mm.auto.html

    相关文章

      网友评论

          本文标题:Autorelease背后的机制

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