在ARC中,总是将self的弱引用传递到块中?

269

我对Objective-C中块的使用有些困惑。我目前正在使用ARC,我的应用程序中有相当多的块,目前始终引用self而不是它的弱引用。这可能导致这些块保留self并阻止其被释放吗?问题是,在块中是否应始终使用selfweak引用?

-(void)handleNewerData:(NSArray *)arr
{
    ProcessOperation *operation =
    [[ProcessOperation alloc] initWithDataToProcess:arr
                                         completion:^(NSMutableArray *rows) {
        dispatch_async(dispatch_get_main_queue(), ^{
            [self updateFeed:arr rows:rows];
        });
    }];
    [dataProcessQueue addOperation:operation];
}

ProcessOperation.h

@interface ProcessOperation : NSOperation
{
    NSMutableArray *dataArr;
    NSMutableArray *rowHeightsArr;
    void (^callback)(NSMutableArray *rows);
}

ProcessOperation.m

-(id)initWithDataToProcess:(NSArray *)data completion:(void (^)(NSMutableArray *rows))cb{

    if(self =[super init]){
        dataArr = [NSMutableArray arrayWithArray:data];
        rowHeightsArr = [NSMutableArray new];
        callback = cb;
    }
    return self;
}

- (void)main {
    @autoreleasepool {
        ...
        callback(rowHeightsArr);
    }
}

如果您想深入探讨这个主题,请阅读http://dhoerl.wordpress.com/2013/04/23/i-finally-figured-out-weakself-and-strongself/。 - David H
6个回答

741

专注于讨论的或者部分并没有帮助。相反,应该关注循环部分。

保留循环是一个循环,当对象A保留对象B,并且对象B保留对象A时发生。在这种情况下,如果任何一个对象被释放:

  • 对象A不会被释放,因为对象B持有对它的引用。
  • 但只要对象A有一个引用,对象B就永远不会被解除分配。
  • 但只要对象B有一个引用,对象A就永远不会被解除分配。
  • 无限循环

因此,即使一切正常,这两个对象也将在程序的生命周期内停留在内存中,而不会被解除分配。

因此,我们担心的是保留循环,块本身并不会导致这些循环的出现。例如,这不是一个问题:

[myArray enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop){
   [self doSomethingWithObject:obj];
}];

该块保留self,但self不保留该块。如果其中一个被释放,则不会创建循环引用,并且所有内容都会按照应有的方式被释放。

问题出现在以下情况:

//In the interface:
@property (strong) void(^myBlock)(id obj, NSUInteger idx, BOOL *stop);

//In the implementation:
[self setMyBlock:^(id obj, NSUInteger idx, BOOL *stop) {
  [self doSomethingWithObj:obj];     
}];

现在,你的对象(self)明确地对该块有一个强引用。该块对self具有隐式的强引用。那就是一个循环,现在两个对象都无法得到正确释放。

因为在这种情况下,self按定义已经对该块有一个强引用,最简单的解决方法通常是为块创建一个弱引用以便使用:

__weak MyObject *weakSelf = self;
[self setMyBlock:^(id obj, NSUInteger idx, BOOL *stop) {
  [weakSelf doSomethingWithObj:obj];     
}];

但是当处理调用self的块时,这不应该成为您遵循的默认模式!这只应在必要时用于打破self和块之间本来会形成的保留周期。如果您到处采用这种模式,您将面临将块传递给在self被解除分配后执行的某些内容的风险。

//SUSPICIOUS EXAMPLE:
__weak MyObject *weakSelf = self;
[[SomeOtherObject alloc] initWithCompletion:^{
  //By the time this gets called, "weakSelf" might be nil because it's not retained!
  [weakSelf doSomething];
}];

2
我不确定A保留B,B保留A是否会产生无限循环。就引用计数的角度而言,A和B的引用计数均为1。导致保留环的原因是当没有其他组强引用A和B时 -- 这意味着我们无法访问这两个对象(无法控制A释放B反之亦然),因此,A和B相互引用以保持彼此存活。 - Danyun Liu
@Danyun 虽然 A 和 B 之间的保留循环在所有对这些对象的其他引用被释放之前不是不可恢复的,但这并不意味着它不再是一个循环。相反,仅因为特定的循环可能是可恢复的,并不意味着在您的代码中拥有它就可以了。保留循环是糟糕设计的一种迹象。 - jemmons
@jemmons 是的,我们应该尽可能避免保留循环设计。 - Danyun Liu
1
@Master 我无法确定。这完全取决于您的-setCompleteionBlockWithSuccess:failure:方法的实现方式。但是,如果paginatorViewController拥有,并且这些块在ViewController释放后不会被调用,则使用__weak引用将是安全的选择(因为self拥有拥有这些块的东西,所以即使它们不保留它,当块调用它时仍然可能存在)。但这有很多“如果”。这真的取决于这个功能的目的是什么。 - jemmons
1
@Jai 不,这正是块/闭包内存管理问题的核心所在。当没有任何对象拥有它们时,对象会被释放。MyObjectSomeOtherObject都拥有该块。但由于该块对MyObject的引用是弱引用,因此该块不拥有MyObject。因此,虽然只要MyObjectSomeOtherObject存在,就保证该块存在,但不能保证MyObject与该块同时存在。MyObject可能已经完全被释放,只要SomeOtherObject仍然存在,该块仍将存在。 - jemmons
显示剩余7条评论

33

我完全同意@jemmons的观点:

但是在处理调用self的块时,这不应该成为您遵循的默认模式!这只应该用于打破本来会导致self和块之间保留循环的情况。如果您在任何地方都采用了这种模式,那么您可能会在将块传递给一些在self被清除后执行的内容时出现问题。

//SUSPICIOUS EXAMPLE:
__weak MyObject *weakSelf = self;
[[SomeOtherObject alloc] initWithCompletion:^{
  //By the time this gets called, "weakSelf" might be nil because it's not  retained!
  [weakSelf doSomething];
}];
为了解决这个问题,可以在块内定义对 weakSelf 的强引用:
__weak MyObject *weakSelf = self;
[[SomeOtherObject alloc] initWithCompletion:^{
  MyObject *strongSelf = weakSelf;
  [strongSelf doSomething];
}];

3
strongSelf会增加weakSelf的引用计数,从而形成一个循环引用吗? - mskw
12
只有当保留循环存在于对象的静态状态中时,它们才是重要的。在代码执行并且其状态处于流动状态时,多个可能重复的保留是可以的。关于这种模式,捕获强引用不会对self在块运行之前被释放的情况产生影响,这种情况仍然可能发生。但是它确保了self在执行块时不会被释放。如果块本身执行异步操作,则这一点很重要,因为这会给出一个自我释放的机会窗口。 - Pierre Houston
@smallduck,你的解释很好。现在我更好地理解了这个问题。书上没有涉及到这点,谢谢。 - Huibin Zhang
6
这不是 strongSelf 的一个好的例子,因为明确添加 strongSelf 的作用与运行时本身所做的一样:在 doSomething 这一行,会在整个方法调用期间获取一个强引用。如果 weakSelf 已经失效,强引用就为空并且方法调用不会执行。使用 strongSelf 帮助的地方在于当你有一系列操作或访问成员字段(->),你想保证你实际上得到了一个有效的引用,并且在整个一系列操作中保持它的持续性,例如if ( strongSelf ) { /* 多个操作 */ } - Ethan

26

您不必总是使用弱引用。如果您的块没有被保留,但执行后被丢弃,则可以强制捕获self,因为它不会创建保留周期。在某些情况下,甚至希望该块保持self直到块完成,以避免过早释放。但是,如果您强制捕获块,并在内部捕获self,则会创建保留周期。


我只是将块作为回调执行,而且我不希望self被dealloc。但似乎我创建了保留循环,因为相关的视图控制器没有被dealloc。 - the_critic
1
例如,如果您有一个由视图控制器保留的工具栏按钮项,并且在该块中强烈捕获了self,则会产生保留循环。 - Léo Natan
1
@MartinE。你应该更新你的问题并附上代码示例。Leo是正确的(+1),它不一定会导致强引用循环,但这取决于你如何使用这些块。如果你提供代码片段,我们帮助你会更容易。 - Rob
@LeoNatan 我理解保留循环的概念,但我不太确定在块中会发生什么,所以这让我有点困惑。 - the_critic
在上面的代码中,一旦操作完成并释放块,您的self实例将被释放。您应该了解块是如何工作以及它们在其范围内捕获什么和何时捕获。 - Léo Natan
显示剩余4条评论

19
正如Leo所指出的那样,您在问题中添加的代码不会暗示强引用循环(也称为保留循环)。可能会导致强引用循环的一个与操作相关的问题是,如果该操作没有被释放。虽然您的代码片段表明您尚未将操作定义为并发,但如果您这样做了,并且从未发布isFinished,或者存在循环依赖关系等情况,则不会释放它。如果操作没有被释放,视图控制器也不会被释放。我建议在操作的dealloc方法中添加断点或NSLog,并确认是否已调用。

您说:

我理解保留循环的概念,但不太确定在块中会发生什么,所以有些困惑

块所遇到的保留循环(强引用循环)问题与您熟悉的保留循环问题一样。块将保持对块中出现的任何对象的强引用,并且在块本身被释放之前,它不会释放这些强引用。因此,如果块引用self,甚至只是引用self的实例变量,那么就会维护对self的强引用,直到块被释放(或在这种情况下,直到NSOperation子类被释放)。
有关更多信息,请参见《Objective-C编程:使用块》文档中的捕获self时避免强引用循环部分。
如果您的视图控制器仍未被释放,您需要确定未解决的强引用存在于哪里(假设您已确认NSOperation已被释放)。一个常见的例子是使用重复的NSTimer。或者一些自定义的delegate或其他错误地保持strong引用的对象。您通常可以使用Instruments跟踪对象获取其强引用的位置,例如:

record reference counts in Xcode 6

或者在 Xcode 5 中:

record reference counts in Xcode 5


1
另一个例子是,如果操作在块创建者中保留,并且一旦完成就不会释放。对于这篇好的写作,点赞! - Léo Natan
@LeoNatan 同意,尽管代码片段将其表示为本地变量,如果他正在使用ARC,则会被释放。但你是完全正确的! - Rob
1
是的,我只是举了一个例子,因为OP在另一个答案中请求这样做。 - Léo Natan
顺便提一下,Xcode 8有“Debug Memory Graph”,这是一种更简单的方法来查找未被释放的对象的强引用。请参见https://dev59.com/F10Z5IYBdhLWcg3w3DUW#30993476。 - Rob

0

有些解释忽略了关于保留循环的条件[如果一组对象通过强引用形成一个圆形关系,即使没有来自该组外部的强引用,它们也会相互保持存活状态]。欲了解更多信息,请阅读文档


-2

这是如何在块内使用self的方法:

//调用块

 NSString *returnedText= checkIfOutsideMethodIsCalled(self);

NSString* (^checkIfOutsideMethodIsCalled)(*)=^NSString*(id obj)
{
             [obj MethodNameYouWantToCall]; // this is how it will call the object 
            return @"Called";


};

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