为什么ARC仍然需要@autoreleasepool?

mk1*_*k12 191 memory-management objective-c xcode4.2 automatic-ref-counting

在大多数情况下使用ARC(自动引用计数),我们不需要考虑使用Objective-C对象的内存管理.不允许再创建NSAutoreleasePools,但是有一个新的语法:

@autoreleasepool {
    …
}
Run Code Online (Sandbox Code Playgroud)

我的问题是,当我不应该手动释放/自动释放时,为什么我需要这个呢?


编辑:总结我从所有的答案和评论中得到的简洁:

新语法:

@autoreleasepool { … } 是新的语法

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
…
[pool drain];
Run Code Online (Sandbox Code Playgroud)

更重要的是:

  • ARC使用autorelease以及release.
  • 它需要一个自动释放池才能这样做.
  • ARC不会为您创建自动释放池.然而:
    • 每个Cocoa应用程序的主线程都有一个自动释放池.
  • 有两种情况你可能想要使用@autoreleasepool:
    1. 当您在辅助线程中并且没有自动释放池时,您必须自己制作以防止泄漏,例如myRunLoop(…) { @autoreleasepool { … } return success; }.
    2. 当你想创建一个更本地的游泳池时,正如@mattjgalloway在他的回答中所示.

mat*_*way 212

ARC没有摆脱保留,发布和自动释放,它只是为您添加所需的.所以仍有调用保留,仍有调用释放,仍有调用自动释放,仍有自动释放池.

他们使用新的Clang 3.0编译器和ARC进行的其他一项更改是NSAutoReleasePool@autoreleasepool编译器指令替换它们.NSAutoReleasePool无论如何,它总是有点特殊的"对象",并且它们使得使用它的语法不会与对象混淆,因此它通常更简单一些.

所以基本上,你需要@autoreleasepool因为仍然需要担心自动释放池.您只需要担心添加autorelease呼叫.

使用自动释放池的示例:

- (void)useALoadOfNumbers {
    for (int j = 0; j < 10000; ++j) {
        @autoreleasepool {
            for (int i = 0; i < 10000; ++i) {
                NSNumber *number = [NSNumber numberWithInt:(i+j)];
                NSLog(@"number = %p", number);
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

一个非常人为的例子,当然,如果你没有@autoreleasepool在外部for循环中,那么你将在以后释放100000000个对象,而不是每次围绕外循环释放10000个for.

更新: 另请参阅此答案 - /sf/answers/556544551/ - 为什么@autoreleasepool与ARC无关.

更新: 我看了一下这里发生了什么,并在我的博客上写下来.如果您查看那里,那么您将看到ARC正在做什么以及@autoreleasepool编译器如何使用新样式及其如何引入范围来推断需要保留,释放和自动释放的信息.

  • 它没有摆脱保留.它会为您添加它们.引用计数仍在进行中,它只是自动计算.因此自动参考计数:-D. (11认同)
  • 那么为什么不为我添加`@ autoreleasepool`呢?如果我不控制自动释放或释放的内容(ARC为我做了这些),我应该如何知道何时设置自动释放池? (6认同)
  • 循环示例在没有自动释放的情况下完美地工作:当变量超出范围时,每个对象都被释放.在没有自动释放的情况下运行代码会占用一定量的内存,并显示指针被重用,并且在对象的dealloc上放置一个断点,表明每次调用objc_storeStrong时都会调用它一次.也许OSX在这里做了一些蠢事,但在iOS上完全没有autoreleasepool. (6认同)
  • 但您可以控制自动发布池的位置.默认情况下,整个应用程序都有一个,但您可能需要更多. (5认同)
  • 好问题.你只需要"知道".考虑添加一个类似于为什么人们可能会在GC语言中向垃圾收集器添加提示以继续并立即运行收集周期.也许你知道有大量的对象准备被清除,你有一个循环来分配一堆临时对象,所以你"知道"(或者工具可能会告诉你:)在循环周围添加一个发布池将是一个好主意. (5认同)

out*_*tis 15

@autoreleasepool不会自动释放任何东西.它创建一个自动释放池,以便在到达块结束时,在块处于活动状态时由ARC自动释放的任何对象将被发送释放消息.Apple的高级内存管理编程指南解释如下:

在自动释放池块的末尾,在块中接收到自动释放消息的对象被发送释放消息 - 对象在每次在块内发送自动释放消息时接收释放消息.


nac*_*o4d 7

人们经常误解ARC进行某种垃圾收集等.事实是,经过一段时间后人们在苹果(感谢LLVM和铛项目)意识到,Objective-C的内存管理(所有retainsreleases等),可以完全在自动化的编译时间.这就是通过阅读代码,甚至在它运行之前!:)

为了做到这一点,只有一个条件:我们必须遵循规则,否则编译器将无法在编译时自动化该过程.因此,为了确保我们永远不会打破规则,我们是不允许明确写入release,retain等这些调用由编译器自动注入到我们的代码.因此,在内部我们还是有autoreleaseS, retain,release等它只是我们并不需要把它们写下去了.

ARC的A在编译时是自动的,这比在垃圾收集时的运行时要好得多.

我们仍然拥有@autoreleasepool{...}它,因为它没有违反任何规则,我们可以随时创建/消耗我们的池我们需要它:).


Mec*_*cki 6

从方法返回新创建的对象需要自动释放池。例如考虑这段代码:

- (NSString *)messageOfTheDay {
    return [[NSString alloc] initWithFormat:@"Hello %@!", self.username];
}
Run Code Online (Sandbox Code Playgroud)

在该方法中创建的字符串的保留计数将为 1。现在谁来平衡保留计数和释放计数?

方法本身?不可能,它必须返回创建的对象,因此在返回之前不能释放它。

该方法的调用者?调用者并不期望检索一个需要释放的对象,方法名称并不暗示创建一个新对象,它只是表示返回一个对象,并且这个返回的对象可能是一个需要释放的新对象,但也可能是我们可能是现有的一个,但没有。该方法返回的内容甚至可能取决于某些内部状态,因此调用者无法知道是否必须释放该对象,并且不必关心。

如果调用者必须始终按照约定释放所有返回的对象,则每个不是新创建的对象在从方法返回之前都必须保留,并且一旦超出范围,调用者就必须将其释放,除非它再次被返回。在许多情况下,这将是非常低效的,因为如果调用者并不总是释放返回的对象,则在许多情况下可以完全避免更改保留计数。

这就是为什么有自动释放池,所以第一个方法实际上会变成

- (NSString *)messageOfTheDay {
    NSString * res = [[NSString alloc] initWithFormat:@"Hello %@!", self.username];
    return [res autorelease];
}
Run Code Online (Sandbox Code Playgroud)

调用autorelease一个对象会将其添加到自动释放池中,但这到底意味着什么,将对象添加到自动释放池中?好吧,这意味着告诉你的系统“我希望你为我释放该对象,但在以后的某个时间,而不是现在;它有一个保留计数,需要通过释放来平衡,否则内存会泄漏,但我自己不能这样做现在,因为我需要该对象在当前范围之外保持活动状态,并且我的调用者也不会为我执行此操作,所以它不知道需要执行此操作。因此,将其添加到您的池中,一旦您清理了该对象泳池,还帮我清理我的物体。

使用 ARC,编译器可以为您决定何时保留对象、何时释放对象以及何时将其添加到自动释放池,但它仍然需要自动释放池的存在,以便能够从方法返回新创建的对象而不泄漏内存。苹果刚刚对生成的代码做了一些巧妙的优化,有时会在运行时消除自动释放池。这些优化要求调用者和被调用者都使用 ARC(请记住,混合 ARC 和非 ARC 是合法的,并且也是官方支持的),并且是否确实如此只能在运行时知道。

考虑这个 ARC 代码:

// Callee
- (SomeObject *)getSomeObject {
    return [[SomeObject alloc] init];
}

// Caller
SomeObject * obj = [self getSomeObject];
[obj doStuff];
Run Code Online (Sandbox Code Playgroud)

系统生成的代码可以像以下代码一样运行(这是允许您自由混合 ARC 和非 ARC 代码的安全版本):

// Callee
- (SomeObject *)getSomeObject {
    return [[[SomeObject alloc] init] autorelease];
}

// Caller
SomeObject * obj = [[self getSomeObject] retain];
[obj doStuff];
[obj release];
Run Code Online (Sandbox Code Playgroud)

(注意调用者中的保留/释放只是一种防御性安全保留,不是严格要求的,没有它代码将完全正确)

或者它可以像下面的代码一样运行,以防在运行时检测到两者都使用 ARC:

// Callee
- (SomeObject *)getSomeObject {
    return [[SomeObject alloc] init];
}

// Caller
SomeObject * obj = [self getSomeObject];
[obj doStuff];
[obj release];
Run Code Online (Sandbox Code Playgroud)

正如你所看到的,Apple 消除了 atuorelease,因此也消除了池被销毁时的延迟对象释放,以及安全保留。要了解更多关于这是如何实现的以及幕后到底发生了什么,请查看这篇博客文章。

现在讨论实际问题:为什么要使用@autoreleasepool

对于大多数开发人员来说,现在在代码中使用此构造的原因只有一个,那就是在适用的情况下保持较小的内存占用。例如考虑这个循环:

for (int i = 0; i < 1000000; i++) {
    // ... code ...
    TempObject * to = [TempObject tempObjectForData:...];
    // ... do something with to ...
}
Run Code Online (Sandbox Code Playgroud)

假设每次调用都tempObjectForData可能创建一个TempObject返回 autorelease 的新对象。for 循环将创建一百万个临时对象,这些对象全部收集在当前的自动释放池中,并且只有在该池被销毁后,所有临时对象也会被销毁。在此之前,内存中将有一百万个临时对象。

如果你写这样的代码:

for (int i = 0; i < 1000000; i++) @autoreleasepool {
    // ... code ...
    TempObject * to = [TempObject tempObjectForData:...];
    // ... do something with to ...
}
Run Code Online (Sandbox Code Playgroud)

然后,每次 for 循环运行时都会创建一个新池,并在每次循环迭代结束时销毁。这样,尽管循环运行了一百万次,但任何时候最多有一个临时对象挂在内存中。

过去,在管理线程(例如使用NSThread)时,您通常还必须自己管理自动释放池,因为只有主线程会自动拥有 Cocoa/UIKit 应用程序的自动释放池。然而,这在今天几乎是遗留下来的,因为今天您可能不会一开始就使用线程。您可以使用 GCDDispatchQueueNSOperationQueue,这两个都为您管理顶级自动释放池,在运行块/任务之前创建并在完成后销毁。