为啥 Swift 闭包不捕获自我?

Posted

技术标签:

【中文标题】为啥 Swift 闭包不捕获自我?【英文标题】:Why Swift closure not capture self?为什么 Swift 闭包不捕获自我? 【发布时间】:2016-12-05 16:12:15 【问题描述】:

我正在使用 Xcode Playground 测试快速关闭。

这是我的代码:

import UIKit

class A
    var closure: ()->() = 

    var name: String = "A"
    init() 
        self.closure = 
            self.name = self.name + " Plus"
        
    

    deinit 
        print(name + " is deinit")
    


var a: A?
a = A()
a = nil

正如预期的那样,a 是由闭包自包含的,所以 a 永远不会被释放。

但是,当我在最后一行之前添加这一行时:

a?.closure =  a?.name = "ttt" 

然后,我在输出窗口中发现“A is deinit”,这意味着a被释放了。 为什么?是不回收参考吗?

为了测试,我使用了一个函数来设置闭包,代码是版本2:

import UIKit

class A
    var closure: ()->() = 
    func funcToSetClosure()
        self.closure =  self.name = "BBB"
    
    var name: String = "A"
    init() 
        self.closure = 
            self.name = self.name + " Plus"
        
    

    deinit 
        print(name + " is deinit")
    




var a: A?


a = A()


a?.funcToSetClosure()


a = nil

同样,a 永远不会被释放。

所以我得出结论,当闭包被init或类中的函数设置时,会引起循环引用,当它在类中设置时,不会引起循环引用。我说的对吗?

【问题讨论】:

【参考方案1】:

在这两种情况下都有保留周期。区别在于 reference 的性质,而不是设置 closureplace。这种差异体现在打破循环需要什么:

在“内部”情况下,闭包内的引用是self。当您发布对a 的引用时,不足以打破循环,因为循环是直接自引用的。为了打破这个循环,您应该在将a 设置为nil 之前将a.closure 设置为nil,但你没有这样做。

在“外部”情况下,引用为a。只要您的a 引用未设置为nil,就会有一个保留周期。但您最终确实将其设置为nil,这足以打破循环。

(插图来自 Xcode 的内存图功能。太酷了。)

【讨论】:

感谢大家的帮助,终于明白了,也感谢Xcode的内存图。【参考方案2】:

正如SIL documentation 所说,当您在闭包中捕获局部变量时,它将通过引用计数存储在堆中:

捕获的局部变量和indirect值类型的有效载荷是 存储在堆上。 @box T 类型是一个引用计数类型,它 引用一个包含 T 类型的可变值的框。

所以当你说:

var a : A? = A()
a?.closure =  a?.name = "ttt" 

确实有一个参考周期(您可以轻松验证)。这是因为A 的实例引用了closure 属性,该属性引用了堆分配的盒装A? 实例(由于它被闭包捕获),而后者又引用了A 的实例。

但是,你接着说:

a = nil

这将堆分配的装箱A?实例的值设置为.none,从而释放其对A实例的引用,因此意味着您不再有引用循环,因此A可以已解除分配。

只是让a 超出范围而不分配a = nil不会打破引用循环,因为A? 在堆上的实例仍由closure 保留A 的属性,A? 实例仍保留该属性。

【讨论】:

【参考方案3】:

导致保留循环的原因是您在闭包中引用了self

var a: A?
a = A()
a?.closure =  a?.name = "ttt" 
a = nil

您将闭包更改为不再引用self,这就是它被释放的原因。

在最后一个例子中,你让它在闭包中再次引用self,这就是它不释放的原因。有很多方法可以解决这个问题,这篇文章是一个很好的列表,列出了何时在 swift 中使用每种情况:How to Correctly handle Weak Self in Swift Blocks with Arguments

我想你正在寻找这样的东西,你在块内使用对 self 的弱引用。 Swift 有一些新方法可以做到这一点,最常见的是在代码块前面使用 [unowned self] 表示法。

init() 
    self.closure =  [unowned self] in
        self.name = self.name + " Plus"
    

更多关于这里发生的事情的阅读:Shall we always use [unowned self] inside closure in Swift

【讨论】:

以上是关于为啥 Swift 闭包不捕获自我?的主要内容,如果未能解决你的问题,请参考以下文章

由闭包错误自我捕获

转义闭包捕获变异的“自我”参数,Firebase

错误:转义闭包捕获变异的“自我”参数

SwiftUI 转义闭包捕获变异的“自我”参数

在闭包中引用属性需要明确的“自我”。使捕获语义明确

获取 JSON,附加到数组:转义闭包捕获变异的“自我”参数