原文:Weak self, a story about memory management and closure in Swift
内存管理是 Swift 和 iOS 开发中的一个大话题。如果有很多教程解释何时将 weak self
与闭包一起使用,这里有一个简短的故事,讲述它何时会发生内存泄漏。
出于本文章(演示)目的,假设我们有以下具有两个函数的类。每个函数都执行一些事务并通过执行闭包来完成执行。
💡 更新 - 2022 年 4 月 9 日:我重新访问了这些示例,以突出显示引用计数器何时增加以及何时可能导致内存泄漏。
swift
class MyClass {
func doSomething(_ completion: (() -> Void)?) {
// do something
completion?()
}
func doSomethingElse(_ completion: (() -> Void)?) {
// do something else
completion?()
}
}
现在,出现了一个新需求,我们需要一个新函数 doEverything
,它将按顺序同时执行 doSomething
和 doSomethingElse
。一路上,我们正在改变 class 的状态以跟随进度:
swift
var didSomething: Bool = false
var didSomethingElse: Bool = false
func doEverything() {
self.doSomething {
self.didSomething = true // <- strong reference to self
print("did something")
self.doSomethingElse {
self.didSomethingElse = true // <- strong reference to self
print("did something else")
}
}
}
马上,我们可以看到 self
在第一个和第二个闭包中被强捕获:闭包保持对 self
的强引用,它在内部增加引用计数器,并可以防止在执行 doSomething
期间取消分配实例。
这意味着如果这些函数是异步的并且我们想在它完成执行之前释放实例,系统仍然必须等待它完成才能释放内存。
当然,我们知道得更多,并为闭包设置了 weak self
:
swift
func doEverything() {
self.doSomething { [weak self] in
self?.didSomething = true
print("did something")
self?.doSomethingElse { [weak self] in
self?.didSomethingElse = true
print("did something else")
}
}
}
等等,我们真的需要为每个闭包都使用
[weak self]
吗?
实际上,我们没有(必要这么做)。
当我们有像这里这样的嵌套闭包时,我们应该始终将 weak self
设置为第一个,即外部闭包。嵌套在外部闭包中的内部闭包可以重用相同的 weak self
。
swift
func doEverything() {
self.doSomething { [weak self] in
self?.didSomething = true
print("did something")
self?.doSomethingElse { in
self?.didSomethingElse = true
print("did something else")
}
}
}
然而,如果我们反其道而行之,只在嵌套闭包中使用 weak self
,外部闭包仍然会强捕获 self
并增加引用计数器。所以要小心你在哪里设置这个。
swift
func doEverything() {
self.doSomething { in
self.didSomething = true // <- strong reference to self**
print("did something")
self.doSomethingElse { [weak self] in
self?.didSomethingElse = true
print("did something else")
}
}
}
到目前为止,一切都很好。
由于我们想在此过程中更改其他变量,因此让我们使用 guard let
清理代码以确保实例仍然可用。
swift
func doEverything() {
self.doSomething { [weak self] in
guard let self = self else {
return
}
self.didSomething = true
print("did something")
self.doSomethingElse { in
self.didSomethingElse = true // <-- strong reference?**
print("did something else")
}
}
}
但是现在,问题来了:既然我们在外层闭包中有一个名为 self
的强引用,那么内层闭包是否强捕获了它?我们如何验证这一点?
这是值得深入研究的问题,Xcode Playground 非常适合这个问题。我将包含一些日志来跟踪步骤以及记录引用计数器。
对于第一个例子,让我们保持简单,这样我们就可以看到引用计数器是如何递增的。
swift
class MyClass {
func doSomething(_ completion: (() -> Void)?) {
// do something
completion?()
}
func doSomethingElse(_ completion: (() -> Void)?) {
// do something else
completion?()
}
var didSomething: Bool = false
var didSomethingElse: Bool = false
deinit {
print("Deinit")
}
func printCounter() {
print(CFGetRetainCount(self))
}
func doEverything() {
print("start")
printCounter() // 2
self.doSomething {
self.didSomething = true
print("did something")
self.printCounter() // 4
self.doSomethingElse {
self.didSomethingElse = true
print("did something else")
self.printCounter() // 6
} // 6-2=4
} // 4-2=2
printCounter() // 2
}
}
do {
let model = MyClass()
model.doEverything()
}
这是输出:
arduino
# output
start
2
did something
4
did something else
6
2
Deinit
只有对 self
的强引用,我们可以看到计数器上升到 6。但是,正如预期的那样,一旦执行完两个函数,实例就会被释放。
现在让我们在外部闭包中引入 weak self
。
swift
func doEverything() {
print("start")
printCounter() // 2
self.doSomething { [weak self] in
self?.didSomething = true
print("did something")
self?.printCounter() // 3
self?.doSomethingElse {
self?.didSomethingElse = true
print("did something else")
self?.printCounter() // 4
} // 4-1=3
} // 3-1=2
printCounter() // 2
}
对于第一个 weak self
,实例仍然被取消分配,计数器只增加到 4。
arduino
# output
start
2
did something
3
did something else
4
2
Deinit
那么 guard let self
会发生什么呢?
swift
func doEverything() {
print("start")
printCounter() // 2
self.doSomething { [weak self] in
guard let self = self else { return } // +1
self.didSomething = true
print("did something")
self.printCounter() // 3
self.doSomethingElse { // + 1
self.didSomethingElse = true // +1
print("did something else")
self.printCounter() // 5
}
}
printCounter() // 2
}
这是输出:
arduino
# output
start
2
did something
3
did something else
5
2
Deinit
如果实例成功取消初始化,我们可以看到当我们执行 doSomethingElse
时计数器实际上从 4 增加到 5,这意味着嵌套闭包强烈捕获了我们的临时的 self
。
看起来已经很可疑了,但让我们尝试一个不同的例子。如果 doSomething
和 doSomethingElse
不是函数,而是类的闭包属性,会怎样?让我们调整代码以进行类似的执行:
swift
class MyClass {
var doSomething: (() -> Void)?
var doSomethingElse: (() -> Void)?
var didSomething: Bool = false
var didSomethingElse: Bool = false
deinit {
print("Deinit")
}
func printCounter() {
print(CFGetRetainCount(self))
}
func doEverything() {
print("start")
printCounter() // 2
doSomething = { [weak self] in
guard let self = self else { return } // +1
self.didSomething = true
print("did something")
self.printCounter() // 3
self.doSomethingElse = { // +1
self.didSomethingElse = true // +1
print("did something else")
self.printCounter() // 5
}
self.doSomethingElse?()
}
doSomething?()
printCounter() // 3
}
}
do {
let model = MyClass()
model.doEverything()
}
这是输出:
arduino
# output
start
2
did something
3
did something else
5
3
这一次,类甚至没有取消分配🤯。要修复它,我们必须保留一个 weak
实例:
swift
func doEverything() {
print("start")
printCounter() // 2
doSomething = { [weak self] in
self?.didSomething = true
print("did something")
self?.printCounter() // 3
self?.doSomethingElse = {
self?.didSomethingElse = true
print("did something else")
self?.printCounter() // 3
}
self?.doSomethingElse?()
}
doSomething?() // 3-1=2
printCounter() // 2
}
arduino
# output
start
2
did something
3
did something else
3
2
Deinit
是的,这次实例已正确解除分配。可以确认,内部闭包是对 guard let self
的强引用。
那么,这对我的代码意味着什么?
当我们面对闭包时,我们倾向于编写 weak self
后跟一个 guard let
快速绕过而不会过多考虑进一步执行。这是我们仍然需要小心的地方。很容易错过这种内存泄漏,所以这里有一些要点:
首先,关于格式,我个人在闭包中使用 guard let strongSelf
而不是 guard let self
。原因是在代码审查期间,很难知道我们指的是代码中的哪个 "self
"。
其次,如果有嵌套闭包,我宁愿保留对 weak
(和 optional
)self?
的引用?并且永远不会指向 strongSelf
,因此我有把握避免对它的任何强引用。
swift
func doEverything() {
print("start")
self.printCount() // 2
doSomething = { [weak self] in
// 在闭包中声明 guard let strongSelf 以明确指向的 "self"
guard let strongSelf = self else { return } // +1
strongSelf.didSomething = true
print("did something")
strongSelf.printCount() // 3
strongSelf.doSomethingElse = { // + 1
// 在嵌套闭包中保持对 weak self? 的引用
self?.didSomethingElse = true
print("did something else")
self?.printCount() // 4
}
strongSelf.doSomethingElse?()
}
doSomething?()
self.printCount() // 2
}
最后但同样重要的是,如果我们有太多的闭包需要处理,最好的办法仍然是将其重构为一个单独的函数,或者使用更新的 API 来避免这些错误。我正在考虑像 RxSwift 或 Combine 这样的函数式响应式编程,但也考虑使用 Swift Async。
当然,今天分享的代码可能有点牵强,可能无法反映您对闭包的日常使用,但在我看来,记住我们对实例的内存管理和引用仍然很重要。此外,这个问题正好进入同行评审的中间,所以我们永远不会太小心;)
希望你喜欢这篇文章,编码愉快!
有问题?反馈?请随时在 Twitter 上给我发消息。