为什么在ARC下仍然需要使用@autoreleasepool?

207

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

@autoreleasepool {
    …
}
我的问题是,当我不应该手动释放/自动释放时,为什么我需要这个?
编辑: 简要总结所有答案和评论得出的结论: 新语法: @autoreleasepool { … }新语法
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];[pool drain];

更重要的是:

  • ARC还使用autoreleaserelease
  • 为此,它需要一个自动释放池。
  • ARC不会为您创建自动释放池。 不过:
    • 每个Cocoa应用程序的主线程中已经有一个自动释放池。
  • 有两种情况可以使用@autoreleasepool
    1. 当您在次要线程中且没有自动释放池时,您必须创建自己的自动释放池以防止内存泄漏,例如:myRunLoop(…) { @autoreleasepool { … } return success; }
    2. 当您想创建一个更局部的池时,就像 @mattjgalloway 在他的答案中展示的那样。

2
还有第三种情况:当您开发的内容与UIKit或NSFoundation无关时。例如使用命令行工具等。 - Garnik
8个回答

231

ARC并没有摆脱保留、释放和自动释放池,它只是为您添加了必需的调用。因此仍然会调用retain、release、autorelease以及自动释放池。

他们在新的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);
            }
        }
    }
}
一个极度牵强的例子,但是如果你没有在外部循环里加上@autoreleasepool,那么你最后会释放1亿个对象而不是每次循环只释放1万个对象。

更新:还可以看看这个答案:https://dev59.com/MWsz5IYBdhLWcg3wWmjL#7950636,了解为什么@autoreleasepool与ARC无关。

更新:我深入研究了这里正在发生的事情,并在我的博客上写了篇文章。如果你在那里看一下,就会看到ARC具体做了什么以及新式@autoreleasepool如何引入一个作用域供编译器推断需要保留、释放和自动释放哪些对象。

14
它不会消除保留,它会为您添加保留。引用计数仍在进行,只是自动执行。因此称之为“自动引用计数” :-D。 - mattjgalloway
7
那么为什么它不为我添加@autoreleasepool呢?如果我不控制什么将被自动释放(因为ARC会替我完成),那么我怎么知道何时需要设置自动释放池呢? - mk12
6
你仍然可以控制自动释放池的位置。默认情况下,整个应用程序都被一个自动释放池包围,但你可能需要更多的自动释放池。 - mattjgalloway
6
好问题。你需要“知道”。想象一下,添加一个释放池就像在GC语言中为垃圾回收器添加提示来立即运行收集周期一样。也许你知道有大量的对象可以清理,你有一个分配了大量临时对象的循环,所以你“知道”(或者 Instruments 可能会告诉你 :) 在循环周围添加一个释放池是个好主意。 - Graham Perks
7
即使没有自动释放池,循环示例也可以完美运行:每个对象在变量超出作用域时都会被释放。在不使用自动释放池的情况下运行代码将占用恒定的内存,并显示指针被重复使用。在对象的dealloc上设置断点将显示它在每次循环中被调用一次,当调用objc_storeStrong时。也许OSX在这里做了一些蠢事,但在iOS上自动释放池是完全不必要的。 - Glenn Maynard
显示剩余11条评论

18
@autoreleasepool并不会自动释放任何对象。它会创建一个自动释放池,以便在块结束时,由ARC在块活动期间自动释放的任何对象都会收到一条release消息。根据Apple的《高级内存管理编程指南》的解释:

在自动释放池块的末尾,块中接收autorelease消息的对象将收到一条release消息——每次在块中接收到autorelease消息,对象就会相应地接收到一条release消息。


1
不一定。对象将会接收到一个release消息,但如果保留计数大于1,则该对象将不会被释放。 - andybons
@andybons:已更新;谢谢。这是与ARC之前的行为有所不同吗? - outis
这是不正确的。由ARC释放的对象将在被ARC释放时立即发送释放消息,无论是否有自动释放池。 - Glenn Maynard

9
人们经常误解ARC是一种垃圾回收或类似的东西。事实上,经过一段时间,苹果公司的人(感谢llvm和clang项目)意识到Objective-C的内存管理(所有的retainsreleases等)可以在编译时完全自动化。也就是说,仅通过阅读代码,甚至在运行之前就可以做到这一点! :)
为了做到这一点,只有一个条件:我们必须遵循规则,否则编译器将无法在编译时自动化处理。因此,为了确保我们永远不会违反规则,不允许我们显式地写releaseretain等。这些调用是由编译器自动注入到我们的代码中的。因此,在内部,我们仍然有autoreleaseretainrelease等。只是我们不再需要写它们了。
ARC中的A是在编译时自动的,这比像垃圾回收那样在运行时更好。

我们仍然有@autoreleasepool{...},因为它不违反任何规则,我们可以自由地在需要时创建/释放我们的池 :)


1
ARC是引用计数垃圾回收机制,不同于JavaScript和Java中的标记-清除垃圾回收机制,但它确实是一种垃圾回收机制。这并没有回答问题——“你可以”并不能回答“为什么应该”的问题。你不应该这样做。 - Glenn Maynard

6

自动释放池在从方法返回新创建的对象时是必需的。例如,考虑以下代码:

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

这个方法创建的字符串其保留计数(retain count)为1。现在,谁来平衡保留计数和释放?

这个方法本身吗?不可能,因为它必须返回被创建的对象,所以在返回之前不能释放它。

调用该方法的人呢?调用者不希望检索需要释放的对象,该方法名并不意味着创建了一个新对象,它只表示返回了一个对象,而这个返回的对象可能是需要释放的新对象,但也可能是现有的无需释放的对象。方法返回的内容甚至可能取决于某些内部状态,因此调用者无法知道是否必须释放该对象,也不应该关心。

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

这就是为什么有autorelease池存在,所以第一个方法实际上将会变成:

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

调用autorelease方法将一个对象添加到自动释放池中,但这到底意味着什么,将一个对象添加到自动释放池中?这意味着告诉系统:“我希望你在稍后的某个时间释放该对象,而不是现在;它有一个需要通过释放来平衡保留计数的引用计数,否则会造成内存泄漏,但我现在无法做到这一点,因为我需要该对象继续存在于我的当前范围之外,并且我的调用者也不会这样做,他不知道需要这样做。所以将其添加到您的池中,一旦清理该池,也请替我清理我的对象。

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

考虑以下ARC代码:

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

// Caller
SomeObject * obj = [self getSomeObject];
[obj doStuff];

系统生成的代码可以像以下代码一样运行(这是安全版本,允许您自由混合ARC和非ARC代码):
// Callee
- (SomeObject *)getSomeObject {
    return [[[SomeObject alloc] init] autorelease];
}

// Caller
SomeObject * obj = [[self getSomeObject] retain];
[obj doStuff];
[obj release];

(Note the retain/release in the caller is just a defensive safety retain, it's not strictly required, the code would be perfectly correct without it)
或者它可以像这段代码一样运行,以防在运行时检测到两者都使用ARC:
// Callee
- (SomeObject *)getSomeObject {
    return [[SomeObject alloc] init];
}

// Caller
SomeObject * obj = [self getSomeObject];
[obj doStuff];
[obj release];

如您所见,苹果公司取消了自动释放池,因此也消除了当池被销毁时延迟对象释放以及安全保留。要了解更多关于这是如何实现的以及幕后究竟发生了什么,请查看这篇博客文章
现在来到实际问题:为什么要使用@autoreleasepool
对于大多数开发人员而言,今天只有一个原因可以在其代码中使用这个结构,那就是在适用的情况下使内存占用小。例如,请考虑以下循环:
for (int i = 0; i < 1000000; i++) {
    // ... code ...
    TempObject * to = [TempObject tempObjectForData:...];
    // ... do something with to ...
}

假设每次调用tempObjectForData都会创建一个新的TempObject,并返回自动释放池。for循环将创建一百万个这样的临时对象,这些对象都被收集在当前自动释放池中,只有在销毁该池时,所有临时对象才会被销毁。在此之前,您在内存中拥有一百万个这样的临时对象。
如果您像这样编写代码:
for (int i = 0; i < 1000000; i++) @autoreleasepool {
    // ... code ...
    TempObject * to = [TempObject tempObjectForData:...];
    // ... do something with to ...
}

每次for循环运行时都会创建一个新的池,并在每个循环迭代结束时销毁该池。这样,尽管循环运行一百万次,最多只有一个临时对象挂在内存中。
过去,当管理线程(例如使用NSThread)时,您经常需要自己管理autorelease池,因为只有主线程自动为Cocoa / UIKit应用程序提供autorelease池。然而,这几乎已经成为遗留问题了,因为现在您可能根本不会使用线程。您将使用GCD的DispatchQueue或NSOperationQueue,这两者都会为您管理顶级autorelease池,在运行块/任务之前创建并在完成后销毁。

3
引用自https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/mmAutoreleasePools.html

自动释放池块和线程

在Cocoa应用中,每个线程都维护着自己的自动释放池块堆栈。如果你正在编写一个仅基于Foundation库的程序,或者你分离了一个线程,那么你需要创建自己的自动释放池块。

如果你的应用程序或线程是长期运行并可能生成大量自动释放对象,则应使用自动释放池块(例如AppKit和UIKit在线程主线程上所做的那样);否则,自动释放对象会累积,并且内存占用量会增加。如果您的分离线程没有使用Cocoa调用,则不需要使用自动释放池块。

注:如果您使用POSIX线程API而不是NSThread创建辅助线程,则除非Cocoa处于多线程模式,否则无法使用Cocoa。 Cocoa仅在分离其第一个NSThread对象后进入多线程模式。要在次要POSIX线程上使用Cocoa,您的应用程序必须首先分离至少一个NSThread对象,该对象可以立即退出。您可以使用NSThread类方法isMultiThreaded测试Cocoa是否处于多线程模式。

...

在自动引用计数(ARC)中,系统使用与MRR相同的引用计数系统,但它会在编译时为你插入适当的内存管理方法调用。强烈建议您在新项目中使用ARC。如果您使用ARC,则通常不需要理解本文档中描述的底层实现,尽管在某些情况下可能会有所帮助。有关ARC更多信息,请参见过渡到ARC发布说明。


3

这是因为你仍然需要向编译器提供关于何时可以安全地让自动释放的对象离开作用域的提示。


使用自动释放池 - rob mayoff
@Mk12 - 哦,那我很抱歉,是的,你需要一个自动释放池。但如果你使用GCD,一切都会神奇地工作(因为它会为你创建一个自动释放池)。很抱歉我错过了关于次要线程的部分 :-/. - mattjgalloway
@mattjgalloway - 好的,准确来说,它不仅仅是一个“提示”。但是@autoreleasepool不是一条运行时指令。它是一个预编译器指令。来自苹果文档的描述:“这个简单结构允许编译器推断引用计数的状态”。 - DougW
2
@DougW - 我深入研究了编译器实际上正在做什么,并在这里写了一篇博客文章 - http://iphone.galloway.me.uk/2012/02/a-look-under-arcs-hood-–-episode-3/。希望能够解释编译时和运行时发生的事情。 - mattjgalloway
@mattjgalloway - 很好的写作。我认为我们都是意见一致的,我相信有些人对比我所提供的更详细的解释感兴趣,所以感谢您提供它。 - DougW
显示剩余6条评论

2

TL;DR

为什么在ARC下还需要@autoreleasepool?

@autoreleasepool被Objective-C和Swift用于处理内部的autorelese

当您使用纯Swift并分配Swift对象时,ARC会处理它

但是,如果您决定调用/使用Foundation / Legacy Objective-C codeNSDataData),该代码内部使用autorelease,那么@autoreleasepool就会派上用场。

//Swift
let imageData = try! Data(contentsOf: url)

//Data init uses Objective-C code with [NSData dataWithContentsOfURL] which uses `autorelese`

长答案

MRC, ARC, GC

手动引用计数(MRC)手动保留-释放(MRR),作为开发人员,您需要手动计算对象的引用计数。

自动引用计数(ARC)在iOS v5.0和OS X Mountain Lion中引入,使用xCode v4.2。

垃圾收集(GC)适用于Mac OS,并在OS X Mountain Lion中被弃用。必须转移到ARC。

MRC和ARC中的引用计数

//MRC
NSLog(@"Retain Count: %d", [variable retainCount]);

//ARC
NSLog(@"Retain Count: %ld", CFGetRetainCount((__bridge CFTypeRef) variable));

堆中的每个对象都有一个整数值,表示指向它的引用计数。当引用计数等于0时,对象将被系统解除分配。

  • 分配对象
  • 使用引用计数
  • 解除分配对象。当retainCount == 0时调用deinit

MRC

A *a1 = [[A alloc] init]; //this A object retainCount = 1
    
A *a2 = a1;
[a2 retain]; //this A object retainCount = 2

// a1, a2 -> object in heap with retainCount

释放对象的正确方法:

  1. release 如果只使用这个 - 会出现悬空指针。因为它仍然可以指向堆中的对象,而且可能会发送消息。
  2. = nil 如果只使用这个 - 会出现内存泄漏。deinit不会被调用。
A *a = [[A alloc] init]; //++retainCount = 1
[a release]; //--retainCount = 0
a = nil; //guarantees that even somebody else has a reference to the object, and we try to send some message thought variable `a` this message will be just skipped

使用引用计数(对象所有者规则):

  • (0 -> 1) allocnewcopymutableCopy
  • (+1) retain:您可以拥有一个对象多次(可以多次调用retain
  • (-1) release:如果您是对象的所有者,则必须释放它。如果您释放的次数超过了retainCount,则它将变为0
  • (-1) autorelease:将应该被释放的对象添加到autorelease pool中。此池将在RunLoop迭代周期结束时(也就是当堆栈上的所有任务都完成时)[关于]进行处理,之后将对池中的所有对象应用release
  • (-1) @autoreleasepool:强制在块结束时处理自动释放池。当您在循环中处理autorelease并希望尽快清除资源时,可以使用它。如果不这样做,您的内存占用量将不断增加

在方法调用中使用autorelease,当您在其中分配并返回一个新对象时。

- (B *)foo {
    B *b1 = [[B alloc] init]; //retainCount = 1

    //fix - correct way - add it to fix wrong way
    //[b1 autorelease];

    //wrong way(without fix)
    return b; 
}

- (void)testFoo {
    B *b2 = [a foo];
    [b2 retain]; //retainCount = 2
    //some logic
    [b2 release]; //retainCount = 1
    
    //Memory Leak
}

@autoreleasepool示例

- (void)testFoo {
    for(i=0; i<100; i++) {
        B *b2 = [a foo];
        //process b2
    }
}

ARC

自动引用计数(ARC)的最大优势之一是它会在编译时自动插入retainreleaseautorelease,作为开发者,您不再需要关心这些细节。

启用/禁用 ARC

//enable
-fobjc-arc
//disable
-fno-objc-arc

优先级由高到低的变体

//1. local file - most priority
Build Phases -> Compile Sources -> Compiler Flags(Select files -> Enter) 

//2. global
Build Settings -> Other C Flags(OTHER_CFLAGS)

//3. global
Build Settings -> Objective-C Automatic Reference Counting(CLANG_ENABLE_OBJC_ARC)

检查ARC是否已启用/禁用

使用预处理器 __has_feature函数。

__has_feature(objc_arc)

编译时间

// error if ARC is Off. Force to enable ARC
#if  ! __has_feature(objc_arc)
    #error Please enable ARC for this file
#endif

//or

// error if ARC is On. Force to disable ARC
#if  __has_feature(objc_arc)
    #error Please disable ARC for this file
#endif

运行时

#if __has_feature(objc_arc)
    // ARC is On
    NSLog(@"ARC on");
#else
    // ARC is Off
    NSLog(@"ARC off");
#endif

逆向工程(针对Objective-C)

//ARC is enabled
otool -I -v <binary_path> | grep "<mrc_message>"
//e.g.
otool -I -v "/Users/alex/ARC_experiments.app/ARC_experiments"  | grep "_objc_release"

//result
0x00000001000080e0   748 _objc_release

//<mrc_message>
_objc_retain
_objc_release
_objc_autoreleaseReturnValue
_objc_retainAutoreleaseReturnValue
_objc_retainAutoreleasedReturnValue
_objc_storeStrong

迁移Objective-C MRC到ARC的工具

ARC会生成错误提示,您需要手动删除retainreleaseautorelease等问题。

Edit -> Convert -> To Objective-C ARC...

使用MRC的新Xcode

如果您启用MRC,您将收到以下错误(警告)(但构建将成功):

//release/retain/autorelease/retainCount
'release' is unavailable: not available in automatic reference counting mode
ARC forbids explicit message send of 'release'

-4

关于这个话题似乎存在很多困惑(至少有80个人现在可能对此感到困惑,并认为他们需要在代码中添加@autoreleasepool)。

如果一个项目(包括其依赖项)完全使用ARC,则永远不需要使用@autoreleasepool,因为ARC会在正确的时间释放对象。例如:

@interface Testing: NSObject
+ (void) test;
@end

@implementation Testing
- (void) dealloc { NSLog(@"dealloc"); }

+ (void) test
{
    while(true) NSLog(@"p = %p", [Testing new]);
}
@end

显示:

p = 0x17696f80
dealloc
p = 0x17570a90
dealloc

每个测试对象在值超出范围时立即被释放,而不必等待自动释放池退出。(NSNumber示例也是如此;这只是让我们观察dealloc。)ARC不使用autorelease。

@autoreleasepool仍然允许的原因是为了混合ARC和非ARC项目,这些项目尚未完全过渡到ARC。

如果调用非ARC代码,可能会返回一个自动释放的对象。在这种情况下,上面的循环将泄漏,因为当前的自动释放池永远不会退出。这就是你需要在代码块周围放置@autoreleasepool的地方。

但是,如果您已经完全完成了ARC转换,那么请忘记autoreleasepool。


5
这个答案是错误的,也与 ARC 文档相违背。你的证据是个人经验,因为你使用了编译器决定不自动释放的分配方法。如果你为自定义类创建新的静态初始化器并在循环中使用它:“+ (Testing *) testing { return [Testing new] }”,你会很容易地看到这种方法不起作用。然后,dealloc 直到稍后才会被调用。如果你将循环内部的代码块包装在一个 @autoreleasepool 块中,就可以解决这个问题。 - Dima
@Dima 在 iOS10 上尝试过,dealloc 在打印对象地址后立即被调用。+ (Testing *) testing { return [Testing new];} + (void) test { while(true) NSLog(@"p = %p", [self testing]);} - KudoCC
@KudoCC - 我也是这样,我看到了你看到的相同行为。但是,当我将[UIImage imageWithData]加入到方程式中时,突然间,我开始看到传统的autorelease行为,需要@autoreleasepool来保持峰值内存在一定合理水平。 - Rob
@Rob 我忍不住要加上这个链接 - KudoCC

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接