Swift 中的 Weak self 和 unowned self 对我们许多人来说很难理解。 尽管 Automatic Reference Counting (ARC)已经为我们解决了很多问题,但是当我们不使用值引用类型时,我们仍然需要管理引用。
在写 Swift 代码时,我们经常使用闭包 closures,这种情况下,我们需要十分小心引用计数,从而避免循环引用。
What is ARC, retain and release?
我们从基础开始,想充分了解 weak self and unowned self ,最好通读 Swift 文档中的自动引用计数进行理解,这些都归结为内存管理。
在使用 ARC 之前,我们必须手动管理内存和引用。 开发人员进行手动管理内存时很容易出错。 当新实例保留一个对象时,引用计数将增加,而一旦释放引用,引用计数则减少。一旦对象没了引用,就释放内存,就意味着不再需要该对象了。
在 Swift 中,我们需要使用 Weak self 和 unowned self 为 ARC 提供关系之间所需的信息。 在不使用 Weak self 和 unowned self 的情况下,基本上是在告诉ARC 需要某个“强引用”,防止引用计数变为零。如果没有正确使用这些关键字,可能会导致应用程序中的内存泄漏。如果未正确使用 Weak self 和 unowned self ,也可能发生所谓的循环强引用(Strong Reference Cycles)或循环引用(Retain Cycles)。
引用计数仅适用于类的实例。结构体和枚举是值类型,不是引用类型,不通过引用存储和传递。
When to use weak self?
首先,弱引用始终被声明为可选变量,因为当释放其引用时,ARC 可以将它们自动设置为 nil。 以下两个例子将说明何时使用弱引用:
class Blog {
let name: String
let url: URL
var owner: Blogger?
init(name: String, url: URL) { self.name = name; self.url = url }
deinit {
print("Blog \(name) is being deinitialized")
}
}
class Blogger {
let name: String
var blog: Blog?
init(name: String) { self.name = name }
deinit {
print("Blogger \(name) is being deinitialized")
}
}
一旦释放了这两个类中的任何一个,就会打印出一条消息。 在以下代码示例中,我们将两个实例设置为 nil,然后将其定义为可选实例。尽管我们期望会有两个打印,但实际上并没有:
var blog: Blog? = Blog(name: "SwiftLee", url: URL(string: "www.avanderlee.com")!)
var blogger: Blogger? = Blogger(name: "Antoine van der Lee")
blog!.owner = blogger
blogger!.blog = blog
blog = nil
blogger = nil
// Nothing is printed
这是 retain cycle 的结果。 Blog 对 Blogger 有强引用,并且不释放。 同时,Blogger 也不释放 Blog。嗯,这就是一个无限循环:retain cycle。
因此,我们需要使用 weak 引用。在这个例子中,只需要一个 weak reference 就可以打破循环引用。例如,设置 Blog 的 owner 属性为 weak 引用:
class Blog {
let name: String
let url: URL
weak var owner: Blogger?
init(name: String, url: URL) { self.name = name; self.url = url }
deinit {
print("Blog \(name) is being deinitialized")
}
}
class Blogger {
let name: String
var blog: Blog?
init(name: String) { self.name = name }
deinit {
print("Blogger \(name) is being deinitialized")
}
}
var blog: Blog? = Blog(name: "SwiftLee", url: URL(string: "www.avanderlee.com")!)
var blogger: Blogger? = Blogger(name: "Antoine van der Lee")
blog!.owner = blogger
blogger!.blog = blog
blog = nil
blogger = nil
// Blogger Antoine van der Lee is being deinitialized
// Blog SwiftLee is being deinitialized
But how about weak self?!
对于我们许多人来说,最好的做法是始终将 weak self 与 closures 结合使用,以避免 retain cycles。 但是,只有在 self 也保留了 closures 的情况下才需要这样做。默认情况下,通过添加 weak ,我们在很多情况下实际上不需要使用 optionals 选项。
假设我们为 blog posts 引入一种 publish 方法(注意,在此示例中,我们通过手动添加延迟来“伪造”网络请求):
struct Post {
let title: String
var isPublished: Bool = false
init(title: String) { self.title = title }
}
class Blog {
let name: String
let url: URL
weak var owner: Blogger?
var publishedPosts: [Post] = []
init(name: String, url: URL) { self.name = name; self.url = url }
deinit {
print("Blog \(name) is being deinitialized")
}
func publish(post: Post) {
/// Faking a network request with this delay:
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.publishedPosts.append(post)
print("Published post count is now: \(self.publishedPosts.count)")
}
}
}
var blog: Blog? = Blog(name: "SwiftLee", url: URL(string: "www.avanderlee.com")!)
var blogger: Blogger? = Blogger(name: "Antoine van der Lee")
blog!.owner = blogger
blogger!.blog = blog
blog!.publish(post: Post(title: "Explaining weak and unowned self"))
blog = nil
blogger = nil
将打印如下结果:
// Blogger Antoine van der Lee is being deinitialized
// Published post count is now: 1
// Blog SwiftLee is being deinitialized
我们看到在发布博客之前请求已完成。 强引用使我们能够完成发布并将帖子保存到我们已发布的帖子中。
如果我们将publish方法更改为包括弱引用,则:
func publish(post: Post) {
/// Faking a network request with this delay:
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
self?.publishedPosts.append(post)
print("Published post count is now: \(self?.publishedPosts.count)")
}
}
我们将得到如下打印:
// Blogger Antoine van der Lee is being deinitialized
// Blog SwiftLee is being deinitialized
// Published post count is now: nil
由于博客在发布请求完成之前已经 release,因此我们将永远无法更新本地已发布帖子的状态。
因此,如果在执行闭包后立即对引用实例进行处理,请确保不要使用 weak self。
Weak references and retain cycles
一旦 closure 保持 self 并且 self 保持 closure ,就会发生 retain cycle 。 如果我们有一个包含 onPublish 闭包的变量,则可能会发生这种情况:
class Blog {
let name: String
let url: URL
weak var owner: Blogger?
var publishedPosts: [Post] = []
var onPublish: ((_ post: Post) -> Void)?
init(name: String, url: URL) {
self.name = name
self.url = url
// Adding a closure instead to handle published posts
onPublish = { post in
self.publishedPosts.append(post)
print("Published post count is now: \(self.publishedPosts.count)")
}
}
deinit {
print("Blog \(name) is being deinitialized")
}
func publish(post: Post) {
/// Faking a network request with this delay:
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.onPublish?(post)
}
}
}
var blog: Blog? = Blog(name: "SwiftLee", url: URL(string: "www.avanderlee.com")!)
var blogger: Blogger? = Blogger(name: "Antoine van der Lee")
blog!.owner = blogger
blogger!.blog = blog
blog!.publish(post: Post(title: "Explaining weak and unowned self"))
blog = nil
blogger = nil
closure 持有 blog,blog 也持有 closure. 打印结果如下:
// Blogger Antoine van der Lee is being deinitialized
// Published post count is now: 1
尽管 count 为 1 时一切正常,但我们看不到博客和发布者被释放。 这是由于 retain cycle 导致无法释放内存。
在 onPublish 方法内添加对我们的博客实例的弱引用可以解决我们的 retain cycle :
onPublish = { [weak self] post in
self?.publishedPosts.append(post)
print("Published post count is now: \(self?.publishedPosts.count)")
}
打印结果:
// Blogger Antoine van der Lee is being deinitialized
// Published post count is now: Optional(1)
// Blog SwiftLee is being deinitialized
这样数据可以本地保存,并且释放实例。不再有 retain cycles.
最后,总结一下,我们最好谨记:
ARC将弱引用设置为 nil 时,不会调用属性观察器(Property observers)。
When to use unowned self?
与弱引用不同,使用 unowned 时引用不会变成可选的。 尽管如此,unowned 和 weak 并不能建立强引用。
Apple文档中指出:
只要该引用在其生命周期中的某个时刻变为 nil 是有效的,就使用 weak 引用。相反,如果属性在初始化后永远不会被设置为 nil,则使用 unowned reference。
通常,在使用 unowned 时要非常小心。 可能我们访问的实例已不再存在,从而导致 crash。 使用 unowned 而不是 weak 的唯一好处是,我们不必处理可选项。
因此,多数情况下使用 weak 总是更安全。
Why don’t we need this with value types like structs?
在 Swift中,我们有值类型和引用类型。
对于引用类型,我们实际上需要管理引用。 这意味着我们需要将此关系管理为 strong,weak 或 unowned。
值类型将保留其数据的唯一副本,唯一实例。 这意味着在多线程环境中无需使用弱引用,因为没有引用,我们使用的是唯一副本。
Are weak and unowned only used with self inside closures?
不,绝对不是。我们可以将任意属性和变量定义为 weak 或者 unowned,只要它是引用类型。如:
download(imageURL, completion: { [weak imageViewController] result in
// ...
})
我们甚至可以引用多个实例,因为它基本上是一个数组:
download(imageURL, completion: { [weak imageViewController, weak imageFinalizer] result in
// ...
})
小结:
总之,这是一个很难掌握的课题。最好从阅读Swift文档开始,该文档更深入地讨论该主题。
另外,如果不确定,请使用 weak 而不是 unowned。它可以避免烦人的错误。 此外,如果需要在闭包内部完成工作,请不要使用 weak 并确保代码正在执行。
参考:
网友评论