Autorelease背后的机制

2019-05-17  本文已影响0人  skogt

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指针)

所以,若当前线程中只有一个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

上一篇下一篇

猜你喜欢

热点阅读