提起Objective-C的内存管理,除了MRC和ARC之外,我们也不得不提及autorelease。
autorelease看起来很像ARC,使用NSObject类的autorelease方法可以在AutoreleasePool被释放时,调用对象中的release方法,它是提供了一种在将来某个时间释放对象的机制,能够避免对象立刻被释放的情况。
我们常常见到的代码是这个样子的

@autoreleasepool {
    // balabala
}

当然也有可能是这个样子的(非ARC环境下),但是苹果并不推荐一下这种形式

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain];
// [pool release];

以下仅讨论ARC环境下。

应用场景

在大部分情况下,我们不需要手动提供autoreleasePool,UIKit能够在很多情况下自动将对象交于autoreleasePool保管。在ARC环境下,作为以alloc/new/copy/mutableCopy开头的方法的返回值取得的对象是自己生成并持有的,其他情况下便是取得非自己持有的对象,此时对象的持有者就是autoreleasePool。
下面这个方法,在运行时,该对象的持有者即为autoreleasePool

+ (id)object {
    return [[NSObject alloc] init];
}

下面我们来看一个完整的实例:

#import <Foundation/Foundation.h>
@interface MyObject : NSObject
+ (id)testObject;
@end
@implementation MyObject
+ (id)testObject {
    id obj = [[MyObject alloc] init];
    return obj;
}
+ (id)allocObject {
    id obj = [[MyObject alloc] init];
    return obj;
}
@end

int main(int argc, const char * argv[]) {
    __weak id a;
    @autoreleasepool {
        a = [MyObject testObject];
        // a = [Myobject allocObject];
        _objc_autoreleasePoolPrint();
        NSLog(@"in:%@",a);
    }
    NSLog(@"out:%@",a);
    return 0;
}

我们可以使用非公开方法_objc_autoreleasePoolPrint()方法来看查看信息,
调用testObject时,输出结果如下:

可以看到,autorelease持有一个MyObject对象,这其实就是我们的对象a,当autorelease结束时,对象被释放掉置为nil。
现在我们调用allocObject方法,再次运行:

可以发现,autoreleasePool并没有持有该对象,而且,由于a是__weak指针,返回之后由于该对象无人持有,当然就被释放掉了。这里in:后面输出就已经是nil了。其实如果你细心的话,此时编译器已经给出警告了。

其实即便是我们不写@autoreleasepool效果也是一样的,@autoreleasepool可以进行嵌套,这里之所以手动生命一个,是想说明当autoreleasepool被释放的时候,其持有的对象也会被释放。
当然,也有些时候需要我们手动声明autoreleasePool,

  1. 非Apple UI框架下
  2. 有时候在循环体内,我们会创建大量的临时对象,为了降低循环体带来的内存使用峰值,我们可以在循环体内手动添加autoreleasePool来达到在下次迭代之前处理掉这些对象。
  3. 派生多个线程时,在执行线程的位置添加autoreleasePool,这样可以避免内存泄露

@autoreleasepool原理 - AutoreleasePoolPage

当我们使用@autoreleasepool{}时,编译器会将其转换成以下形式

/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

}

__AtAutoreleasePool定义如下:

struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

可以看到,实际上是创建时调用了objc_autoreleasePoolPush()方法,而释放时调用objc_autoreleasePoolPop()方法,只是一层简单的封装。这两个方法都能在runtime源码objc/source/NSObject.mm中看到它们的定义如下:

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

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

到了这里,我们就可以看到实际上没有AutoreleasePool这个东西,而出现了AutoreleasePoolPage,我们先暂停对AutoreleasePoolPage::push()AutoreleasePoolPage::pop(ctxt)的追究,这两个方法只是AutoreleasePoolPage类的两个类方法而已。实际上,AutoreleasePool可以看作一个双向链表,节点则是AutoreleasePoolPage的实例对象。AutoreleasePoolPage也可以在NSObject.mm中查看其实现,其主要的静态变量和实例变量如下:

#define POOL_SENTINEL nil
    static pthread_key_t const key = AUTORELEASE_POOL_KEY;  // 线程相关
    static uint8_t const SCRIBBLE = 0xA3;  // 0xA3A3A3A3 after releasing
    static size_t const SIZE =  // 每个实例page的大小,与虚拟内存一页的大小一致,实际为4096字节
#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; // 指向最后一个添加进page的OC对象的下一个位置
    pthread_t const thread;   // 线程引用
    AutoreleasePoolPage * const parent; // 指向链表的前置节点(父节点)
    AutoreleasePoolPage *child; // 指向链表的后置节点(子节点)
    uint32_t const depth; // page的index
    uint32_t hiwat;

也就是说,AutoreleasePool实际是这个样子的,

每个AutoreleasePoolPage的实例是4096字节,除去实例变量的大小,剩下的空间用于存储autorelease对象的地址。它的构造函数如下:

    AutoreleasePoolPage(AutoreleasePoolPage *newParent) 
        : magic(), next(begin()), thread(pthread_self()),
          parent(newParent), child(nil), 
          depth(parent ? 1+parent->depth : 0), 
          hiwat(parent ? parent->hiwat : 0)
    { 
        if (parent) {
            parent->check();
            assert(!parent->child);
            parent->unprotect();
            parent->child = this;
            parent->protect();
        }
        protect();
    }

可以看到AutoreleasePoolPage在初始化时,parent指针指向了父节点,child指针置为nil,depth即为父节点的fepth+1,其实完全可以把depth理解成这个page的索引,而后又将parent的child指针指向本page,这也就是典型的双链表在尾部添加节点的形式了,不理解的话自行复习数据结构。

我们重新回到AutoreleasePoolPage::push()AutoreleasePoolPage::pop(ctxt)这两个方法来。定义如下(嫌长可以跳过这段代码):

    static inline void *push() 
    {
        id *dest;
        if (DebugPoolAllocation) {
            // Each autorelease pool starts on a new pool page.
            dest = autoreleaseNewPage(POOL_SENTINEL);
        } else {
            dest = autoreleaseFast(POOL_SENTINEL);
        }
        assert(*dest == POOL_SENTINEL);
        return dest;
    }

    static inline void pop(void *token) 
    {
        AutoreleasePoolPage *page;
        id *stop;

        page = pageForPointer(token);
        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);

        // 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();
            }
        }
    }

AutoreleasePoolPage::push()方法被调用是,只是向当前的page添加一个哨兵对象(POOL_SENTINEL,值为nil),于是page就变成了下面这个样子

AutoreleasePoolPage::pop()其实顺着链表往parent方向走找到对应哨兵对象,将晚于哨兵对象插入的所有autorelease对象全部进行release。这样一来,也很容易理解AutoreleasePool可以进行嵌套了,无非是多插入了几个哨兵对象而已。

AutoreleasePool对象什么时候释放?

这是面试时经常会问道的一个问题,最初我也一样踩过坑——“当前作用域大括号结束时释放”,只怪当时太年轻啊。

在没有手加Autorelease Pool的情况下,Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop

可以参考一下黑幕背后的Autorelease的小实验,很有意思。

AutoreleasePool与ARC的关系?

AutoreleasePool可以理解为一个对象,在ARC环境下,这个对象本身也是自动计数的,当这个pool的引用计数为0时,就被清掉了,这是手动声明时候的情况,当系统自动为我们建立AutoreleasePool以及在runloop结束时释放就是系统的事情了。但同时AutoreleasePool不同于一般的对象指针,它可以持有多个对象,当pool被release的时候,其会向其持有的所有对象发送release消息,在非ARC环境下用来延长对象的作用域是很有效的。我们知道ARC只是编译器在帮我们自动添加retain和release,而并非垃圾回收器,这样以来,就会不可避免的产生一些内存问题,AutoreleasePool则可以看作是ARC的一种补充,补足了有可能产生内存峰值过高(循环体)或内存泄露(新线程)的情况。

其他一些事情

观察以下代码

int main(int argc, const char * argv[]) {
    NSArray *persons = @[@"Bob", @"Steve"];
    [persons enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        _objc_autoreleasePoolPrint();
    }];
    NSLog(@"=============================");

    for(NSString *person in persons){
        _objc_autoreleasePoolPrint();
    }
}

输出如下:

可以很明显的看到enumerateObjectsUsingBlock里,多了一个AutoreleasePool,也就是说Apple在enumerateObjectsUsingBlock自动帮我们使用了@autoreleasepool{}来达到在下次迭代前清掉临时对象的目的,从而降低循环体内存占用峰值,而传统的for循环是没有的,事实上,enumerate...系列的方法都是会自动添加@autoreleasepool{},这也是我们通常推荐使用enumerate...方法的原因之一。

Perference

  1. 黑幕背后的Autorelease
  2. NextPrevious Using Autorelease Pool Blocks