在从UIViewController调用的非保留完成块中引用self时,是否真的需要使用weakSelf/strongSelf技巧?

40

假设我有一个在UIViewController子类中的以下方法:

- (void)makeAsyncNetworkCall
{
    [self.networkService performAsyncNetworkCallWithCompletion:^{
        dispatch_async(dispatch_get_main_queue(), ^{
                [self.activityIndicatorView stopAnimating];
            }
        });
    }];
}

我知道在这个块内引用self会导致UIViewController实例被块保留。只要performAsyncNetworkCallWithCompletion没有将块存储在NetworkService的属性(或ivar)中,那么我认为没有循环引用,对吗?

我意识到上面的结构将导致UIViewController被保留,直到performAsyncNetworkCallWithCompletion完成,即使它已经被系统提前释放。但是,系统是否可能(甚至可以?)在任何情况下释放我的UIViewControlleriOS 6更改了UIViewController后备CALayer内存管理方式之后)?

如果有必须使用“weakSelf/strongSelf”技巧的原因,它应该是这样的:

- (void)makeAsyncNetworkCall
{
    __weak typeof(self) weakSelf = self;
    [self.networkService performAsyncNetworkCallWithCompletion:^{
        typeof(weakSelf) strongSelf = weakSelf;
        if (!strongSelf) {
            return;
        }
        dispatch_async(dispatch_get_main_queue(), ^{
                [strongSelf.activityIndicatorView stopAnimating];
            }
        });
    }];
}

但我觉得这太丑了,如果不必要的话,我想避免使用它。


1
不需要使用strongSelf。如果self已被释放,weakSelf将为nil,这是可以的。 - Patrick Goley
1
是的,在这个例子中是正确的,但请注意接受答案末尾的警告。 - Robert Atkins
4个回答

34

我相信你的诊断是正确的,使用 self 在这种情况下不一定会导致强引用循环。但是这将在网络操作完成时保留视图控制器,在大多数情况下,这是没有必要的。因此,可能不需要使用 weakSelf,但是这样做可能是明智的。它最小化了意外强引用循环的机会,并导致更有效地使用内存(释放与视图控制器关联的内存,直到该视图控制器被解除后才释放,而不是在网络操作完成后不必要地保留视图控制器)。

但是,没有必要使用 strongSelf 结构,你可以:

- (void)makeAsyncNetworkCall
{
    __weak typeof(self) weakSelf = self;
    [self.networkService performAsyncNetworkCallWithCompletion:^{
        dispatch_async(dispatch_get_main_queue(), ^{
            [weakSelf.activityIndicatorView stopAnimating];
        });
    }];
}

仅在必须要有强引用(例如,您正在取消引用实例变量)或者需要担心竞争条件的情况下,才需要使用weakSelf/strongSelf组合。这似乎不是这种情况。


6
顺便提一句,如果您的网络操作很耗时(可能是因为规模或请求数量),并且您不想它们在视图控制器消失后继续执行,除了使用weakSelf模式之外,您还可以考虑使您的网络操作可取消,并在视图控制器消失时取消任何待处理的请求。使用可取消的基于NSOperation的请求的操作队列,而不是GCD,可以促进该过程。 - Rob
我认为你是对的,直到我写了一个测试代码,该代码在 AFNetworking 方法中执行块,如下所示:关闭网络,使用 AFNetworking 推送视图控制器请求,弹出视图控制器,打开网络。无论我使用 self 还是 weakSelf,该块都会被执行。因此,我猜测当您使用块执行方法时,self 被块对象捕获并强制保留。因此,weakSelf 技巧没有用处。 - Gon
1
首先,“weakSelf”模式不会影响块是否运行。它会一直运行,除非您取消它。其次,“weakSelf”只是指示该块不会对“self”保持强引用(但显然,它是否被释放取决于是否解决了所有其他强引用)。如果您有违反既定的“weakSelf”模式的示例,请使用MCVE发布您自己的问题。 - Rob
由于此块,networkService 不会有任何强引用循环。现在,有时(甚至经常)网络对象被设计为在某些请求的持续时间内保留自己(这是一个非常有用的模式),但这是 networkService 本身的内部实现细节,与上述内容无关。但是,上面显示的代码不会对 networkService 保持强引用。 - Rob
是的,没有保留循环。我的观点是使用weakSelf并不能确保在弹出时视图控制器被释放。还有其他条件需要考虑,比如self.networkService - Gon
显示剩余2条评论

8
我认为问题在于networkService可能会对block保持强引用。而视图控制器可能会对networkService有强引用。因此VC->NetworkService->block->VC的循环引用是可能存在的。然而,在这种情况下,通常可以安全地假设block在运行后将被释放,这样循环就会被打破。所以,在这种情况下是不必要的。
如果block没有被释放,则需要使用弱引用。例如,不是只运行一次网络调用的block,而是被用作回调的block。也就是说,networkService对象对block保持强引用,并将其用于所有回调。在这种情况下,block将对VC进行强引用,从而创建一个强引用循环,因此最好使用弱引用。

在这种特定情况下,我编写了NetworkService,因此我知道它不会保留该块。我明白如果我没有看到那个代码,我不能依靠它。 - Robert Atkins
我的观点是,在考虑是否需要使用weakSelf结构时,这就是你应该思考的内容。 - Abizern

3
不,如果您的self.networkService没有将其用作块属性,则应该没问题。

你能详细说明UIViewController可能会被保留比必要时间更长,以及这是否重要吗?这是问题的实质。特别感谢提供权威资料的链接(如NSHipster、Mike Ash、objc.io、@bbum等):-)。 - Robert Atkins
为什么UIViewController会被保留比必要的时间更长?iOS 6中的新功能是视图控制器不会自动卸载其视图。这与保留视图控制器对象本身无关。 - eofster
2
@AlexeiKuznetsov 如果用户在网络操作仍在进行时关闭视图控制器,如果网络操作的完成块引用了视图控制器self,则视图控制器直到网络操作完成之前都不会被释放。虽然通常不是一个重大问题,但如果您有一堆积压的网络操作或者它是一个非常漫长的网络操作,这可能会加剧问题。网络操作没有理由保持对视图控制器的强引用,因此建议使用weakSelf模式。 - Rob
在最简单的情况下,如果回调函数中只有对self的调用,那么是比较容易的。但是,如果回调函数除了引用self之外还做其他事情,可能会产生副作用,因为程序员可能不希望self突然变成nil,控制器也不存在了。因此,为了避免这些情况,我们可能仍然需要检查控制器的当前状态。你认为呢? - eofster
如果开发者采用了weakSelf模式(该模式专门设计用于处理视图控制器被释放的情况),我对“可能不希望self突然变成nil”的理论并不同情。但是,如果开发者正在做很多事情,并希望确保一旦完成块开始,weakSelf不会在某些竞争条件中被释放,那么他可能会采用更繁琐的weakSelf/strongSelf模式,检查strongSelf是否不为nil。但是,在大多数情况下,仅使用weakSelf就足够了。 - Rob

1
答案并不那么简单。我同意@Rob的回答,但它需要额外的解释:
  1. __weak 被认为是一种安全的方式,因为当释放时它会将 self 设为 nil,这意味着如果回调发生在调用对象已被释放并被块引用的情况下,例如从堆栈中弹出的 UIViewController,则不会出现异常。添加取消任何类型操作的可能性仅仅是卫生和资源问题。例如,您也可以取消 NSURLConnection,不仅限于可以取消 NSOperation,您可以在回调到块的方法中异步执行任何内容。

  2. 如果让块保留 self,则当调用对象(如 UIViewController)由 UINavigationController 释放并且块仍然保留它并进行回调时,故事可能会变得有点复杂。在这种情况下,回调块将被执行,并假定某些数据将由其结果更改。这可能是期望的行为,但在大多数情况下不是。因此,在这种情况下,操作的取消可能更加重要,在 UINavigationControllerDelegate 方法中非常明智地使用可变集合中的异步任务来取消与 UINavigationController 作为关联对象或单例存在的任务。

当然,如果您不希望异步操作在取消调用对象后继续执行,那么第一种选择是最安全的。


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