Objective-C内存管理与块、ARC和非ARC

6

我已经使用块一段时间了,但我觉得在ARC和非ARC环境中都会错过内存管理方面的一些事情。我认为更深入的理解将使我避免许多内存泄漏。

在特定应用程序中,AFNetworking是我主要使用块的工具。大多数时候,在操作的完成处理程序内部,我会执行类似于“[self.myArray addObject]”这样的操作。

根据Apple的这篇文章,在ARC和非ARC启用的环境中,“self”将被保留下来。

这意味着每当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的那一行,显示“在此块中强烈捕获“self”可能导致保留循环。Xcode是正确的:失败块会保留self,而self持有自己的块的副本,因此两者都不会被释放。
然而,我有以下问题/观察结果。
1)如果将_failureBlock(error)更改为“self.failureBlock(error)”(不包含引号),编译器就会停止抱怨。为什么?这是编译器遗漏的内存泄漏吗?
2)通常,在使用实例变量块时,无论是ARC还是非ARC启用的环境中,最佳实践是什么?看起来,在AFNetworking中完成和失败块不是实例变量,因此它们可能不属于我上面描述的保留循环类别。但是,在AFNetworking中使用进度块时,如何避免像上面那样的保留循环呢?
我很想听听其他人对ARC和非ARC与块以及内存管理问题/解决方案的想法。我发现这些情况容易出错,我觉得有必要讨论一下这个问题以澄清事情。
我不知道这是否重要,但我使用具有最新LLVM的Xcode 4.4。
2个回答

6

如果我把_failureBlock(error) 改成 "self.failureBlock(error)"(不带引号),编译器就不再报错。这是为什么?这会导致编译器忽略内存泄漏吗?

两种方式都存在保留循环。如果你的目标平台是iOS5及以上版本,可以传递一个弱引用给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中使用进度块时,有什么方法可以避免像上面那样的保留循环呢?

大多数情况下,我认为 最好不要将块存储在实例变量中。相反,您可以从类中的方法返回块,这样您仍然会存在保留循环,但它只存在于调用方法到块释放的时间段内。因此,它将防止实例在块执行期间被解除分配,但是保留循环在块释放时结束:

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

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

谢谢你的回答。这给了我很多思考的食物。顺便说一下,你关于回调和后台线程导致崩溃的说法是正确的。然而,在我的情况下,所有的完成块都在主线程中调用,以避免这样的问题。至于另一件事,从方法中返回块的解决方案来保留循环,这并不能解决创建可重用组件的基本问题,其中调用者将决定完成块。 - csotiriou
如果您正在使用块回调公开API,则无法防止调用者创建自己的保留循环。您的API应该注意在完成后释放回调块,并且可能应该公开一个取消方法,该方法也将释放回调块。实际上,大多数调用者都会内联实现回调,而不是在属性中实现。在这种情况下,内存语义与从方法返回块相同--当他们调用您的API时,保留循环开始,并在您的API释放回调块时结束。 - Christopher Pickslay

2
这意味着每当调用AFNetworking网络操作的完成块时,self都会在该块内被保留,并在该块超出范围时释放。 但实际上,当块创建时,块会保留self,并在块被释放时释放。 对于失败的块而言,它保留了self,而self则持有自己的块副本,因此两者都不会被释放。保留self的块是传递给setCompletionBlockWithSuccess的完成块。self没有持有对此块的引用。相反,self.operation(可能是某种NSOperation)在执行时保留块。因此,存在暂时的循环。但是,当操作执行完毕时,循环将被打破。 如果我将_failureBlock(error)更改为"self.failureBlock(error)"(不带引号),编译器就不再抱怨。这是为什么?这是编译器忽略的内存泄漏吗?实际上应该没有区别。self在两种情况下都被捕获。编译器不能保证捕获所有保留周期的情况。

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