为什么添加闭包捕获列表会阻止我的实例被释放?

Why does adding a closure capture list prevent my instance from being deallocated?

class Name {
    var name: String
    init(name: String) {
        self.name = name
    }
    deinit {
        print("\(name) deinit")
    }
}
var x: Name? = Name(name: "abc")

var someClosure = {
    print("\(x?.name)")
}

someClosure()

x = nil

然后控制台会输出:

Optional("abc")
abc deinit

可以看出调用了"deinit"函数。所以没有形成强引用循环。 但是如果我在闭包中添加一个捕获列表:

var someClosure = { [x] in
    print("\(x?.name)")
}

控制台会输出:

Optional("abc")

并且"deinit"函数没有被调用。所以对象和引用形成了一个强引用循环。

这是什么原因?这两个条件有什么区别?

这就是我认为这里发生的情况。

ARC 在创建 x 时将引用计数添加到 x。当使用捕获列表调用闭包时,它会添加另一个计数(闭包 captures 对象:从而向编译器表明它将来需要 x,通过增加引用计数)。所以 x 现在 ARC 计数为 2。

当您将 x 分配给 nil 时,它会将引用计数减 1。如果计数为 0,或者只有对对象的弱引用,则对象将被取消初始化。如果计数大于 0,并且引用很强,则保留对象。

如果不显式地将捕获列表传递给闭包,它只会微弱地捕获对象。然后 ARC 决定一旦闭包完成就可以安全地 deinit 对象。传递捕获列表向 ARC 指示闭包可能需要对象在不同的​​多次操作,因此进行了强引用。

x = nil 之后再次调用 someClosure(),看看两种情况下会发生什么。

首先,在这两种情况下都没有强保留 cycle——你只是简单地拥有一个全局闭包变量,它持有对你的 class 实例的强引用,因此防止它被释放。

在您的第一个示例中,当您在闭包中捕获 x 时:

var someClosure = {
    print("\(x?.name)")
}

你得到的(实际上)是对引用的引用——也就是说,闭包引用了 x 的存储,然后它引用了你的 class 实例。当您将 x 设置为 nil 时 – 闭包仍然引用 x 的存储,但现在 x 没有 引用了您的 class 实例。因此,您的 class 实例不再有任何强引用,可以被释放。

在你使用 capture list 的第二个例子中:

var someClosure = { [x] in
    print("\(x?.name)")
}

您正在复制 x 本身——也就是说,您正在复制对class 实例的引用。因此,闭包将保留你的 class 只要它存在。将 x 设置为 nil 不会影响闭包对您实例的引用,因为它拥有 自己的 强引用。