ARC __block and __weak

8

假设我正在尝试从块内访问self

[someObject successBlock:^(NSArray *result) {
    [self someSuccessMethod];
} failure:^(NSString *errorMessage, int status) {
    [self someFailureMethod];
}];

我知道这会创建一个保留环,someObjectself 永远不会被释放。

让我感到困惑的是在有/没有使用 __block 关键字时实际发生了什么。我可以通过对 self 进行 __weak 引用来修复保留环:

__weak MyClass* me = self;
[someObject successBlock:^(NSArray *result) {
    [me someSuccessMethod];
} failure:^(NSString *errorMessage, int status) {
    [me someFailureMethod];
}];

我这里不需要使用__block,因为我不打算在块中修改me。据我所知,如果我不使用__block,那么块内部会引用me的一份拷贝。我的问题是:如果在块内部引用的只是对象的一个拷贝,为什么原始的代码块会创建保留循环呢?我猜测对self的引用只是一个拷贝,因为我从未使用__block关键字。我是否对此有错误的理解?

4个回答

7
在第一种情况下,该块捕获了self,即将self保存为另一个强引用指针。这增加了指向对象的保留计数,并导致保留循环。
在第二种情况下,该块捕获me,即将me保存为另一个弱引用指针。这不会增加保留计数,因此不会导致保留循环。
(如果您在块外部和内部打印me地址,您将看到地址不同。该块具有其自己的弱指针指向该对象。)
如果所指向的对象被释放,Objective-C运行时将把所有弱引用(包括块保存的引用)设置为nil
(我只是希望我理解得正确。)

假设MyClass实现的是真正的副本,因为-copyWithZone:只能保留...这是完全合法的,并且在几乎所有不可变对象中都会这样做。 - Grady Player
@GradyPlayer:也许我表达不太清楚,但我的意思是该块在其块上下文中使用“当前内容”(即“self”或“me”)保存强(或弱)指针。并未涉及“copy”方法。 - Martin R
有时候SO会在别人对它们做些事情时将某些内容重新置顶... 有时候几个月甚至几年后我会找一个技术宅仔挑刺... 但对象可以在块捕获时被复制,所以我认为这并不是错误的... - Grady Player
@GradyPlayer:你认为那是不正确的吗?还是你认为那是正确的? - Martin R
我认为捕获一个对象可以但不一定会导致块中的对象具有新地址。 - Grady Player
@GradyPlayer:你是在考虑指针本身的地址,还是指向的对象的地址?我认为捕获(指向)一个对象不会导致对象被复制(在copy方法的意义上)。- 但当然我可能错了... - Martin R

4
当两个对象相互保留强引用时,就会发生“保留循环”(retain cycle)。最简单的情况是对象a保留了对对象b的强引用,而b则做出了相反的操作[1]。在Objective-C中,保留循环是一个问题,因为它使ARC相信这些对象始终在使用,即使这些对象没有从任何其他地方引用。
让我们来看一些例子。你有一个对象z,它分配了a和b,利用它们,然后处理它们。如果a和b在第一次创建时之间创建了一个保留循环,那么a和b就不会被解除分配。如果你这样做几次,你将严重泄漏内存。
另一个现实世界的保留循环例子是,如果a分配并强烈引用b对象,但你还从b到a存储了一个强引用(对象图中的许多较小对象可能需要访问它们的父对象)。
在这种情况下,最常见的解决方法是确保包含的对象只对其包含对象具有弱引用,并确保兄弟对象不相互包含强引用。
另一种解决方案(通常不太优雅,但在某些情况下可能适用)是在`a`中拥有某种自定义的`cleanup`方法,将其对`b`的引用设置为nil。因此,当调用`cleanup`时(如果`b`没有被其他地方强引用),`b`将被释放。这很麻烦,因为您无法从`a`的`dealloc`中执行此操作(如果存在保留循环,则永远不会调用它),并且因为您必须记住在适当的时间调用`cleanup`。
请注意,保留循环也是传递性的(例如,对象`a`强引用`b`,`b`强引用`c`,`c`强引用`a`)。

总之,块的内存管理相当棘手,很难理解。

你的第一个示例可能会创建一个临时保留循环(仅在self对象存储对someObject的强引用时才会发生)。当块完成执行并被释放时,这个临时保留循环就会消失。

在执行期间,self将存储对someObject的引用,someObject将引用block,而block又将引用self。但是,这只是暂时的,因为块没有永久存储在任何地方(除非[someObject successBlock:failure:]实现了这一点,但这在完成块中并不频繁发生)。

因此,在您的第一个示例中,保留循环不是问题。

通常情况下,块中的保留循环问题只有在某个对象存储块而不是直接执行它时才会出现。然后很容易看到self强引用了block,而block又强引用了self。请注意,在块内从任何ivar访问自动在该块中生成对self的强引用。
确保包含对象不会强引用其容器的等效方法是使用__weak SelfClass *weakSelf = self来访问方法和实例变量(如果通过访问器访问实例变量,则更好)。您的块对self的引用将是弱引用(它不是副本,而是弱引用),这将允许self在不再被强引用时被释放。
可以认为始终在所有块内部使用weakSelf是一个好习惯,无论是否存储,以防万一。我想知道为什么苹果没有将其作为默认行为。即使实际上不需要,这样做通常不会对块代码造成任何有害影响。

__block在指向对象的变量上很少使用,因为Objective-C不会强制执行对象的不可变性。

如果您有一个指向对象的指针,您可以调用它的方法,并且这些方法可以使用或不使用__block来修改它。 __block对基本类型(int、float等)的变量更有用(唯一有用?)。请参阅此处以了解在对象指针变量中使用__block时会发生什么。您也可以在苹果的块编程主题中了解更多关于__block的信息。

编辑:修复了关于在对象指针上使用__block的错误。感谢@KevinDiTraglia指出。


1
不错的回答,但你确定最后那句话吗?我正在研究使用 __block 而非 __weak 引用类型时出现的问题,它们有不同的行为,__weak 引用会变成 nil,而 __block 引用则不会。我认为它更接近于对象引用的强指针。 - Kevin DiTraglia
谢谢您的评论,您说得对。我修复了答案的那一部分。 - Ricardo Sanchez-Saez
不确定总是使用弱引用来引用自身是否正确。有时我认为您可能希望该块保留引用,以便它不会被释放。据我所知,只有在使用强引用会导致保留循环时才应使用它。 - User

3
你的第一个例子不会创建一个永无止境的保留循环。肯定会有保留循环,但是一旦块完成,从块到 someObject 的引用将被移除。所以 someObject 至少在块完成之前都还活着。这种临时的保留循环可能是好事也可能是坏事,这取决于你想要什么:
如果您需要 someObject 至少在其块完成之前保持存活,那就没问题。然而,如果没有理由保留该对象,则应使用“弱”引用来实现它。
例如,myObject 是一个视图控制器,在这些块中从网上获取图片。如果你从导航控制器中弹出了 someObject,则控制器将无法在获取图片后显示它,因此没有必要保留它。成功或错误都与此无关,用户对 someObject 所应获取的图片已不再感兴趣。在这种情况下,使用weak是更好的选择,但是块中的代码应该预期 self 可能为空。

1
难道说一旦块完成,对于它们的引用就被移除了,这不是更准确吗? - User
这是确实正确的。+1,因为它解释了为什么不会创建永久保留循环。许多新程序员总是使用weakSelf,因为他们对保留周期的误解就像被接受的答案中所列出的那样。虽然这对大多数应用程序来说没问题,但更复杂的应用程序将看到在块完成执行之前引用被释放的问题,如果您稍后尝试引用这些对象,则会导致崩溃。我认为你在最后一句话中想说的是weakSelf可能为空。 - Bot

0

你可以将self作为块的参数进行传递,确切地给出变量名“self”,这将保护块不会自我保留。

关于“someObject和self永远不会被释放”的说法是错误的:当块被释放时,self也会被释放。块将随着someObject的释放而被释放。当someObject没有更多引用时,它将被释放。因此,如果你的self对象拥有someObject,那么在不再需要它时,请释放someObject。


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