为什么在ARC下为解决基于块的保留循环需要复制指针?

4
在ARC下,如果你在块内使用self,就会怀疑一个块引起了循环引用。
我看到了一个解决方法(在此处),像这样: enter image description here 这个解决方法如何防止循环引用? weakRequest只是指向由request引用的确切相同对象的指针。当ARC修改weakRequestrequest的保留计数时,它影响的是同一个对象。
然后,在块中,有这样奇怪的事情发生:
__strong ASIHTTPRequest *strongRequest = weakRequest;

这相当于说:
ASIHTTPRequest *strongRequest = weakRequest;
[strongRequest retain];

但是,一件事情需要注意:它只是一个对象。为什么要有这么多不同的变量名称呢?它们只是指针!
我从来没有太在意块并试图避免使用它们。但现在我很好奇当大家说“一个块捕获变量”时到底在讲什么。直到今天,我认为这只是意味着块将保留您在块范围之外定义的每个指针,这意味着块仅仅会保留您在其中触碰到的任何对象。
我进行了这个快速测试:
UIView *v = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
[self.view addSubview:v];
v.backgroundColor = [UIColor orangeColor];

NSLog(@"self = %p", self); // 0x6a12a40

[UIView animateWithDuration:1.5 
                      delay:0
                    options:UIViewAnimationOptionAllowUserInteraction
                 animations:^{
                     UIViewController *my = self;
                     NSLog(@"my = %p", my); // 0x6a12a40
                     v.frame = CGRectMake(200, 200, 100, 100);
                 }
                 completion:nil];

正如你所看到的,对象本身保持完全相同。块不会创建副本。因此,我可以安全地假定所有关于C和Objective-C的知识仍然有效:

ASIHTTPRequest *strongRequest = internetRequest;
ASIHTTPRequest *foo = strongRequest;
ASIHTTPRequest *bar = foo;

if (bar == internetRequest) {
    NSLog(@"exact same thing, of course");
}

那么发生了什么呢?如果所有的事情都只是为同一个对象创建不同的指针,那么这怎么能解决保留计数问题呢?为什么要走那么多弯路去创建这些指针呢?

难道这不完全一样吗?

[request setCompletionBlock:^{
    NSString *respondeString = [request responseString];
    if ([_delegate respondsToSelector:@selector(pingSuccessful:)]) {
        [_delegate pingSuccessful:responseString];
    }
}];

关于Objective-C,可能有一些秘密可以解释为什么在这里复制指针可以解决内存管理问题。但对我来说,这根本就没有任何意义。

3个回答

6
实际上这与ARC无关,而是与块捕获变量的方式有关。指针被复制,以便由块所捕获的变量具有正确的所有权限定符。
weakRequest只是指向与request相同的对象的指针。当ARC修改weakRequest或request的保留计数时,它影响相同的对象。
没错,它们都指向同一个对象,但weakRequest具有__unsafe_unretained所有权限定符,这意味着当该变量被块捕获时,其保留计数不会改变。
如果request被块捕获,那么它就会被保留,无论你是否使用ARC,都会导致保留环。
将其转换回__strong指针只是为了在块执行期间保持该对象处于活动状态。

我可能说错了,但我认为块内的 __strong 指针不能让它在块的执行期间一直存在。ARC 唯一能做到这一点的方法是保留对象,这会创建一个保留循环,特别是因为 ARC 不知道块的寿命(最有可能它是被存储以供稍后执行)。如果它确实保留了块(我认为它没有),那么在块内添加 __strong 变量完全破坏了块外 __weak 变量的目的。 - Aaron Hayman
很好的解释!@Aaron,我认为保留周期的问题在于如果你创建了一个块并且很长时间不使用它,那么保留的对象可能直到块被执行并释放对象之前都不会消失。在这种情况下,现在对我来说有意义了:请求对象在块开始时被保留,之后被释放,保留周期被打破。当然,这也开启了请求对象在块执行之前就被释放的可能性,如果块延迟执行的话。对吧? - Proud Member
使用__weak引用的主要原因是因为您将块存储以便稍后执行。我有自定义控件,它们需要一个块,并将该块存储为iVar。这些控件通常由提供块的对象保留,因此如果块保留了该对象,则会出现保留循环...另外,如果在块内实例化的任何变量保留了该对象,则只要该变量存在,您就会再次出现保留循环。这就是为什么我认为将__strong变量分配给传入的__weak变量不起作用,因为它不会增加对象的保留计数。 - Aaron Hayman

2

当你将一个变量指定为 __weak 时,确切的意思是这个 block 不会对它进行保留,以避免出现保留循环。然而,在 block 内部创建一个 __strong 变量并将其指向 __weak 变量完全是多余的。你将其指定为 weak 是为了让 block 不会对其进行保留。创建一个新的并指定为 __strong 的变量并没有什么意义,因为没有任何情况需要 block 对其进行保留。__strong 只是编译器关键字,告诉 ARC 如果需要则保留该值,但由于已经传递到 block 中,ARC 永远不会发现那个需要。最终,你可以简单地使用 weakRequest 变量,并且放弃 strongRequest 变量。


2

你可能会感到困惑,因为有两个不同的问题需要解决。你引用了这句话:

__strong ASIHTTPRequest *strongRequest = weakRequest;

这行代码不能防止保留循环。

潜在的保留循环是一个问题。该保留循环包含三个对象:selfASIHTTPRequest和块。使用weakRequest变量打破了该循环,因为该块捕获了weakRequest,它不拥有ASIHTTPRequest对象。从引用计数的角度来看,分配weakRequest不会增加ASIHTTPRequest的引用计数。

你引用的那行代码是为了解决第一个问题而存在的另一个问题。另一个问题是潜在的悬空指针。由于weakRequest不拥有ASIHTTPRequest,所以在完成块的执行期间,所有ASIHTTPRequest的所有者都可能释放它。然后,weakRequest将成为一个悬空指针 - 指向已释放的对象的指针。任何使用它都可能导致崩溃或堆栈损坏。

在你引用的那一行中,该块将weakRequest复制到strongRequest。因为strongRequest__strong,所以编译器会生成代码来保留(增加)ASIHTTPRequest的引用计数,并在块结束时释放它。这意味着即使所有其他ASIHTTPRequest的所有者在块运行时释放它,ASIHTTPRequest也将保持活动状态,因为该块已暂时使自己成为请求的所有者。

请注意,此解决方案不是线程安全的。如果请求的所有者可以从其他线程释放它,则存在竞争条件,可能仍导致悬空指针。这就是为什么应尽量使用__weak而不是__unsafe_unretained用于弱指针的原因:__weak引用可以被复制到__strong引用中,而不会出现竞争条件。


我不相信如果所引用的对象被销毁,__weak 请求变量会成为悬空指针。相反,它将指向 nil。只有在使用 __unsafe_unretained 时才会出现悬挂问题。 - Aaron Hayman
此外,即使您使用__weak而不是__unsafe_unretained,复制到强引用也是必要的,因为否则__weak引用随时可能变为nil。使用__weak而不是__unsafe_unretained仅仅是防止使用僵尸引用和避免竞争条件。 - rob mayoff
1
一个临时的保留循环是可以的。你只需要确保在循环中的所有对象应该被释放的时候,保留循环将会被打破。在示例代码中,编译器在块结束时生成了strongRequest的释放,这打破了分配给strongRequest所创建的保留循环。 - rob mayoff
嗨,Rob,你说这是线程安全的,但这篇文章的作者在最后一段中说它不是。请查看http://blog.random-ideas.net/?p=160#comment-1158 - lockedscope
在他的最后一段中,他说最后一个模式解决了该文章中显示的所有线程安全问题。因此,我认为他所指的最后一个线程安全问题是它并不完全线程安全,我们仍然需要考虑代码完全线程安全所需的同步问题。但是,在块中捕获self时,它是线程安全的。我是对的吗? - lockedscope
显示剩余3条评论

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