深入理解Objective-C: Autorelease Pool (自动释放池)

本文详细介绍了Objective-C中的自动释放池(Autorelease Pool)的工作原理和使用,包括对象销毁、创建autorelease pool、AutoreleasePoolPage以及Autorelease Pool Blocks的相关操作。在iOS开发中,自动释放池对于内存管理至关重要,了解其内部机制能有效防止内存泄漏,提高程序性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

代码

官方文档NSAutoreleasePool 传送门

官方文档NSAutoreleasePool个人翻译 传送门

在MRC环境下

   NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
   [pool release];

在ARC or MRC 环境下均可使用

    @autoreleasepool {
    }

内存管理

关于NSAutoReleasePool的理解 传送门

Cocoa的内存管理主要依赖于Reference Counting, 而NSAutoReleasePool就是用来支持它的。
Autorelease pool中存放的对象会在其自身干枯(drain)时被release

对象的销毁

我们都知道当一个objectrelease方法被触发时, 这个对象就被销毁了, 再也不能对它有任何引用, 否则就会出现异常。
但如果在销毁它时触发的是autorelease方法, 那这个object就进入了对应的autorelease pool, 它的生命就被延长了(当pool drain时才真正被销毁).

在引用计数的环境中,Cocoa期望有一个可以随时可用的自动释放池。如果一个池不可用,则自动释放的对象不会被释放,您会泄漏内存。在这种情况下,您的程序通常会记录适当的警告消息。

应用程序包在事件循环的每一个循环开始时在主线程上创建一个自动释放池,并在结束时将其耗尽,从而释放在处理事件时生成的任何自动释放的对象。如果您使用的是应用程序包,那么您通常不需要创建自己的池。如果您的应用程序在事件循环中创建了许多临时的自动回调对象,那么创建“本地”自动回调池将有助于最小化峰值内存占用。

创建autorelease pool

NSAutoReleasePool的初始化与普通的NSObject一样, 都是alloc+init, 不过pool不能被retain, 因为在drain的时候默认就销毁它自身了.

还有一点需要注意的是, 通常在销毁pool的时候用的不是它的release方法, 而是drain!
原因是为了让程序同时兼容Reference Counting内存管理环境 与 Garbge Collection环境, 因为在Garbage Colloection环境中drain的作用是触发collect garbage动作.

一般来说在应用的main thread中, 已经存在了一个autorelease pool. 有两种情况需要开发者自己新建autorelease pool:

1、在main thread中, 在某个方法中出现大量的autoreleased objects, 为了避免memory footprint的增大, 可以手动创建一些autorelease pool用来drain objects.
2、创建新的thread, 并在其中访问了Cocoa, 需要在访问的前创建autorelease pool, 访问结束后drain.

最后一点, 在每个thread中都会维持一个stack(栈堆), 其中放置着所有在这个thread中创建但未销毁的pool, 每当一个新的pool创建后, 它就位于stack的最顶端, 相应autoreleased object就会放入其中. 当pool drain的时候, 它就会从stack的顶端移除, 并且release掉其包含的objects`.

AutoreleasePoolPage

在MRC环境下,我们可以看到-[NSAutoreleasePool release]方法最终是通过调用 AutoreleasePoolPage::pop(void *) 函数来负责对autoreleasepool中的autoreleased对象执行release操作的。

其实,autoreleasepool 是没有单独的内存结构的,它是通过以 AutoreleasePoolPage 为结点的双向链表来实现的。
我们打开 runtime的源码工程,在 NSObject.mm 文件的第 671-1228 行可以找到 autoreleasepool的实现源码。

通过阅读源码,我们可以知道:

1、每一个线程的 autoreleasepool 其实就是一个指针的堆栈;
2、每一个指针代表一个需要release的对象或者 POOL_BOUNDARY(哨兵对象,代表一个autoreleasepool的边界);
3、一个pool token 就是这个pool所对应的POOL_BOUNDARY的内存地址。当这个poolpop的时候,所有内存地址在 pool token 之后的对象都会被release
4、这个堆栈被划分成了一个以 pool page为结点的双向链表。pool pages 会在必要的时候动态地增加或删除;
5、Thread-local storage(线程局部存储)指向hot page ,即最新添加的 autoreleased对象所在的那个page

一个空的AutoreleasePoolPage的内存结构如下所示:

    magic_t const magic;
    id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;
magic 用来校验 AutoreleasePoolPage 的结构是否完整;
next 指向最新添加的 autoreleased 对象的下一个位置,初始化时指向 begin() ;
thread 指向当前线程;
parent 指向父结点,第一个结点的 parent 值为 nil ;
child 指向子结点,最后一个结点的 child 值为 nil ;
depth 代表深度,从 0 开始,往后递增 1;
hiwat 代表 high water mark 。

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

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

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

Autorelease Pool Blocks

我们使用 clang -rewrite-objc命令将下面的Objective-C代码重写成C++代码:

@autoreleasepool {
}

将会得到以下输出结果(只保留了相关代码):

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

不得不说,苹果对@autoreleasepool {} 的实现真的是非常巧妙,真正可以称得上是代码的艺术。

苹果通过声明一个 __AtAutoreleasePool类型的局部变量 __autoreleasepool来实现@autoreleasepool {}

当声明 __autoreleasepool 变量时,构造函数__AtAutoreleasePool()被调用,即执行atautoreleasepoolobj = objc_autoreleasePoolPush();;
当出了当前作用域时,析构函数~__AtAutoreleasePool() 被调用,即执行 objc_autoreleasePoolPop(atautoreleasepoolobj);

也就是说@autoreleasepool {}的实现代码可以进一步简化如下


/* @autoreleasepool */ 
{
    void *atautoreleasepoolobj = objc_autoreleasePoolPush();
    // 用户代码,所有接收到 autorelease 消息的对象会被添加到这个 autoreleasepool 中
    objc_autoreleasePoolPop(atautoreleasepoolobj);
}

因此,单个autoreleasepool的运行过程可以简单地理解为 objc_autoreleasePoolPush()[对象 autorelease]objc_autoreleasePoolPop(void *) 三个过程。

objc_autoreleasePoolPush()操作

上面提到的objc_autoreleasePoolPush()函数本质上就是调用的 AutoreleasePoolPagepush函数。

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

因此,我们接下来看看 AutoreleasePoolPagepush 函数的作用和执行过程。

一个 push 操作其实就是创建一个新的autoreleasepool ,对应 AutoreleasePoolPage 的具体实现就是往AutoreleasePoolPage中的next位置插入一个 POOL_BOUNDARY ,并且返回插入的 POOL_BOUNDARY 的内存地址。
这个地址也就是我们前面提到的pool token ,在执行pop 操作的时候作为函数的入参。

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

push 函数通过调用 autoreleaseFast函数来执行具体的插入操作。

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

autoreleaseFast 函数在执行一个具体的插入操作时,分别对三种情况进行了不同的处理:

【1】当前 page存在且没有满时,直接将对象添加到当前 page中,即 next 指向的位置;
【2】当前page 存在且已满时,创建一个新的page,并将对象添加到新创建的page中;
【3】当前 page不存在时,即还没有 page 时,创建第一个page ,并将对象添加到新创建的 page中。

每调用一次 push 操作就会创建一个新的autoreleasepool ,即往 AutoreleasePoolPage 中插入一个POOL_BOUNDARY ,并且返回插入的POOL_BOUNDARY的内存地址。

[对象 autorelease]操作

通过NSObject.mm 源文件,我们可以找到-autorelease方法的实现:

- (id)autorelease {
    return ((id)self)->rootAutorelease();
}

通过查看((id)self)->rootAutorelease()的方法调用,我们发现最终调用的就是AutoreleasePoolPageautorelease 函数。

objc_object::rootAutorelease()
{
    if (isTaggedPointer()) return (id)this;
    if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;

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

AutoreleasePoolPageautorelease 函数的实现对我们来说就比较容量理解了,它跟 push 操作的实现非常相似。
只不过push操作插入的是一个POOL_SENTINEL ,而 autorelease操作插入的是一个具体的autoreleased 对象。

    static inline id autorelease(id obj)
    {
        assert(obj);
        assert(!obj->isTaggedPointer());
        id *dest __unused = autoreleaseFast(obj);
        assert(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);
        return obj;
    }

objc_autoreleasePoolPop(void *)操作

同理,前面提到的 objc_autoreleasePoolPop(void *) 函数本质上也是调用的AutoreleasePoolPagepop函数。

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

pop 函数的入参就是 push函数的返回值,也就是POOL_SENTINEL的内存地址,即pool token
当执行pop 操作时,内存地址在 pool token之后的所有 autoreleased 对象都会被release
直到pool token所在 pagenext 指向pool token为止。

参考链接

NSAutoreleasePool
https://developer.apple.com/documentation/foundation/nsautoreleasepool

对官方文档中NSAutoreleasePool的个人翻译
http://blog.youkuaiyun.com/wiki_su/article/details/77198341

关于NSAutoReleasePool的理解
http://eleda.iteye.com/blog/1108700

Objective-C Autorelease Pool 的实现原理
http://www.cocoachina.com/ios/20150610/12093.html
(这篇文章的runtime源代码是老版本的了,本文的代码是新版的runtime源码。)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值