为什么 Swift 闭包不捕获 self?

Why Swift closure not capture self?

我正在用 Xcode 游乐场测试 swift 关闭。

这是我的代码:

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" }

然后,我在输出window中发现"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或者class中的函数设置时,它会导致回收引用,当它被设置在class之外时,它不会导致回收参考。我说得对吗?

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

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

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

在最后一个示例中,您在闭包中再次引用 self,这就是它不释放的原因。有很多方法可以解决这个问题,这个 post 是一个很好的列表,列出了何时使用 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

两种情况下都有循环保留。不同之处在于 reference 的性质,而不是设置 closureplace 的性质。这种差异体现在打破循环所需要的东西:

  • 在"inside"的情况下,闭包内部的引用是self。当你释放你对a的引用时,即不足以打破循环,因为循环是直接自引用的。要打破这个循环,在将 a 设置为 nil 之前,您需要 alsoa.closure 设置为 nil,而您没有别那样做。

  • 在"outside"的情况下,引用是a。只要您的 a 引用未设置为 nil,就会有一个保留周期。但你最终将其设置为nil足以打破循环。

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

正如SIL documentation所说,当你在闭包中捕获一个局部变量时,它会被存储在堆上并进行引用计数:

Captured local variables and the payloads of indirect value types are stored on the heap. The type @box T is a reference-counted type that references a box containing a mutable value of type T.

因此当你说:

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

有一个参考周期(你可以很容易地验证)。这是因为 A 的实例引用了 closure 属性,它引用了堆分配的盒装 A? 实例(因为它是被闭包捕获),它又引用了 A.

的实例

然而,你接着说:

a = nil

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

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