为什么在 Swift 闭包中 [weak self] 能正常工作但 [unowned self] 却会出错?

13
这个SpriteKit动作通过调用包含完成闭包的自身函数来重复执行。它使用闭包而不是SKAction.repeatActionForever(),因为它需要在每次重复时生成一个随机变量:
class Twinkler: SKSpriteNode {
  init() {
    super.init(texture:nil, color:UIColor.whiteColor(), size:CGSize(width:10.0, height:10.0))
    twinkle()
  }
  func twinkle() {
      let rand0to1 = CGFloat(arc4random()) / CGFloat(UINT32_MAX)
      let action = SKAction.fadeAlphaTo(rand0to1, duration:0.1)
      let closure = {self.twinkle()}
      runAction(action, completion:closure)
  }
}

我认为我应该使用[unowned self]来避免闭包中的强引用循环。当我这样做时:

let closure = {[unowned self] in self.twinkle()}

它会崩溃并显示错误:_swift_abortRetainUnowned。但是如果我使用[weak self]

let closure = {[weak self] in self!.twinkle()}

它可以无错误地执行。为什么[weak self]可以工作,而[unowned self]会出问题?在这里我应该使用其中的哪一个吗?

程序中的其他位置强引用了Twinkler对象,作为另一个节点的子级。因此,我不明白为什么[unowned self]引用会出现问题。它不应该被释放。

我尝试在SpriteKit之外使用dispatch_after()复制这个问题,但我失败了。


在iOS 9.1上,即使未执行闭包本身,我仍能重现崩溃。不确定这是否是一个错误,因为在iOS 9.3上没有发生这种情况。https://dev59.com/zJXfa4cB1Zd3GeqPe3HG#36274194 - Whirlwind
5个回答

23

如果闭包中的self可能为空,则使用[weak self]

如果闭包中的self永远不会为空,则使用[unowned self]

如果在使用[unowned self]时出现崩溃,则该闭包中的self某些时候可能为空,因此您需要改用[weak self]

文档中的示例非常好地阐明了在闭包中使用strongweakunowned

https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html


9
这似乎是一个bug。{[unowned self] in self.twinkle()}应该与{[weak self] in self!.twinkle()}完全相同。

是的。随着Xcode和Swift的更新版本,这个问题已经消失了。 - Warren Whipple
4
现在我又在 Swift 1.2 中遇到了这个问题......需要注意的是,崩溃发生在闭包被声明时,而不是被调用时。 - jtlim
我曾经在Swift 2.2中遇到过这个问题,使用weak而不是unowned解决了它。有趣的是,在闭包内检查self时,它并不是nil - Andrii Chernenko
@deville:“有趣的是,在闭包内部检查self时,它不是nil。”我认为这可能是因为纯Swift中实现弱引用的方式——弱引用在尝试访问它们之前不会被设置为nil(并且已解初始化的对象直到所有弱引用都未设置才会被释放,以允许此操作)。https://www.mikeash.com/pyblog/friday-qa-2015-12-11-swift-weak-references.html 但仍然无法解释为什么weakunowned之间会有差异。 - user102008

6
我最近也遇到了类似的崩溃。在我的情况下,有时重新初始化的对象恰好具有与已释放对象相同的内存地址。但是,如果两个对象具有不同的内存地址,则代码将正常执行。
这是我疯狂的解释。当Swift将强引用放入闭包并检查其捕获列表时,如果捕获列表中的变量说“未拥有”,则会检查对象是否已被释放。如果对象标记为“弱”,则不进行检查。
由于该对象在闭包中保证永远不为nil,因此它实际上永远不会在那里崩溃。
所以,可能是语言错误。我的看法是使用弱引用而不是未拥有。

1
当新初始化的对象与已释放的对象具有相同的内存地址时,我在使用 unowned 时遇到了完全相同的问题。你是否已经提交了错误报告? - Kevin Hirsch

2
为了避免出错,应该这样写:
let closure = {[weak self] in self?.twinkle()}

不是

let closure = {[weak self] in self!.twinkle()}

在强制解包后面加上感叹号,如果为nil则会抛出错误。与强制解包类似,Unowned 在self为nil时也会抛出错误。当使用这两种选项之一时,应该使用guard或if语句来防止nil。


楼主表示第二个例子(强制解包弱引用self)不会抛出错误。这个问题不是关于最佳实践,而是关于为什么在他的例子中在捕获列表中使用unowned self会抛出错误(以及为什么弱引用self的强制解包不会抛出错误)。;) - Whirlwind
我刚在 playground 中尝试了一下,如果 self 为 nil,则强制解包 weak self 会抛出错误。因此,像我展示的那样使用它永远不会抛出错误,而如果在运行闭包之前将对象(self)设置为 nil,则强制解包它将会抛出错误。@Whirlwind - Jose Castellanos
我相信你。这就是一切应该运作的方式。但是原帖作者说它对他有不同的作用... 如果你有时间,也可以看看我的类似问题(你可以在原帖作者的评论中找到链接)。 - Whirlwind
是的,我想我只是在分享我的最佳实践经验啦,哈哈 @Whirlwind - Jose Castellanos

1
这只是我对文档的理解,以下是一个理论。
像弱引用一样,无主引用不会对其所指向的实例保留强引用。然而,与弱引用不同的是,无主引用被认为总是有值。因此,无主引用始终被定义为非可选类型。[来源]
你说Twinkler对象作为另一个节点的子对象被强引用,但SKNode的子对象隐式展开为可选项。我的猜测是问题不在于self被释放,而是当你尝试创建闭包时,Swift不愿意创建对可选变量的无主引用。因此,在这里使用[weak self]是正确的闭包捕获列表。

在场景对象中将 Twinkler 对象作为非可选常量保持强引用仍会导致相同的 abortRetainUnowned 运行时崩溃。 - Warren Whipple

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