AutoreleasePool 理解

从一些问题开始

  1. 什么是 AutoreleasePool ? 说明一下 NSAutoreleasePool 具体机制?
  2. ARC 时代和 MRC 时代的 AutoreleasePool 机制有什么区别?
  3. AutoreleasePool 的实现机制?
  4. AutoreleasePool 和 NSRunloop 有什么关系?
  5. AutoreleasePool 和线程有什么关系?
  6. 什么时候需要我们手动创建 AutoreleasePool ?

# 什么是 AutoreleasePool ? 如何理解 NSAutoreleasePool?

NSAutoreleasePool 对象的官方说明是一个支持 Cocoa 引用计数式内存管理的一个对象。 当池子排掉的时候向池子内存储的对象发送 release 消息。

An object that supports Cocoa’s reference-counted memory management system. An autorelease pool stores objects that are sent a release message when the pool itself is drained.

具体机制说明: 在引用计数式的内存管理中,NSAutoreleasePool 对象包含了收到了 _autorelease 消息的对象,这些 autorelease 对象(我们称被标记了 __autorelease 的对象为 autorelease 对象)的生命周期被延长到了这个 NSAutoreleasePool drain 的时候。也可以这么说 autoreleaserelease 的区别仅仅是 autorelease 是延时释放(即等待 AutoreleasePool drain) 而 release 是立即释放。

感觉说到这儿,其实我们可以说 NSAutoreleasePool 就是一个帮助我们管理内存的一个工具。

其实不光是我们自己可以手动创建 NSAutoreleasePool 对象,系统也帮我们维护了一个 NSAutoreleasePool 对象,在 runloop 迭代中不断 PushPop,从而不会堆积过多的 autorelease 对象引起内存疯长。你可能会好奇,哪会有那么多 autorelease 对象?举个例子来看一下:

- (void)viewDidLoad {
    [super viewDidLoad];
    // str 其实是一个 autorelease 对象
    NSString *str = [NSString stringWithFormat:@"sunnyxx"];
    reference = str;
}

题外话:为啥 str 是一个 autorelease 对象呢? 这个就需要知道下内存管理的知识了,使用 alloc,new,copymutableCopy这些关键字生成的对象是自己持有,反之不是(参考 Memory Management Policy (opens new window))。使用 stringWithFormat: 类方法生成的 str 没有持有它的对象,只能通过 autorelease 这种方式来延长它的生命周期。具体 autorelease 的时机是在 stringWithFormat 内部做的。

CocoaFramework 里大量生成了 autorelease 的对象,所以官方说明里 Cocoa 代码执行是预期在一个 autorelease 环境中。

# ARC 时代和 MRC 时代的 AutoreleasePool 机制有什么区别?

没啥根本区别,只是写法稍有不同。看两个 ARC 和 MRC 时代 autorelease 的两个经典写法。

MRC 的 case:

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

ARC 的 case(注:其实 MRC 也可以这么写 (opens new window)):

@autoreleasepool {
    //_autorelease 为所有权修饰符。
    id _autorelease obj = [[NSObject alloc] init];
}

ARC 中的几点变化:

  1. ARC 中是不能使用 autorelease 方法,也不能使用 NSAutoreleasePool 类。

  2. ARC 系统提供了 @autoreleasepool 块来替代 NSAutoreleasePool 对象的生成持有以及废弃的功能。

  3. 通过将对象赋值给附加了 __autoreleaseing 修饰符变量来替代调用 autorelease 方法。即

    id obj = [[NSObject alloc] init];
    [obj autorelease];
    

    等价于

    id _autorelease obj = [[NSObject alloc] init];
    

一般我们不会显式的去使用 __autorelease 修饰符,因为 ARC 下编译器帮我们做了一些工作,即编译器会检查方法是否以 alloc/new/copy/mutableCopy 开始,如果不是的话将返回的值对象注册到 autoreleasePool

不需要显式地写 __autorelease 的几种场景

  1. 自动释放池随意生成对象,不需要显式地添加 autorelease

    @autoreleasepool {
        //默认的 strong 修饰符会自动处理这种情况.
        id obj = [[NSObject alloc] init];
    }
    
  2. 函数返回值的场景

    + (NSArray *)array {
        id obj = [[NSMutableArray alloc] init];
        return obj;
    }
    

    在 MRC 时代,obj 是需要被发送 autorelease 方法的,ARC 时代不需要这么做,这个对象作为函数的返回值会自动被注册到 autoreleasePool

  3. 访问 weak 变量的时肯定会涉及到 autoreleasePool

    因为 weak 对对象是弱引用,对象随时会被释放,但是使用 autoreleasePool 会延时释放,保证 weak 访问过程中不会出现对象被释放这种状况。

  4. NSObject **obj 其实就是 NSObject *_autorelease * obj。 因为我们不持有通过引用返回的对象 (opens new window)。这种情况只能是 autorelease

# AutoreleasePool 的实现机制?

# 分析过程

对以下代码所在文件执行 clang -rewrite-objc xx.m 重写命令,可以看到 OC 对应的 C++ 的源码。

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

转换后的 C++ 代码。

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_9w_q4lvthyn17v0cxxm5s7fsb500000gn_T_main_1280f1_mi_0);
    }
    return 0;
}

可以看到 @autoreleasepool 被转换为一个名为 __AtAutoreleasePool 的数据结构。

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

main 函数其实可以理解为

int main(int argc, const char * argv[]) {
    {
        void *atautoreleasepoolobj = objc_autoreleasePoolPush();
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_9w_q4lvthyn17v0cxxm5s7fsb500000gn_T_main_1280f1_mi_0);
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
    return 0;
}

具体 objc_autoreleasePoolPushobjc_autoreleasePoolPop 的实现在 runtime 源码 NSObject.mm中可以找到。

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

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

# AutoreleasePoolPage 的介绍

这里涉及到了 AutoreleasePoolPage 这个数据结构,接下来就看下 AutoreleasePoolPage 这个数据结构是啥样的?AutoreleasePoolPage 是个 C++ 的类

class AutoreleasePoolPage  {
    magic_t const magic;    //magic 用于对当前 AutoreleasePoolPage 完整性的校验
    id *next;               //当前 autoreleasePoolPage 最上层的对象的指针。
    pthread_t const thread; //thread 保存了当前页所在的线程
    AutoreleasePoolPage * const parent;//指向上一个 AutoreleasePoolPage 对象.
    AutoreleasePoolPage *child; //指向下一个 AutoreleasePoolPage 对象.
    uint32_t const depth;
    uint32_t hiwat;
}

关于 AutoreleasePoolPage 的说明

  1. 可以看到其实并没有一个整体的自动释放池对象,自动释放池是由一个双向链表构成。当一个 AutoreleasePoolPage 的空间被占满之后继续创建新的 AutoreleasePoolPage 对象。

    //child 指向的是下一个 AutoreleasePoolPage 对象的指针
    // 这个方法是当前 page 如果满的情况下创建新的 page.
    id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) {
        do {
            if (page->child) page = page->child;
            else page = new AutoreleasePoolPage(page);
        } while (page->full());
        ....
        return page->add(obj);
    }
    // 初始化 pool 的方法 在这个里面对 parent 和 child 进行了赋值.
    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->child = this;
        }
    }
    
  2. 每个 AutoreleasePoolPage 对象都存储着当前的线程 id 参考上面的 AutoreleasePoolPage 的初始化方法。使用 pthread_self() 拿到当前的线程 id 然后保存到 thread 成员变量里。

  3. AutoreleasePoolPage 的内存大小是 4096 个字节。是 80386 机器上的每页的字节数。

    //初始化 AutoreleasePoolPage 的方法,size 是个宏定义的 4096
    static void * operator new(size_t size) {
        return malloc_zone_memalign(malloc_default_zone(), SIZE, SIZE);
    }
    
  4. AutoreleasePoolPage 存储 autorelease 对象是通过自己内部的 next 指针去实现。从实现上可以看到 AutoreleasePoolPage 还是从低内存地址向高内存地址增长。

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

    由此大致能得到 AutoreleasePoolPage 的内存结构如图(来自 Sunny 大神博客) Jietu20180125-103109

# autorelease 消息调用栈

了解了这个数据结构后看下 autorelease 消息的调用栈。

我们看下 AutoreleasePoolPageautorelease 方法实现其实就是将 autorelease 对象存储到 AutoreleasePoolPage 的过程。下面是大致实现的代码

static inline id autorelease(id obj) {
    ...
    id *dest __unused = autoreleaseFast(obj);
    ...
    return obj;
}
//这个是将 obj 存入 AutoreleasePoolPage 的方法。
static inline id *autoreleaseFast(id obj) {
    //hotPage 应该是去 TLS(线程本地存储) 中获取 AutoreleasePoolPage。
    //如果是程序刚启动的话,这儿肯定拿到的空。
    AutoreleasePoolPage *page = hotPage();
    if (page && !page->full()) {
        // AutoreleasePoolPage 不满的时候直接往进加
        return page->add(obj); //绝大多数情况我们走的都是这个分支。
    } else if (page) {
        // AutoreleasePoolPage 满了,则创建新的 page,将 obj 放到新的 page 里去.
        return autoreleaseFullPage(obj, page);
    } else {
        // 创建新的 page.
        return autoreleaseNoPage(obj);
    }
}

# autorelease pop 消息

对应 push 的是 poppop 即为将存储到 AutoreleasePoolPage 的对象释放对应原型为

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

注意的是这里并没有直接传入对象,而是传入了一个 ctxt 的指针,根据内部实现来看,自动释放池根据 ctxt 拿到它当前所在的 AutoreleasePoolPage ,然后将 AutoreleasePoolPagectxt 的位置开始到到最新的 AutoreleasePoolPage 存储的 autorelease 对象全部释放。即我们可以理解为自动释放池代码块儿开始的时候会在 AutoreleasePoolPage 进行一个占位,然后将后续的 autorelease 对象都放到占位后,这样就能确定当前自动释放池块儿里的对象是从哪到哪,理解了这一点也就能理解 autorelease 的嵌套实现了。

static inline void pop(void *token)  {
    AutoreleasePoolPage *page; id *stop;
    ..
    page = pageForPointer(token); //拿到 token 所在的 AutoreleasePoolPage
    stop = (id *)token;
    if (*stop != POOL_BOUNDARY) {
        if (stop == page->begin()  &&  !page->parent) {
            // Start of coldest page may correctly not be POOL_BOUNDARY:
            // 1. top-level pool is popped, leaving the cold page in place
            // 2. an object is autoreleased with no pool
        } else {
            return badPop(token);
        }
    }
    if (PrintPoolHiwat) printHiwat();
    page->releaseUntil(stop);  //一直释放对象到 token 的位置.
}
//一直释放对象的函数
void releaseUntil(id *stop)  {
    while (this->next != stop) {
        AutoreleasePoolPage *page = hotPage(); //拿到当前的 page.
        id obj = *--page->next;
        memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
        if (obj != POOL_BOUNDARY) {
            objc_release(obj);  //取出对象不断发送 relesse 消息..
        }
    }
    setHotPage(this);
}

# AutoreleasePool 和 NSRunloop 有什么关系?

先来个实例看下 Runloop 是什么东西。建一个普通的 Single View App 工程。点击按钮然后在按钮点击事件里打印

- (void)btnPressed:(id)sender {
    NSRunLoop *runloop = [NSRunLoop currentRunLoop];
    //在这里打断点然后 po runloop 得到下面结果。(省略大部分无关内容)
}

(lldb) po runloop
common mode items = <CFBasicHash 0x604000249360 [0x110875bb0]>
	1 : <CFRunLoopObserver 0x6040001370c0 [0x110875bb0]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x110a24276), ....
	......
	4 : <CFRunLoopObserver 0x604000136ee0 [0x110875bb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x110a24276), ....

注意看上面的 activities,它对应的定义是这里

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),
    kCFRunLoopBeforeTimers = (1UL << 1),
    kCFRunLoopBeforeSources = (1UL << 2),
    kCFRunLoopBeforeWaiting = (1UL << 5),
    kCFRunLoopAfterWaiting = (1UL << 6),
    kCFRunLoopExit = (1UL << 7),
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

可以确定 Autorelease 机制在 Runloop 进入和退出(和休眠前触发) CommonMode 的时候进行观察,当 Runloop 运行到指定的时机的时候回触发 _wrapRunLoopWithAutoreleasePoolHandler 回调方法。

_wrapRunLoopWithAutoreleasePoolHandler 这个方法的实现其实我们并不清楚,网上没有找到对应的实现,不过我们可以打下符号断点来看看有没有线索。果然应用刚启动就执行了这些方法。看左侧的调用栈确实是从 Observer 的回调执行过来的。下面两个是我们熟悉的 Pop 和 Push 操作,基本上可以确认,Autorelease 机制是在进入 Runloop 的时候就创建了一个新的 AutoreleasePoolPage。退出或者休眠的的时候回收 AutoreleasePoolPage

# AutoreleasePool 和线程有什么关系?

Cocoa 应用程序里的每个线程会自动维护一个释放池,就是通过上面 Runloop 的方式。但是如果没有 Runloop 呢?

之前看到有人问了一个问题:子线程默认不会开启 Runloop,那出现 Autorelease 对象如何处理?不手动处理会内存泄漏吗? 答案是不会。

具体 demo 如下 参考 (opens new window)

- (void)viewDidLoad {
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil];
    [thread start];
}

-(void)test {
    MyClass *my = [[[MyClass alloc] init] autorelease];
    NSLog(@"%@",[my description]);
}

最后的结果是 MyClass 实例被释放掉了。理论上来说子线程并没有 Runloop 也就没有自动释放池观察 Runloop 状态,也就不会自动去执行对应的 autorelease 的方法。根据引用计数来看的话,autorelease 方法和 AutoreleasePool 在一起才能发生作用,而目前又没有 AutoreleasePool,所以那是咋回事?

事实上即使没有 Runloop,线程和 AutoreleasePool 也能直接发生关系。向某个对象发送 autorelease 消息后,会自动创建 AutoreleasePoolPageautorelease 消息的调用栈可以参考上面的说明。最终 TLS(线程本地存储)会存储 AutoreleasePoolPage 对象。大致代码如下:

AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
tls_set_direct(key, (void *)page);

这里具体实现比较复杂,而且根据是这种情况并不适用于主线程。可以看 StackOverflow (opens new window) 的相关回答。这里不具体贴了。

我个人觉得为了程序可读性还有稳定性,还是加上 @autoreleasepool 更妥。说稳定性是因为不能过度依赖于 runtime 的底层机制,万一 runtime 底层机制后续有变化可能会造成程序的异常。

# 什么时候需要我们手动创建 AutoreleasePool?

  1. 如果工程只是 Foundation-Only(命令行那种),而不是 Cocoa application。那需要手动创建自动释放池。
  2. 如果程序存活时间长,而且可能生成大量临时对象(比如循环里创建了一堆)那应该在合适地方(比如循环里)手动释放池,降低内存峰值(不用担心嵌套使用 AutoreleasePool 的问题)
  3. 你创建了一个新线程,需要创建自动释放池。这个跟我们上面一小节说的是略微冲突,但是在上面已经说过了,添加 AutoreleasePool 是最佳实践。

# 参考地址

黑幕背后的 Autorelease (opens new window) 自动释放池的前世今生 ---- 深入解析 autoreleasepool (opens new window) 深入理解 RunLoop (opens new window) iOS 中 autorelease 的那些事儿 (opens new window) Transitioning to ARC Release Notes (opens new window) NSAutoreleasePool (opens new window) Using Autorelease Pool Blocks (opens new window) iOS ARC 内存管理要点 (opens new window) 各个线程 Autorelease 对象的内存管理 (opens new window)