Autorelease Pool备览

AutoreleasePool是什么

AutoreleasePool(自动释放池)是OC中的一种内存自动回收机制,它可以延迟加入AutoreleasePool中的变量release的时机。在正常情况下,创建的变量会在超出其作用域的时候release,但是如果将变量加入AutoreleasePool,那么release将延迟执行。

主线程AutoreleasePool创建和释放

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

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

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

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

也就是说AutoreleasePool创建是在一个RunLoop事件开始之前(push),AutoreleasePool释放是在一个RunLoop事件即将结束之前(pop)。
AutoreleasePool里的Autorelease对象的加入是在RunLoop事件中,AutoreleasePool里的Autorelease对象的释放是在AutoreleasePool释放时。

子线程AutoreleasePool创建和释放

前面我们已经知道,在AutoreleasePoolPage pop的时候释放autorelease对象,在主线程的runloop中,有两个oberserver负责创建和清空autoreleasepool,详情可以看YY的深入理解runloop

子线程的runloop都需要手动开启,那么子线程中中AutoreleasePool是何时创建和释放的呢?

如果当前线程没有AutorelesepoolPage的话,代码执行顺序为autorelease -> autoreleaseFast -> autoreleaseNoPage。
在autoreleaseNoPage方法中,会创建一个hotPage,然后调用page->add(obj)。也就是说即使这个线程没有AutorelesepoolPage,使用了autorelease对象时也会new一个AutoreleasepoolPage出来管理autorelese对象,不用担心内存泄漏。

明确了何时创建autoreleasepool以后就自然而然的有下一个问题,这个autoreleasepool何时清空?

对于这个问题,这里使用watchpoint set variable命令来观察。
首先是一个最简单的场景,创建一个子线程。

__weak id obj;
...
[NSThread detachNewThreadSelector:@selector(createAndConfigObserverInSecondaryThread) toTarget:self withObject:nil];

使用一个weak指针观察子线程中的autorelease对象,子线程中执行的任务。

- (void)createAndConfigObserverInSecondaryThread{
    __autoreleasing id test = [NSObject new];
    NSLog(@"obj = %@", test);
    obj = test;
    [[NSThread currentThread] setName:@"test runloop thread"];
    NSLog(@"thread ending");
}

在obj = test处设置断点使用watchpoint set variable obj命令观察obj,可以看到obj在释放时的方法调用栈是这样的。(缺图)

通过这个调用栈可以看到释放的时机在_pthread_exit。然后执行到AutorelepoolPage的tls_dealloc方法。这个方法如下

static void tls_dealloc(void *p)
{
    if (p == (void*)EMPTY_POOL_PLACEHOLDER) {
        // No objects or pool pages to clean up here.
        return;
    }

    // reinstate TLS value while we work
    setHotPage((AutoreleasePoolPage *)p);

    if (AutoreleasePoolPage *page = coldPage()) {
        if (!page->empty()) pop(page->begin());  // pop all of the pools
        if (DebugMissingPools || DebugPoolAllocation) {
            // pop() killed the pages already
        } else {
            page->kill();  // free all of the pages
        }
    }
    
    // clear TLS value so TLS destruction doesn't loop
    setHotPage(nil);
}

在这找到了if (!page->empty()) pop(page->begin());这句关键代码。再往上看一点,在_pthread_exit时会执行下面这个函数:

void
_pthread_tsd_cleanup(pthread_t self)
{
#if !VARIANT_DYLD
   int j;

   // clean up dynamic keys first
   for (j = 0; j < PTHREAD_DESTRUCTOR_ITERATIONS; j++) {
   	pthread_key_t k;
   	for (k = __pthread_tsd_start; k <= self->max_tsd_key; k++) {
   		_pthread_tsd_cleanup_key(self, k);
   	}
   }

   self->max_tsd_key = 0;

   // clean up static keys
   for (j = 0; j < PTHREAD_DESTRUCTOR_ITERATIONS; j++) {
   	pthread_key_t k;
   	for (k = __pthread_tsd_first; k <= __pthread_tsd_max; k++) {
   		_pthread_tsd_cleanup_key(self, k);
   	}
   }
#endif // !VARIANT_DYLD
}

也就是说thread在退出时会释放自身资源,这个操作就包含了销毁autoreleasepool,在tls_delloc中,执行了pop操作。
线程在销毁时会清空autoreleasepool。但是上述这个例子中的线程并没有加入runloop,只是一个一次性的线程。

至于子线程加入runloop的情况:

  • 如果子线程的RunLoop是由CF框架构建的话,自动释放池需要自己维护,否则将完全由线程接管。源码中的确没有这部分内容。
  • 如果是由Foundation框架构建的话,在runMode: (NSString*)mode beforeDate: (NSDate*)date方法中,其实是包裹了一个autoreleasepool的。如果在深入一些函数里面,发现其实很多地方都有autoreleasepool的函数,所以即使是我们自定义的source,执行函数中没有释放autoreleasepool的操作也不用担心,系统在各个关键入口都给我们加了这些操作。这部分可以参考GNU实现,也可以看苹果的汇编代码来验证。

AutoreleasePool实现原理

在终端中使用clang -rewrite-objc命令将下面的OC代码重写成C++的实现:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    
    @autoreleasepool {
        NSLog(@"Hello, World!");
    }
 
    return 0;
}

在cpp文件代码中我们找到main函数代码如下:

int main(int argc, const char * argv[]) {

    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_kb_06b822gn59df4d1zt99361xw0000gn_T_main_d39a79_mi_0);
    }

    return 0;
}

可以看到苹果通过声明一个__AtAutoreleasePool类型的局部变量__autoreleasepool实现了@autoreleasepool{}。
__AtAutoreleasePool的定义如下:

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;
};

根据构造函数和析构函数的特点(自动局部变量的构造函数是在程序执行到声明这个对象的位置时调用的,而对应的析构函数是在程序执行到离开这个对象的作用域时调用),我们可以将上面两段代码简化成如下形式:

int main(int argc, const char * argv[]) {

    /* @autoreleasepool */ {
        void *atautoreleasepoolobj = objc_autoreleasePoolPush();
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_kb_06b822gn59df4d1zt99361xw0000gn_T_main_d39a79_mi_0);
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }

    return 0;
}

至此,我们可以分析出,单个自动释放池的执行过程就是objc_autoreleasePoolPush() —> [object autorelease] —> objc_autoreleasePoolPop(void *)。
来看一下objc_autoreleasePoolPush 和 objc_autoreleasePoolPop 的实现:

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

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

上面的方法看上去是对 AutoreleasePoolPage 对应静态方法 push 和 pop 的封装。
下面分析一下AutoreleasePoolPage的实现,揭开AutoreleasePool的实现原理。

AutoreleasePoolPage实现

AutoreleasePoolPage介绍

AutoreleasePoolPage 是一个 C++ 中的类,它的结构图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jtGyRR9O-1652109849071)(https://files.jb51.net/file_images/article/201805/201853851001.png)]

AutoreleasePool是按线程一一对应的(结构中的thread指针指向当前线程)。

AutoreleasePool并没有单独的结构,而是由若干个AutoreleasePoolPage以双向链表的形式组合而成(分别对应结构中的parent指针和child指针)。

AutoreleasePoolPage在 NSObject.mm 中的定义是这样的:

class AutoreleasePoolPage {
#   define EMPTY_POOL_PLACEHOLDER ((id*)1)

#   define POOL_BOUNDARY nil
    static pthread_key_t const key = AUTORELEASE_POOL_KEY;
    static uint8_t const SCRIBBLE = 0xA3;  // 0xA3A3A3A3 after releasing
    static size_t const SIZE = 
#if PROTECT_AUTORELEASEPOOL
        PAGE_MAX_SIZE;  // must be multiple of vm page size
#else
        PAGE_MAX_SIZE;  // size and alignment, power of 2
#endif
    static size_t const COUNT = SIZE / sizeof(id);

    magic_t const magic;
    id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;
};
  • magic 检查校验完整性的变量
  • next 指向当前页中下一个可以存储变量的地址,初始为栈底
  • thread page当前所在的线程,AutoreleasePool是按线程一一对应的(结构中的thread指针指向当前线程)
  • parent 父节点 指向前一个page
  • child 子节点 指向下一个page
  • depth 链表的深度,节点个数
  • hiwat high water mark 数据容纳的一个上限
  • EMPTY_POOL_PLACEHOLDER 空池占位
  • POOL_BOUNDARY 是一个边界对象 nil,之前的源代码变量名是POOL_SENTINEL哨兵对象,用来区别每个Pool的边界,一个pool的范围可能覆盖多个AutoreleasePoolPage,一个AutoreleasePoolPage也可能放多个pool。
  • PAGE_MAX_SIZE = 4096, 为什么是4096呢?其实就是虚拟内存每个扇区4096个字节,4K对齐的说法。
  • COUNT 一个page里对象数

AutoreleasePoolPage的大小是固定的,一开始就会创建4K大小,即字段SIZE=PAGE_MAX_SIZE所表示的值。但是sizeof(*this)只计算到hiwat字段。

AutoreleasePoolPage通过operator new分配了SIZE-sizeof(*this)这么多内存来作为存放自动释放对象指针的栈。sizeof是编译时计算数据空间字节数的关键字,因此只会计算AutoreleasePoolPage中字段的占用字节数。

另外,当 next == begin() 时,表示 AutoreleasePoolPage 为空;当 next == end() 时,表示 AutoreleasePoolPage 已满。

id * begin() {
        return (id *) ((uint8_t *)this+sizeof(*this));
    }

    id * end() {
        return (id *) ((uint8_t *)this+SIZE);
    }

    bool empty() {
        return next == begin();
    }

    bool full() { 
        return next == end();
}

objc_autoreleasePoolPush

每当自动释放池调用objc_autoreleasePoolPush时都会把边界对象放进栈顶,然后返回边界对象,用于释放。

atautoreleasepoolobj = objc_autoreleasePoolPush();

atautoreleasepoolobj就是返回的边界对象(POOL_BOUNDARY),值为0(也就是个nil),那么这一个page就变成了下面的样子:

push实现如下:

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

它调用AutoreleasePoolPage的类方法push:

static inline void *push() {
   return autoreleaseFast(POOL_BOUNDARY);
}

在这里会进入一个比较关键的方法autoreleaseFast,并传入边界对象(POOL_BOUNDARY):

static inline id *autoreleaseFast(id obj)
{
   AutoreleasePoolPage *page = hotPage();
   if (page && !page->full()) {
       return page->add(obj);
   } else if (page) {
       return autoreleaseFullPage(obj, page);
   } else {
       return autoreleaseNoPage(obj);
   }
}

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

  • 有 hotPage 并且当前 page 不满,调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中
  • 有 hotPage 并且当前 page 已满,调用 autoreleaseFullPage 初始化一个新的页,调用 page->add(obj) 方法将对象添加至新建的AutoreleasePoolPage的栈中
  • 无 hotPage,调用 autoreleaseNoPage内部创建一个 hotPage,内部调用 page->add(obj) 方法将对象添加至 新建的AutoreleasePoolPage的栈中

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

hotPage是autoreleasePage链表中正在使用的autoreleasePage节点。实质上是指向autoreleasepage的指针,并存储于线程的TSD(线程私有数据:Thread-specific Data)中:

static inline AutoreleasePoolPage *hotPage() 
    {
        AutoreleasePoolPage *result = (AutoreleasePoolPage *)
            tls_get_direct(key);
        if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
        if (result) result->fastcheck();
        return result;
    }

static inline void *tls_get_direct(tls_key_t k)
{ 
    ASSERT(is_valid_direct_key(k));

    if (_pthread_has_direct_tsd()) {
        return _pthread_getspecific_direct(k);
    } else {
        return pthread_getspecific(k);
    }
}

来看看add的实现

 id *add(id obj)
    {
        assert(!full());
        unprotect();
        id *ret = next;  // faster than `return next-1` because of aliasing
        *next++ = obj;
        protect();
        return ret;
}

add函数主要是将obj地址放到next指针指向的位置,并将next指向下一个位置,最后返回next上一次指向的位置。

再来看满页处理autoreleaseFullPage,会根据传入的page找到最后一页,最后根据这页创建一个新页,在构造函数中会把这页赋值给前一页的child,把前一页赋值给这一页的parent,就完成了页的连接。再将新建的这页设为hot page后加入需要放入page的那个obj。

 static __attribute__((noinline))
    id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
    {
        // 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.
        assert(page == hotPage());
        assert(page->full()  ||  DebugPoolAllocation);

        do {
            if (page->child) page = page->child;
            else page = new AutoreleasePoolPage(page);
        } while (page->full());

        setHotPage(page);
        return page->add(obj);
}

再来看看空页处理,空页说明没有pool被push,或者一个带有空占位标记的pool被push了,但是还没有内容。

    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", 
                         pthread_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.

        // Install the first page.
        AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
        setHotPage(page);
        
        // Push a boundary on behalf of the previously-placeholder'd pool.
        if (pushExtraBoundary) {
            page->add(POOL_BOUNDARY);
        }
        
        // Push the requested object or pool.
        return page->add(obj);
}

这里有个概念EMPTY_POOL_PLACEHOLDER,当一个外部调用第一次调用创建AutoreleasePoolPage,但是没有任何要进栈的对象时候,那么它不会先创建一个AutoreleasePoolPage对象,而是把EMPTY_POOL_PLACEHOLDER作为指针返回,会在TLS里记录一条以AUTORELEASE_POOL_KEY为key,以EMPTY_POOL_PLACEHOLDER(1)为值的信息。这样的实现有点像懒加载,在需要的时候才创建对象。

空页处理时会先检查TLS上是否有EMPTY_POOL_PLACEHOLDER标识,如果没有空占位符,并且本次插入的是POOL_BOUNDARY,那么设置并返回空占位符就好了。如果有空占位符的话,那么本次需要新建一页AutoreleasePoolPage,并插入POOL_BOUNDARY和obj。

AutoreleasePoolPage::autorelease(id obj)

对一个对象调用autorelease方法,就是将其放入自动释放池。
autorelease方法的实现,先来看一下方法的调用栈:

- [NSObject autorelease]
└── id objc_object::rootAutorelease()
    └── id objc_object::rootAutorelease2()
        └── static id AutoreleasePoolPage::autorelease(id obj)
            └── static id AutoreleasePoolPage::autoreleaseFast(id obj)
                ├── id *add(id obj)
                ├── static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
                │   ├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
                │   └── id *add(id obj)
                └── static id *autoreleaseNoPage(id obj)
                    ├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
                    └── id *add(id obj)

在autorelease方法的调用栈中,最终都会调用上面提到的autoreleaseFast方法,将当前对象加到AutoreleasePoolPage中。

//被[NSObject autorelease]内部所调用
inline id objc_object::rootAutorelease() {
    if (isTaggedPointer()) return (id)this;
    if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;

    return rootAutorelease2();
}

__attribute__((noinline,used)) id objc_object::rootAutorelease2() {
    return AutoreleasePoolPage::autorelease((id)this);
}

static inline id autorelease(id obj) {
   id *dest __unused = autoreleaseFast(obj);
   return obj;
}

autorelease函数和push函数一样,关键代码都是调用autoreleaseFast函数向自动释放池的链表栈中添加一个对象,
不过push函数的入栈的是一个边界对象,而autorelease函数入栈的是需要加入autoreleasepool的对象。

若当前线程中只有一个AutoreleasePoolPage对象,并记录了很多autorelease对象地址时,内存如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jPPgmWzz-1652109849074)(http://ds.devstore.cn/20141101/1414826929671/2.jpg)]

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

这里我们要注意的一点是objc_object::rootAutorelease这个函数中的处理,对于isTaggedPointer,则直接返回;对于返回值优化了情况,也直接返回;剩下的情况才会走autoreleaseFast。第二种情况就是下文将要提到的Autorelease返回值的快速释放机制。

Autorelease返回值的优化机制

ARC下,runtime有一套对autorelease返回值的优化策略。详见runtime/objc-object.h
通过调用方和被调用方的配合,使得被返回的对象不进入自动释放池,并减少不必要的retain/release调用。

一个被优化的被调方检查调用方跟在返回对象后的指令。如果调用方的指令同样被优化,那么被调方跳过所有的retain count的操作:没有autorelease,也没有retain/autorelease。作为替代,它将返回对象当前的retain count变动(+0或+1)存储到TLS(线程本地存储)中。如果调用方看起来没有被优化过,那么被调方和通常一样,调用一下autorelease或者retain/autorelease。

被优化的调用方会查看TLS。如果变动已被设置,它调用必要的retain或release,将被调方留下的retain count变为调用方所希望的retain count。否则调用方就假设当前结果是来自一个未被优化的被调方的+0变动,并调用这种情形下必要的retain。

有两种被优化的被调方:

objc_autoreleaseReturnValue
result is currently +1. The unoptimized path autoreleases it.
引用计数当前被+1。未优化的话会对它调用autorelease。
      
objc_retainAutoreleaseReturnValue
result is currently +0. The unoptimized path retains and autoreleases it.
引用计数不增加。未优化的话会对它调用retain和autorelease。

有两种被优化的调用方:

objc_retainAutoreleasedReturnValue
caller wants the value at +1. The unoptimized path retains it.
调用方希望引用计数+1。未优化的话会对它调用retain。
    objc_unsafeClaimAutoreleasedReturnValue
caller wants the value at +0 unsafely. The unoptimized path does nothing.
调用方希望不增加引用计数。未优化的话不会进行任何操作。

例子

Callee:
    // compute ret at +1
    return objc_autoreleaseReturnValue(ret);
    
Caller:
    ret = callee();
    ret = objc_retainAutoreleasedReturnValue(ret);
      // use ret at +1 here
    Callee sees the optimized caller, sets TLS, and leaves the result at +1.
    Caller sees the TLS, clears it, and accepts the result at +1 as-is.
    Callee看到了优化的caller,将引用计数+1后设置到TLS。
    Caller看到了TLS,清除它,接受引用计数+1的结果。

callee识别优化的caller的方法是依赖于架构的:

 x86_64: Callee looks for `mov rax, rdi` followed by a call or 
    jump instruction to objc_retainAutoreleasedReturnValue or 
    objc_unsafeClaimAutoreleasedReturnValue. 
  i386:  Callee looks for a magic nop `movl %ebp, %ebp` (frame pointer register)
  armv7: Callee looks for a magic nop `mov r7, r7` (frame pointer register). 
  arm64: Callee looks for a magic nop `mov x29, x29` (frame pointer register).

Tagged pointer对象也参与优化返回对象技术,因为它保存了发送的消息。它们在未优化的情况下也不进入autorelease pool。
该技术的关键函数:

static ALWAYS_INLINE bool 
callerAcceptsOptimizedReturn(const void * const ra0)
判断调用方是否支持优化的return

static ALWAYS_INLINE ReturnDisposition 
getReturnDisposition()
static ALWAYS_INLINE void 
setReturnDisposition(ReturnDisposition disposition)
TLS读取,设置和重置

static ALWAYS_INLINE bool 
prepareOptimizedReturn(ReturnDisposition disposition)
根据所给的disposition参数准备优化的return。如果调用方支持优化(如果disposition为ReturnAtPlus1就设置TLS)就返回true。否则返回false,表明返回对象必须和通常一样,被retain和autorelease,或autorelease。

static ALWAYS_INLINE ReturnDisposition 
acceptOptimizedReturn()
获取存在TLS中的disposition,如果是ReturnAtPlus1还需要重置为ReturnAtPlus0。

举个例子

+ (instancetype)createPerson {
    id tmp = [self new]; 
    return objc_autoreleaseReturnValue(tmp);
}
- (void)testFoo {
    id tmp = _objc_retainAutoreleasedReturnValue([Person createPerson]);
 
}

在调用objc_autoreleaseReturnValue的时候,会先通过__builtin_return_address这个内建函数获得return address,然后根据这个地址判断主调方在调用完objc_autoreleaseReturnValue这个函数以后是否紧接着调用了objc_retainAutoreleasedReturnValue函数,如果是,那么objc_autoreleaseReturnValue()就不把返回的对象注册到自动释放池里面(不做autorelease),runtime会将表征这个行为的值ReturnAtPlus1和键RETURN_DISPOSITION_KEY存储在TLS中,做一个优化标记,然后直接返回这个对象给函数的调用方。如果不是,则会objc_autorelease一下这个对象再返回。

在外部接收这个返回值的objc_retainAutoreleasedReturnValue()会先在TLS中查询RETURN_DISPOSITION_KEY对应的值,如果是ReturnAtPlus1,那么就直接返回这个对象(不调用retain)。如果不是,那么就objc_retain一下这个对象。

所以通过objc_autoreleaseReturnValue和objc_retainAutoreleasedReturnValue的相互配合,利用TSL做一个中转,在ARC下省去了autorelease和retain的步骤。

整个过程中涉及到几个关键点:
TLS:Thread Local Storage
Thread Local Storage(TLS)线程局部存储,目的很简单,将一块内存作为某个线程专有的存储,以key-value的形式进行读写,比如在非arm架构下,使用pthread提供的方法实现:

void* pthread_getspecific(pthread_key_t);
int pthread_setspecific(pthread_key_t , const void *);

在返回值身上调用objc_autoreleaseReturnValue方法时,runtime将RETURN_DISPOSITION_KEY标识储存在TLS中,然后直接返回这个object(不调用autorelease);同时,在外部接收这个返回值的objc_retainAutoreleasedReturnValue里,发现TLS中正好存了RETURN_DISPOSITION_KEY,那么直接返回这个object(不调用retain)。
于是乎,调用方和被调方利用TLS做中转,很有默契的免去了对返回值的内存管理。

于是问题又来了,假如被调方和主调方只有一边是ARC环境编译的该咋办?(比如我们在ARC环境下用了非ARC编译的第三方库,或者反之)
就要用到下面这个。

__builtin_return_address
这个内建函数原型是char *__builtin_return_address(int level),作用是得到函数的返回地址,参数表示层数,如__builtin_return_address(0)表示当前函数体返回地址,传1是调用这个函数的外层函数的返回值地址,以此类推。

- (int)foo {
    NSLog(@"%p", __builtin_return_address(0)); // 根据这个地址能找到下面ret的地址
    return 1;
}
// caller
int ret = [sark foo];

看上去也没啥厉害的,不过要知道,函数的返回值地址,也就对应着调用者结束这次调用的地址(或者相差某个固定的偏移量,根据编译器决定)
也就是说,被调用的函数也有翻身做地主的机会了,可以反过来对主调方干点坏事。
回到上面的问题,如果一个函数返回前知道调用方是ARC还是非ARC,就有机会对于不同情况做不同的处理.

反查汇编指令
通过上面的__builtin_return_address加某些偏移量,被调方可以定位到主调方在返回值后面的汇编指令:

// caller
int ret = [sark foo];
// 内存中接下来的汇编指令(x86,我不懂汇编,瞎写的)
movq ??? ???
callq ???

而这些汇编指令在内存中的值是固定的,比如movq对应着0x48。
于是乎,就有了下面的这个函数,入参是调用方__builtin_return_address传入值.

//x86
static ALWAYS_INLINE bool 
callerAcceptsOptimizedReturn(const void * const ra0)
{
    const uint8_t *ra1 = (const uint8_t *)ra0;
    const unaligned_uint16_t *ra2;
    const unaligned_uint32_t *ra4 = (const unaligned_uint32_t *)ra1;
    const void **sym;

    // 48 89 c7    movq  %rax,%rdi
    // e8          callq symbol
    if (*ra4 != 0xe8c78948) {
        return false;
    }
    ra1 += (long)*(const unaligned_int32_t *)(ra1 + 4) + 8l;
    ra2 = (const unaligned_uint16_t *)ra1;
    // ff 25       jmpq *symbol@DYLDMAGIC(%rip)
    if (*ra2 != 0x25ff) {
        return false;
    }

    ra1 += 6l + (long)*(const unaligned_int32_t *)(ra1 + 2);
    sym = (const void **)ra1;
    if (*sym != objc_retainAutoreleasedReturnValue  &&  
        *sym != objc_unsafeClaimAutoreleasedReturnValue) 
    {
        return false;
    }

    return true;
}

它检验了主调方在返回值之后是否紧接着调用了objc_retainAutoreleasedReturnValue,如果是,就知道了外部是ARC环境,反之就走没被优化的老逻辑。

objc_autoreleasePoolPop

objc_autoreleasePoolPop(atautoreleasepoolobj);

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

1.根据传入的哨兵对象地址找到哨兵对象所处的page

2.在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次- release消息,并向回移动next指针到正确位置

3.补充2:从最新加入的对象一直向前清理,可以向前跨越若干个page,直到哨兵所在的page

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

嵌套的AutoreleasePool

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

AutoreleasePoolPage::pop()实现:

static inline void pop(void *token)   // token指针指向栈顶的地址
{
    AutoreleasePoolPage *page;
    id *stop;

    page = pageForPointer(token);   // 通过栈顶的地址找到对应的page
    stop = (id *)token;
    if (DebugPoolAllocation  &&  *stop != POOL_SENTINEL) {
        // This check is not valid with DebugPoolAllocation off
        // after an autorelease with a pool page but no pool in place.
        _objc_fatal("invalid or prematurely-freed autorelease pool %p; ", 
                    token);
    }

    if (PrintPoolHiwat) printHiwat();   // 记录最高水位标记

    page->releaseUntil(stop);   // 从栈顶开始操作出栈,并向栈中的对象发送release消息,直到遇到第一个哨兵对象

    // memory: delete empty children
    // 删除空掉的节点
    if (DebugPoolAllocation  &&  page->empty()) {
        // special case: delete everything during page-per-pool debugging
        AutoreleasePoolPage *parent = page->parent;
        page->kill();
        setHotPage(parent);
    } else if (DebugMissingPools  &&  page->empty()  &&  !page->parent) {
        // special case: delete everything for pop(top) 
        // when debugging missing autorelease pools
        page->kill();
        setHotPage(nil);
    } 
    else if (page->child) {
        // hysteresis: keep one empty child if page is more than half full
        if (page->lessThanHalfFull()) {
            page->child->kill();
        }
        else if (page->child->child) {
            page->child->child->kill();
        }
    }
}

该过程主要分为两步:

  • page->releaseUntil(stop),对栈顶(page->next)到stop地址(POOL_SENTINEL)之间的所有对象调用objc_release(),进行引用计数减1
  • 清空page对象page->kill(),有两句注释
// hysteresis: keep one empty child if this page is more than half full

// special case: delete everything for pop(0)
除非是pop(0)方式调用,这样会清理掉所有page对象;
否则,在当前page存放的对象大于一半时,会保留一个空的子page,
这样估计是为了可能马上需要新建page节省创建page的开销。

总结

1.子线程在使用autorelease对象时,如果没有autoreleasepool会在autoreleaseNoPage中懒加载一个出来。
2.在runloop的run:beforeDate,以及一些source的callback中,有autoreleasepool的push和pop操作,总结就是系统在很多地方都差不多autorelease的管理操作。
3.就算插入没有pop也没关系,在线程exit的时候会释放资源,执行AutoreleasePoolPage::tls_dealloc,在这里面会清空autoreleasepool。

推荐阅读

黑幕背后的Autorelease http://blog.sunnyxx.com/2014/10/15/behind-autorelease/
带着问题看源码----子线程AutoRelease对象何时释放 https://suhou.github.io/2018/01/21/%E5%B8%A6%E7%9D%80%E9%97%AE%E9%A2%98%E7%9C%8B%E6%BA%90%E7%A0%81----%E5%AD%90%E7%BA%BF%E7%A8%8BAutoRelease%E5%AF%B9%E8%B1%A1%E4%BD%95%E6%97%B6%E9%87%8A%E6%94%BE/
浅谈 AutoreleasePool 的实现原理 https://cloud.tencent.com/developer/article/1350726
深入理解Autorelease Pool https://cloud.tencent.com/developer/article/1006618?fromSource=gwzcw.700857.700857.700857
AutoreleasePool底层实现原理 https://www.jianshu.com/p/50bdd8438857
autoreleasepool的使用场景和原理 https://www.jianshu.com/p/9da2929c9b61
Objective-C Autorelease Pool 的实现原理 http://blog.leichunfeng.com/blog/2015/05/31/objective-c-autorelease-pool-implementation-principle/#jtss-tsina
自动释放池的前世今生 http://www.cocoachina.com/ios/20160702/16569.html

猜你喜欢

转载自blog.csdn.net/Mamong/article/details/124678060