在闭包内将引用转换回强引用,内存管理,Swift

3
我正在尝试以下闭包保留周期的实验:
 class Sample {
        deinit {
            print("Destroying Sample")
        }

        func additionOf(a: Int, b:Int) {
            print("Addition is \(a + b)")
        }

        func closure() {                
          dispatch_async(dispatch_get_global_queue(0, 0)) { [weak self] in
            self?.additionOf(3, b: 3)   
            usleep(500)                 
            self?.additionOf(3, b: 5)  
          }
        }
    }

稍后的某个时间点,我正在进行

var sample : Sample? = Sample()
sample?.closure()

dispatch_async(dispatch_get_global_queue(0, 0)) {
  usleep(100)
  sample = nil
}

输出结果将会是:
Addition is 6
Destroying Sample

这是可以理解的,因为在执行 self?.additionOf(3, b:5) 之前,self 已经被设为了 nil。

如果我在闭包内创建另一个变量引用 [weak self],像下面这样做出改变:

dispatch_async(dispatch_get_global_queue(0, 0)) { [weak self] in
   guard let strongSelf = self else { return }
   strongSelf.additionOf(3, b: 3)
   usleep(500)
   strongSelf.additionOf(3, b: 5)
}

这次输出结果为:
Addition is 6
Addition is 8
Destroying C

我的问题是为什么在sample = nil之后,strongSelf没有被置为nil。这是因为它在闭包内被捕获之前就已经被捕获了吗。


1
当你设置sample = nil时,你所做的仅仅是解决对Sample的那一个强引用。但它对于可能存在的其他强引用没有任何影响(比如在你执行sample = nil之前分配的strongSelf)。在[weak self]闭包内部进行strongSelf模式的整个目的(有时开玩笑地称为“strongSelf/weakSelf dance”)是确保如果在该闭包开始时Sample未被释放,则将其保留直至该闭包完成。 - Rob
@Rob:我想我理解了,但不是100%。从OP的角度来看,当闭包开始时,直到执行第一个additionOf(3, b: 3)之前,Sample还没有被释放。我仍然不明白为什么strongSelf没有改变,因为self已经被nil化了。除非它被捕获在闭包内? - tonytran
1
并不是因为闭包“捕获”了它,而是闭包建立了一个新的、第二个强引用指向Sample对象。所以,当你调用var sample: Sample? = Sample()时,有一个强引用。当代码遇到guard let strongSelf = self else ...时,这建立了第二个强引用,总共有两个强引用。当你在主线程上执行sample = nil时,仍然有一个强引用。只有当strongSelf超出范围时,才会解决这个最终的强引用,并且对象被释放。 - Rob
1个回答

4

让我们考虑一下与您第二个示例相当的现代版本:

DispatchQueue.global().async { [weak self] in
    guard let self else { return }

    self.additionOf(3, b: 3)
    Thread.sleep(forTimeInterval: 0.0005)
    self.additionOf(3, b: 5)
}

这是一种常见的模式,它有效地表示“如果self已被释放,则立即return,否则建立对self的强引用,并在闭包完成之前维护此强引用。”
因此,在您的示例中,如果在此分派块启动时self不为nil,则一旦我们到达guard语句,我们现在有两个对此Sample对象的强引用,即原始的sample引用和此闭包内部的新引用。
因此,当您的其他线程删除其自己对此Sample对象的强引用sample(通过超出作用域或明确将sample设置为nil)时,此闭包的强引用仍然存在,并且将防止对象被释放(或者至少不会被释放,直到此分派块完成)。
在您上面的评论中,您问:

我仍然不理解为什么strongSelf没有因为self被置为nil而改变...

强引用永远不会仅因为其他强引用(即使它是原始强引用)被设置为nil而被设置为nil。引用被设置为nil的行为仅适用于弱引用,不适用于强引用。
当您的代码在第一个线程上执行sample = nil时,所有这样做的只是删除一个强引用。它不会删除对象或类似的内容。它只是删除了其强引用。现在分派块有自己的强引用,sample = nil只会使具有两个强引用的对象现在只剩下一个强引用(对它在分派块中的引用)。只有当最后一个强引用被删除时,对象才会被释放,并且将调用deinit
顺便说一句,如果您立即分派到全局队列,则[weak self]捕获列表实际上没有太多效用。您可能会切除它并执行以下操作:
DispatchQueue.global().async { [self] in
    additionOf(3, b: 3)
    Thread.sleep(forTimeInterval: 0.0005)
    additionOf(3, b: 5)
}

[weak self] 捕获列表最有用的是在稍后的时间点运行闭包,例如完成处理程序或者 asyncAfter,在这些情况下,有一定实际机会在闭包运行之前 self 将超出作用域:

DispatchQueue.global().asyncAfter(deadline: .now() + 3) { [weak self] in
    guard let self else { return }

    self.additionOf(3, b: 3)
    Thread.sleep(forTimeInterval: 0.0005)
    self.additionOf(3, b: 5)
}

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