带有块、ARC 和非 ARC 的 Objective C 内存管理

Posted

技术标签:

【中文标题】带有块、ARC 和非 ARC 的 Objective C 内存管理【英文标题】:Objective C memory management with blocks, ARC and non-ARC 【发布时间】:2012-07-27 11:37:09 【问题描述】:

我使用块已经有一段时间了,但我觉得在 ARC 和非 ARC 环境中的内存管理方面我有些怀念。我觉得更深入的理解会让我避免很多内存泄漏。

AFNetworking 是我在特定应用程序中主要使用 Blocks。大多数时候,在操作的完成处理程序中,我会执行“[self.myArray addObject]”之类的操作。

在启用 ARC 和未启用 ARC 的环境中,“self”将根据this article from Apple 保留。

这意味着每当调用 AFNetworking 网络操作的完成块时,self 会保留在该块内,并在该块超出范围时释放。我相信这适用于 ARC 和非 ARC。我已经运行了 Leaks 工具和 Static Analyzer,以便我可以发现任何内存泄漏。没有任何显示。

但是,直到最近我才偶然发现了一个我无法理解的警告。我在这个特定的例子中使用了 ARC。

我有两个实例变量指示网络操作的完成和失败

@property (nonatomic, readwrite, copy) SFCompletionBlock completionBlock;
@property (nonatomic, readwrite, copy) SFFailureBlock failureBlock;
@synthesize failureBlock = _failureBlock;
@synthesize operation = _operation;

在代码的某处,我这样做:

[self.operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id
                                                    responseObject) 
NSError *error = [NSError errorWithDomain:@"com.test" code:100 userInfo:@@"description": @"zero results"];
            _failureBlock(error);
         failure:^(AFHTTPRequestOperation *operation, NSError *error) 
            NSLog(@"nothing");
        ];

Xcode 抱怨调用failureBlock 的那一行,消息“Capturing "self" strong in this block is likely to result in a retain cycle。我相信Xcode是对的:失败块保留self,self持有它的块的自己的副本,因此两者都不会被释放。

但是,我有以下问题/意见。

1) 如果我将 _failureBlock(error) 更改为“self.failureBlock(error)”(不带引号),编译器将停止抱怨。这是为什么?这是编译器遗漏的内存泄漏吗?

2) 一般来说,在使用作为实例变量的块时,在启用 ARC 和非启用 ARC 的环境中使用块的最佳做法是什么?似乎在 AFNetworking 中的完成和失败块的情况下,这两个块是 not 实例变量,因此它们可能不属于我上面描述的保留周期类别。但是当在 AFNetworking 中使用进度块时,可以做些什么来避免像上面那样的保留循环?

我很想听听其他人对 ARC 和非 ARC 的想法,包括内存管理的块和问题/解决方案。我发现这些情况很容易出错,我觉得有必要对此进行一些讨论以澄清问题。

我不知道这是否重要,但我使用 Xcode 4.4 和最新的 LLVM。

【问题讨论】:

【参考方案1】:

这意味着每当一个 AFNetworking 网络的完成块 调用操作,self 保留在该块内,然后释放 当该块超出范围时。

不,self 在创建块时由块保留。并且在块被释放时释放。

我相信 Xcode 是对的:失败块保留了 self,而 self 拥有自己的块副本,因此两者都不会 已解除分配。

保留self 的块是传递给setCompletionBlockWithSuccess 的完成块。 self 不包含对此块的引用。相反,self.operation(可能是某种NSOperation)在执行时保留块。所以暂时有一个循环。但是,当操作执行完成时,循环将被打破。

1) 如果我将 _failureBlock(error) 更改为“self.failureBlock(error)” (不带引号)编译器停止抱怨。这是为什么?这是 编译器遗漏的内存泄漏?

应该没有区别。 self 在这两种情况下都会被捕获。编译器不能保证捕获所有保留循环的情况。

【讨论】:

【参考方案2】:

1) 如果我将 _failureBlock(error) 更改为“self.failureBlock(error)” (不带引号)编译器停止抱怨。这是为什么?这是 编译器遗漏的内存泄漏?

在这两种情况下都存在保留循环。如果你的目标是 ios 5+,你可以传入一个对 self 的弱引用:

__weak MyClass *weakSelf;
[self.operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) 
    NSError *error = [NSError errorWithDomain:@"com.test" code:100 userInfo:@@"description": @"zero results"];
    if (weakSelf.failureBlock) weakSelf.failureBlock(error);
 failure:^(AFHTTPRequestOperation *operation, NSError *error) 
    NSLog(@"nothing");
];

现在 self 不会被保留,如果在调用回调之前释放它,则回调是空操作。但是,它可能会在后台线程上调用回调时进行释放,因此这种模式可能会导致偶尔的崩溃。

2) 一般来说,在这两种情况下使用块的最佳做法是什么 使用 ARC 和非 ARC 启用的环境 实例变量?似乎在完成和失败的情况下 AFNetworking 中的块,这两个块不是实例变量,所以 他们可能不属于我的保留周期类别 如上所述。但是当在 AFNetworking 中使用进度块时, 可以做些什么来避免像上面那样的保留循环?

大多数时候,我认为it's better not to store blocks in instance variables。如果您从类中的方法返回块,您仍然会有一个保留周期,但它只存在于调用方法到释放块的时间。所以它会阻止你的实例在块执行期间被释放,但是当块被释放时保留周期结束:

-(SFCompletionBlock)completionBlock 
    return ^(AFHTTPRequestOperation *operation , id responseObject ) 
        [self doSomethingWithOperation:operation];
    ;


[self.operation setCompletionBlockWithSuccess:[self completionBlock]
                                      failure:[self failureBlock]
];

【讨论】:

感谢您的回答。这值得深思。在旁注中,您所说的关于回调和导致崩溃的后台线程是正确的。但是,在我的情况下,所有完成块都在主线程中调用,以避免此类问题。至于另一件事,“从方法返回块”解决方案以保留循环,这并没有解决创建可重用组件的基本问题,调用者将决定完成块。 如果您使用块回调公开 API,则无法阻止调用者创建自己的保留周期。您的 API 应该注意在完成回调块后立即释放它们,并且可能应该公开一个取消方法,该方法也将释放回调块。实际上,大多数调用者无论如何都会内联实现回调,而不是在属性中。在这种情况下,内存语义与从方法返回块相同——保留周期从它们调用您的 API 时开始,并在您的 API 释放回调块时结束。

以上是关于带有块、ARC 和非 ARC 的 Objective C 内存管理的主要内容,如果未能解决你的问题,请参考以下文章

ARC和非ARC文件混编

__block在ARC和非ARC下有什么不同

iOS开发之ARC与非ARC的设置

ARC 不允许将非 Objective-C 指针类型“const char *”隐式转换为“id”

带有 ASIHTTPRequest 的 ARC

使用为 ARC 配置的 XMLRPC 库,收到 ARC 错误