我们是否应该在Swift中的闭包内始终使用[unowned self]?

510

在 WWDC 2014 的 403 演讲中,涉及到以下幻灯片 Intermediate Swifttranscript

enter image description here

演讲者说,如果我们不在那里使用[unowned self],就会出现内存泄漏。这是否意味着我们应该始终在闭包内部使用[unowned self]

在Swift天气应用程序的ViewController.swift的第64行上,我没有使用[unowned self]。但是,我通过使用一些@IBOutlet,如self.temperatureself.loadingIndicator来更新UI。这可能没问题,因为我定义的所有@IBOutlet都是weak的。但是为了安全起见,我们是否应该始终使用[unowned self]

class TempNotifier {
  var onChange: (Int) -> Void = {_ in }
  var currentTemp = 72
  init() {
    onChange = { [unowned self] temp in
      self.currentTemp = temp
    }
  }
}

图片链接已损坏。 - Daniel Gomez Rico
@DanielG.R. 谢谢,我看到了。http://i.stack.imgur.com/Jd9Co.png - Jake Lin
4
如果我没有看错,幻灯片中给出的例子是错误的——onChange应该是一个 [weak self] 闭包,因为它是一个公共属性(在内部是这样,但仍然如此),所以另一个对象可以获取并存储该闭包,从而保留 TempNotifier 对象(如果使用的对象没有释放对 onChange 的引用,直到看到 TempNotifier 不见了,通过它自己对 TempNotifier 的弱引用)。如果 var onChange …private var onChange … 那么 [unowned self] 将是正确的。我不完全确定这一点,如果我错了,请有人纠正我。 - Slipp D. Thompson
@Jake Lin var onChange: (Int) -> Void = {} 这里的花括号代表一个空闭包吗?就像用 [] 定义一个空数组一样?我在 Apple 文档中找不到解释。 - bibscy
@bibscy 是的,{} 是空闭包(闭包的实例)作为默认值(什么也不做),(Int) -> Void 是闭包的定义。 - Jake Lin
谁不喜欢一个好的比喻呢... weak someVarName 就像 someVarName?,而 unowned someVarName 就像 someVarName! - Eric
11个回答

928

不,有时您不想使用 [unowned self]。有时,您希望闭包捕获 self 以确保在调用闭包时它仍然存在。

示例:进行异步网络请求

如果您正在进行异步网络请求,则需要闭包保留self以便在请求完成时使用。否则,该对象可能已被释放,但您仍希望能够处理请求完成事件。

何时使用unowned selfweak self

您真正需要使用[unowned self][weak self]的唯一情况是当您会创建强引用循环。强引用循环是指所有权的循环,其中对象最终彼此拥有(可能通过第三方),因此它们永远不会被释放,因为它们都确保对方保持存在。

在闭包的具体情况下,你只需要意识到,任何在其中引用的变量都会被闭包“拥有”。只要闭包存在,这些对象就保证存在。停止所有权的唯一方法是使用[unowned self][weak self]。因此,如果一个类拥有一个闭包,并且该闭包捕获了该类的强引用,则闭包和类之间形成了强引用循环。这也包括如果类拥有拥有闭包的东西。
具体来说,在视频中的示例中,TempNotifier通过onChange成员变量拥有闭包。如果他们没有将self声明为unowned,则闭包也会拥有self,从而创建一个强引用循环。 unownedweak之间的区别是什么? < p > unownedweak 的区别在于,weak 被声明为可选类型,而 unowned 不是。通过将其声明为 weak,您可以处理在某个点上它可能为 nil 的情况。如果尝试访问恰好为 nil 的 unowned 变量,则会导致整个程序崩溃。因此,请仅在确信变量在闭包存在期间始终存在时使用 unowned


1
嗨。非常好的答案。我正在努力理解unowned self。仅仅使用weakSelf的原因是'self变成了可选项',这对我来说不够。为什么我要特别使用'unowned self'呢?https://dev59.com/go_ea4cB1Zd3GeqPScH7 - user1951992
25
使用unowned self的优点在于,如果您确定它永远不会为nil,则不必解包可选项,这样可以避免不必要的代码。最终,unowned self用于简洁性和向未来开发人员暗示您永远不希望出现nil值。 - drewag
93
在一个异步网络请求中使用[weak self]的理由是,在视图控制器(view controller) 中使用该请求来填充视图。如果用户退出,我们不再需要填充视图,也不需要引用视图控制器。 - David James
1
当对象被释放时,weak引用也会被设置为nil。而unowned引用则不会。 - BergQuester
2
我有点困惑。unowned 用于 non-Optional,而 weak 用于 Optional,那么我们的 selfOptional 还是 non-optional - Muhammad Nayab
显示剩余11条评论

218

2016年11月更新

我在这篇回答的基础上撰写了一篇文章(研究SIL以理解ARC的工作原理),请在此处查看:这里

原始回答

之前的回答没有明确的规则说明何时使用其中一个以及为什么,所以让我补充一些内容。

unowned或weak的讨论归结为变量和引用它的闭包的生命周期问题。

swift weak vs unowned

场景

您可以有两种可能的情况:

  1. 闭包具有与变量相同的生命周期,因此将只有当变量可达时,才能访问闭包。变量和闭包具有相同的生命周期。在这种情况下,应将引用声明为unowned。常见示例是在许多小闭包示例中使用的[unowned self],这些闭包在其父对象的上下文中执行某些操作,而且不会被引用到其他地方以超出其父级。

  2. 闭包的生命周期与变量的生命周期无关,即使变量不可访问,闭包仍然可能被引用。在这种情况下,应将引用声明为weak,并在使用之前验证它是否为nil(不要强制解包)。这种情况的常见示例是[weak delegate],您可以在某些闭包引用完全不相关(在生命周期上)的委托对象的示例中看到。

实际使用

那么,大多数情况下你会/应该使用哪个呢?

引用来自Twitter的Joe Groff的话

Unowned是更快的,允许不可变性和非可选性。

如果您不需要weak,请不要使用它。

您可以在 这里 找到有关unowned的更多内部工作原理。

*通常也被称为unowned(安全),以指示在访问unowned引用之前执行运行时检查(对于无效引用会导致崩溃)。


30
我厌倦听到那些“如果self可能为空就使用weak,如果永远不会为空就使用unowned”的类似解释了。好吧,我们已经明白了——听了无数遍!这个答案用通俗易懂的英语更深入地解释了什么情况下self可能为空,直接回答了提问者的问题。感谢这个很好的解释!! - TruMan1
1
很好的回答,非常实用。我受到启发,现在要将一些性能敏感的弱变量转换为未拥有的变量。 - original_username
1
如果闭包的生命周期总是与父对象相同,那么当对象被销毁时,引用计数不会自动处理吗?为什么不能在这种情况下只使用“self”,而要费心使用unowned或weak呢? - LegendLength
没有人可以处理你描述的情况,ARC只是像手动添加保留/释放调用一样。父/子关系是保留循环的一个例子,父对象自身无法被释放,因为闭包将其保留计数设置为+1,而闭包无法被释放,因为其父对象出于明显的原因正在保留它。因此,最终你会得到两个强烈相互保留的对象,计数至少为1。唯一处理此问题的方法是使用某种形式的弱引用,在这种情况下,允许闭包具有... - uraimo
在不增加引用计数的情况下,给其父级添加某种引用。 - uraimo
显示剩余4条评论

151

我想为视图控制器添加一些具体的示例。许多解释(不仅仅是在 Stack Overflow 上)都非常好,但我更喜欢实际的例子(@drewag有一个很好的开端):

  • If you have a closure to handle a response from a network requests use weak, because they are long lived. The view controller could close before the request completes so self no longer points to a valid object when the closure is called.
  • If you have closure that handles an event on a button. This can be unowned because as soon as the view controller goes away, the button and any other items it may be referencing from self goes away at the same time. The closure block will also go away at the same time.

    class MyViewController: UIViewController {
          @IBOutlet weak var myButton: UIButton!
          let networkManager = NetworkManager()
          let buttonPressClosure: () -> Void // closure must be held in this class. 
    
          override func viewDidLoad() {
              // use unowned here
              buttonPressClosure = { [unowned self] in
                  self.changeDisplayViewMode() // won't happen after vc closes. 
              }
              // use weak here
              networkManager.fetch(query: query) { [weak self] (results, error) in
                  self?.updateUI() // could be called any time after vc closes
              }
          }
          @IBAction func buttonPress(self: Any) {
             buttonPressClosure()
          }
    
          // rest of class below.
     }
    

22
这个需要更多的赞。有两个实例表明,按钮按下闭包不会存在于视图控制器的生命周期之外,因此可以使用unowned,但大多数更新UI的网络调用需要使用weak。 - Tim Fuqua
5
为了澄清一下,当我们在闭包块中调用 self 时,是否总是要使用 unowned 或 weak?或者是否有时我们不需要使用 weak/unowned?如果是这样,请也提供一个例子。 - luke
1
非常感谢您。 - Shawn Baek
1
这让我对[weak self]和[unowned self]有了更深入的理解。非常感谢@possen! - Tommy
1
这很好。那么如果我有一个基于用户交互的动画,但需要一段时间才能完成,然后用户移动到另一个视图控制器怎么办?在这种情况下,我想我仍然应该使用weak而不是unowned,对吗? - mfaani
显示剩余5条评论

72

55

以下是来自Apple开发者论坛的精彩引用,描述了美味的细节:

unowned vs unowned(safe) vs unowned(unsafe)

unowned(safe)是一个非拥有引用,在访问时断言对象仍然存在。它类似于弱可选引用, 每次访问时都会隐式解包为x!unowned(unsafe)类似于ARC中的__unsafe_unretained -它是一个非拥有引用, 但在访问时没有运行时检查对象是否仍然存在,所以悬挂引用将指向垃圾内存。 unowned当前始终是unowned(safe)的同义词,但意图是当禁用运行时检查时,在-Ofast 构建中将其优化为unowned(unsafe)

unowned vs weak

unowned实际上比weak使用一个更简单的实现。 原生Swift对象带有两个引用计数,unowned引用将增加无拥有引用计数而不是强引用引用计数。 当其强引用引用计数达到零时,对象将被解构初始化,但直到无拥有引用计数也达到零时才实际释放。这会使内存稍微保留更长时间, 当存在非拥有引用时通常不是问题,因为相关的对象应该具有几乎相等的生命周期,而且与基于辅助表格的实现相比,它要简单得多。

更新:在现代Swift中,weak内部使用了与unowned相同的机制。因此,这个比较是不正确的,因为它将Objective-C的weak与Swift的unowned进行了比较。

原因

拥有引用计数为0后为什么要保留内存?如果代码在解除初始化后尝试使用未拥有的引用来操作对象会发生什么?

保留内存是为了使其保持强引用计数。这样,当某人尝试保持对未拥有对象的强引用时,运行时可以检查强引用计数是否大于零,以确保保持该对象是安全的。

对象持有的所有引用(包括拥有和未拥有)会发生什么?当对象解除初始化时,它们的生命周期与对象是否分离或者它们的内存也会一直保留,直到最后一个未拥有的引用被释放并且对象被释放吗?

当对象的最后一个强引用被释放并运行其析构函数之后,对象所拥有的所有资源都会立即释放。未拥有的引用只会保持内存活性 - 除了具有引用计数的头部外,其内容是无效的。

很兴奋,对吧?


44

这里有一些很棒的答案。但是Swift实现弱引用的最近变化应该改变每个人对于weak self vs. unowned self使用决策。以前,如果您需要最佳性能,则使用unowned self优于weak self,只要您可以确保self永远不会为nil,因为访问unowned self比访问weak self快得多。

但是Mike Ash已经记录了Swift如何更新weak vars的实现以使用side-tables,并且这极大地提高了weak self的性能。

https://mikeash.com/pyblog/friday-qa-2017-09-22-swift-4-weak-references.html

现在弱引用不会有显著的性能损失,我认为我们应该默认使用它。弱引用的好处是它是可选的,这使得编写更正确的代码变得更容易,这基本上是Swift语言如此出色的原因。你可能认为你知道哪些情况可以安全地使用无主引用,但我的经验是,大多数开发者并不知道。我已经修复了很多崩溃,其中无主引用被释放,通常是在控制器被释放后后台线程完成时发生。
程序中最费时间、最痛苦和最昂贵的部分是错误和崩溃。尽你最大努力编写正确的代码并避免它们。我建议制定一个规则,永远不要强制解包可选项,也永远不要使用无主引用代替弱引用。你不会失去任何东西,错过强制解包和无主引用实际上是安全的时间。但你会从消除难以找到和调试的崩溃和错误中获得很多收益。

1
感谢您的更新,并且对最后一段表示赞同。 - motto
2
那么在新更改后,有没有任何情况下weak不能代替unowned的使用呢? - mfaani
我想知道@mfaani上面提出的问题的答案。 - daniel
那么,当我们使用[unowned self]时,最糟糕的情况是应用程序会崩溃,而使用[weak self]时最糟糕的情况是iOS资源的消耗。当我使用guard let `self` = self else { return }时会发生什么?这之后的self常量是否可能被设置为nil,从而导致崩溃,或者该self保留它所引用的对象以及所有其他引用它的对象的内存,直到代码完成并释放所有内存,或者self释放它所引用的内存以及所有直接或间接依赖于self引用的引用? - daniel
1
unowned 可能会导致崩溃,而 weak 不会,但效率较低。在使用 weak self 时,使用 guard 语句是可以的,它不会增加引用计数,但除非 self 仍然被分配,否则不会执行 guard 后面的代码。由于您在同一线程上使用它,因此在使用 self 时应保持其分配状态。 - SafeFastExpressive
我同意,与使用可能导致崩溃的未拥有引用相比,使用未拥有引用的优势目前非常微小,因此根本没有使用它的真正理由。也许如果您同时拥有一百万个对象,那么您可能会看到性能上的差异,但这几乎从不是真实情况。 - Leszek Szary

5
根据Apple-doc

  • 弱引用始终是可选类型,并在所引用的实例被释放时自动变为nil。

  • 如果所捕获的引用永远不会变成nil,则应将其捕获为未拥有的引用,而不是弱引用。

示例 -

    // if my response can nil use  [weak self]
      resource.request().onComplete { [weak self] response in
      guard let strongSelf = self else {
        return
      }
      let model = strongSelf.updateModel(response)
      strongSelf.updateUI(model)
     }

    // Only use [unowned self] unowned if guarantees that response never nil  
      resource.request().onComplete { [unowned self] response in
      let model = self.updateModel(response)
      self.updateUI(model)
     }

1

unowned和weak类似,它们都不会阻止被引用的对象被销毁,但是当weak变量所引用的对象不存在时,它们会变成nil,我们可以通过检查nil来处理这种情况。而unowned则会变成垃圾,你无法判断它们是否已经变成垃圾,使用它们将会导致程序崩溃。weak的问题在于,如果一个对象有多个weak变量引用它,在该对象被销毁时,需要遍历每个引用并将其设置为nil,这显然是很耗费资源的,使用unowned就会直接崩溃,而且很难发现此类错误。使用unowned的一个场景是,当你创建某些仅限于特定数据类型的封装时,它具有明确的接口,且内部不可直接访问。对于实现来说,可能需要大量的循环引用,但这些引用是自包含的,你可以使用unowned引用来打破这些循环引用,而不需要使用weak变量,例如你可能有一棵节点树,每个节点都需要有一个对其父节点的引用,删除一个节点将会删除其所有子节点,因此所有子节点都不需要保留对其父节点的引用。


那么,如果我们使用 [unowned self],最糟糕的情况是应用程序会崩溃,而使用 [weak self] 最糟糕的情况是在 iOS 资源上产生开销,这种说法正确吗?当我使用...时会发生什么? - daniel
即使你知道你的闭包将立即执行,而且不会被存储并在以后执行,但如果你使用unowned,由于另一个线程的操作,你的对象可能会在闭包执行期间被释放。我唯一会使用unowned的地方是如果你构建了自己的数据结构,这是一个对用户来说是黑盒子的东西,并且你可以通过使用unowned获得性能优势,并且你知道由于逻辑运行的方式,当未拥有引用无效时,它将不再被访问。 - Nathan Day
如果我有一个对象的引用,即使我的引用使用[unowned self],另一个线程如何导致该对象被释放?我有一个对象的引用。ARC不会确保如果我有一个对它的引用,即使是[unowned self]引用,该对象不会被释放吗?我认为ARC只有在没有对它的引用时才释放对象进行清理。 - daniel
ARC只是简单地保证引用计数是原子的。因此,如果您有一个在一个线程中运行的闭包,它引用了一个存在于数组中的对象,并且该对象被另一个线程从数组中移除,那么您的对象现在有强引用吗?您已经使用[unowned self]指定不要在闭包中使用对self的强引用,所以您的对象没有强引用了,闭包仍然可以操作该对象,实际上,您的闭包本身可能具有对对象的强引用,甚至不需要线程即可发生这种情况。 - Nathan Day

0
如果以上内容都不合理: 简而言之implicitly unwrapped optional 一样,如果你可以保证引用在使用时不会为 nil,请使用 unowned。否则,应该使用 weak。 解释: 我从weak unowned link中获取了以下内容。据我所知,unowned self 不能为 nil,但 weak self 可以为 nil,并且 unowned self 可能导致悬空指针...这在 Objective-C 中是臭名昭著的。希望有所帮助。

"UNOWNED 弱引用和无主引用行为类似,但并不相同。"

无主引用和弱引用一样,不会增加所引用对象的保留计数。然而,在 Swift 中,无主引用有额外的好处,即它们不是可选类型。这使得它们比使用可选绑定更容易管理。这与隐式解包可选项类似。此外,无主引用是非零化的。这意味着当对象被释放时,它不会将指针清零。这意味着在某些情况下,使用无主引用可能会导致悬空指针。对于那些像我一样记得 Objective-C 时代的极客来说,无主引用映射到 unsafe_unretained 引用。

这就有点令人困惑了。

弱引用和无主引用都不会增加保留计数。

它们都可以用于打破保留循环。那么我们何时使用它们?!

根据苹果的文档:

“每当在其生命周期内该引用可能会变为 nil 时,请使用弱引用。相反,当您知道一旦在初始化期间设置了该引用后,该引用永远不会为 nil 时,请使用无主引用。”

0

你可能有一些引用,不想让它们成为强引用,以避免循环引用。因此,当指向某个对象的最后一个强引用被移除时,该对象本身也会被移除。

那么其他非强引用会发生什么呢?显然,它们不再引用该对象,这是有问题的。有两种处理方式:

  1. 弱引用。当最后一个指向对象的强引用消失时,所有弱引用都会被设置为nil,因此开发人员可以检查所引用的对象是否还存在。显然,弱引用必须是可选的,否则它不能被设置为nil。使用弱引用的策略:您编写“if let ref = weakref”。如果引用仍然存在,由于您刚刚将其分配给了强引用,因此它将保持到“if let”结束。如果不这样做,则可能会访问相同的弱引用两次,并且在第一次访问时它可能(意外地)不是nil,但在第二次访问时却是nil。

  2. 创建无主引用。如果对象消失,没有人会告诉你。看起来好像你有一个引用,但所引用的对象已经消失了。只有在您100%确定所引用的对象不会提前消失时才能使用它。

如果您已经测量过它更快,并且您100%确定在对象消失时不会使用垃圾,请使用无主引用。


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