- 在Swift中,如何解决闭包的循环引用?
js
myClosure = { [weak self] in
guard let self = self else { return }
}
- 那么如果是多个闭包嵌套呢?
js
// ✅ 只需要在最外层写一次
myClosure1 = { [weak self] in [weak self]
// 在顶部进行一次安全解包
guard let self = self else { return }
// 在这个作用域里,`self` 是一个临时的强引用,可以安全使用
// 不用担心它被提前释放
myClosure2 = {
// 它会捕获上面 guard let 创建好的、非可选的强引用 self
myClosure3 = {
// 你可以直接、安全地调用 self 的方法或属性
}
}
}
- 那么又有一个问题:在OC中的block,就需要每一层都要弱引用,第二层要先强引用再弱引用,第三层再先强引用再弱引用吧?为什么Swift不是这个逻辑? 这是因为Swift 的逻辑确实和 OC 不一样,它在这方面做了极大的简化和安全性提升。
OC 的核心问题在于: @strongify(self)
创建的那个临时强引用 strongSelf
的生命周期会持续到包含它的那个 block 执行完毕 。如果这个 block 内部又启动了一个长时间的异步任务(第二个 block),那么 strongSelf
会被第二个 block 捕获,导致 self
实例的生命周期被不必要地延长。因此,为了追求最精细的内存管理,开发者才会在每一层异步调用前都重复进行"弱化 -> 强化"的操作。
Swift 的优势在于: guard let self = self
创建的强引用 self
是一个全新的、仅存在于外层闭包作用域内的局部变量 。当 anotherAsyncTask
的闭包(内层闭包)创建时,它捕获的是这个局部的、新的 self
。一旦外层闭包执行完毕(someAsyncTask
的回调结束),这个局部的 self
变量就会被销毁。anotherAsyncTask
的闭包对它的持有也就自然解除了,完全不会影响到原始 self
实例的生命周期。
特性 / 行为 | Objective-C (block ) |
Swift (closure ) |
---|---|---|
弱引用声明 | @weakify(self) 或 __weak typeof(self) weakSelf = self; |
在捕获列表 [weak self] |
临时强引用 | @strongify(self) 或 __strong typeof(weakSelf) strongSelf = weakSelf; |
guard let self = self else { return } |
嵌套捕获 | 内层 block 捕获由 @strongify 创建的 strongSelf ,其生命周期可能过长,导致需要"再次弱化"。 |
内层闭包捕获由 guard let 创建的局部强引用 self 。该局部变量生命周期很短,因此无需再次弱化。 |
开发者操作 | 需要警惕并可能在每一层异步调用前都重复"弱化-强化"的模式。 | 只需要在最外层做一次"弱化-强化" ,内部可以完全放心使用。 |
- 但是在实际开发中,swift代码提示引用报错,往往xcode是这样解决的,为什么?
js
someClosure = { [self] in }
那是因为Xcode 的首要任务是解决编译错误,而不是帮你分析内存管理。 而 [self] in
正是解决这个特定编译错误的"最直接"的语法。它解决了语法问题 ,但它没有解决循环引用的问题 。它只是把一个隐式的强引用,变成了一个显式的强引用。所以解决循环引用问题还是需要[weak self]
。
- 那么在实际开发中
[self]
对于内存泄露来说是错误的呗?
这个说法不完全准确,但您的警惕性非常对!更精确的说法是: 在【会】产生循环引用的场景下使用 [self]
,是绝对错误的,它会直接导致内存泄漏。 但是,在【不会】产生循环引用的场景下,[self]
则是安全、甚至是被推荐的写法。 所以,[self]
本身不是"错误",它只是一个工具。错误的是在不合适的场景下使用了这个工具。
比如下列两种情况
js
// 这个闭包被传递给 UIView.animate,执行完动画后就会被销毁。
// self 并没有一个属性来持有这个闭包。
// 所以 self -> 闭包 这条强引用链不存在。
UIView.animate(withDuration: 0.5) { [self] in
// 在这里使用 [self] in 是【完全正确】的。
// 它明确地告诉编译器:"我知道我在强引用 self,且我确定这是安全的。"
}
// DispatchQueue.main.asyncAfter 的闭包同样是执行完就销毁。
DispatchQueue.main.asyncAfter(deadline: .now() +1.0) { [self] in
// 这里使用 [self] in 也是【完全正确】的。
}
简单的可以理解为"一次性"的工作 (用 [self]
是安全的),"长期"的规则 (必须用 [weak self]
)。
- 那么又有一个场景,在对网络请求进行二次封装的情况下,在调用网络请求时,是否需要弱引用?
答案是不需要的。为什么呢?因为Alamofire临时持有了您的闭包,由于 ViewController
没有持有任何东西,所以闭包无论如何强引用 ViewController
,都构不成一个闭环。因此,这里使用 [self]
来显式强引用是完全安全的。
- 为什么感觉 OC 的 AFNetworking 封装调用时不需要弱引用?
无论是在 OC 的 AFNetworking 还是 Swift 的 Alamofire,它们【内部都没有,也不可能】自动处理您在闭包中捕获 self
导致的循环引用。防止循环引用的责任始终在调用者(也就是您)身上。
- 那为什么您会有"AFN不需要弱引用"的印象呢?
原因和我们上面分析的完全一样:因为您在 OC 中调用 AFN 封装的场景,很可能也属于"一次性"的调用,本身就不会产生循环引用。由于AFHTTPSessionManager的实例 manager
是一个局部变量,方法执行完就释放了,success
block 被 manager
临时持有,执行完也就释放了。所以,当时您在 OC 里不写弱引用是正确的,不是因为 AFNetworking 内部处理了,而是因为您的【用法】决定了它根本没有循环引用!
js
[MyOCNetworkManager requestWithURL:@"" parameters:nil success:^(id responseObject) {
// 您在这里直接使用 self,比如 [self.tableView reloadData];
// 并没有写 __weak typeof(self) weakSelf = self;
} failure:^(NSError *error) {
// ...
}];
- 那么在OC+AFNetworking的二次封装回调时,如果我依然写弱引用的话,会有问题吗?
完全没有问题,这样做在内存上是绝对安全的,但同样,它也是不必要的,并且有轻微的副作用。 在那个场景下,编写弱引用代码(即 weak/strong dance
)是"安全但多余的"。
我们来对比一下两种写法:
- 在您调用一个"一次性"的网络请求封装时,闭包不会被您的
ViewController
持有,因此不存在循环引用。
js
[MyOCNetworkManager requestWithURL:@""
parameters:nil
success:^(id responseObject) {
// 直接使用 self,闭包会强引用 self。
// 因为没有循环引用,self 会在闭包执行完后被正常释放,这是安全的。
[self.tableView reloadData];
}
failure:nil];
这种写法简洁、清晰,并且正确地表达了意图:"这是一个一次性的任务,我需要 self
在任务执行时是存在的。"
- 如果您坚持使用弱引用,代码会是这样:
js
__weak typeof(self) weakSelf = self;
[MyOCNetworkManager requestWithURL:@"/some/path"
parameters:nil
success:^(id responseObject) {
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) {
return;
}
// 使用 strongSelf 来确保在闭包执行期间 self 不会被释放
[strongSelf.tableView reloadData];
}
failure:nil];
- 内存安全吗?
是的,100%安全。弱引用永远不会增加引用计数,所以它绝不会"创造"出一个循环引用。从这个角度看,它没有任何"问题"。
- 有什么副作用或缺点吗?
有,和 Swift 的情况完全相同:
代码变得冗余:为了一个没有必要的安全措施,您多写了三行样板代码 (__weak
, __strong
, if
)。这降低了代码的简洁性。
意图变得模糊:当其他开发者读到这段代码时,他们会看到 weak/strong dance
,这通常是一个强烈的信号,表示"这里有循环引用的风险"。他们可能会因此花时间去寻找一个实际并不存在的风险点,增加了维护成本。
极端情况下的功能差异:如果 self
(比如一个 UIViewController
) 在网络请求发出后、但在回调执行前被释放了(例如用户快速返回上一个页面),那么:
写法一(强引用) :self
会被闭包"续命",直到闭包执行完毕。[self.tableView reloadData]
会被执行。
写法二(弱引用) :self
会被立即释放。当回调执行时,strongSelf
会是 nil
,代码会直接 return
,[strongSelf.tableView reloadData]
不会被执行。
js
[MyOCNetworkManager requestWithURL:@""
parameters:nil
success:^(id responseObject) {
// 直接使用 self,闭包会强引用 self。
// 因为没有循环引用,self 会在闭包执行完后被正常释放,这是安全的。
[self.tableView reloadData];
}
failure:nil];
js
__weak typeof(self) weakSelf = self;
[MyOCNetworkManager requestWithURL:@""
parameters:nil
success:^(id responseObject) {
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) {
return;
}
// 使用 strongSelf 来确保在闭包执行期间 self 不会被释放
[strongSelf.tableView reloadData];
}
failure:nil];
- 极端情况下的功能差异:如果 self (比如一个 UIViewController) 在网络请求发出后、但在回调执行前被释放了(例如用户快速返回上一个页面),写法二(弱引用):self 会被立即释放。当回调执行时,strongSelf 会是 nil,代码会直接 return,[strongSelf.tableView reloadData] 不会被执行。 但是实际中的这种写法很常见,我以前就是这么写,但是没有发现什么问题啊?返回页面还是会执行dealloc,为什么?
- 大多数情况下,行为差异"无关紧要"
我们回到最常见的场景:网络请求回来后,更新界面。
Objective-C
objectivec
// 弱引用写法
__weak typeof(self) weakSelf = self;
[MyNetworkManager request:^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) {
return; // 如果 self 销毁了,就直接返回
}
[strongSelf.tableView reloadData]; // 更新界面
}];
设想一下用户快速返回,self
(也就是 ViewController
) 被销毁了。这时网络回调回来了,因为 strongSelf
是 nil
,所以 [strongSelf.tableView reloadData]
这行代码没有被执行**。
这对用户来说是问题吗?完全不是。 因为界面都已经消失了,tableView
也不存在了,去刷新一个不存在的界面本来就是一件没有意义的事情。代码不执行,反而更干净利落。
所以,在 99% 的 UI 更新场景中,弱引用导致的"代码不执行"这个行为差异,不仅不是问题,反而是我们期望的、最合理的结果。
- "快速返回"的极端情况发生概率低
要触发这个"功能差异",需要满足一个条件:self
的销毁发生在"网络请求发出后"和"回调执行前"这个短暂的时间窗口内。
对于大多数响应速度很快的 API 来说,这个窗口可能只有几百毫秒。用户需要操作得非常快才能正好卡在这个时间点上。因此,在日常测试和使用中,这个情况本身就不容易遇到。
- 即使代码不执行,也无可见负面影响
假设回调里做的事情是 [self hideLoadingIndicator]
。如果用户已经返回了上一个页面,那个加载指示器 loadingIndicator
本来就已经随着页面消失了,所以 hideLoadingIndicator
这行代码执不执行,用户根本感知不到任何区别。