同步调度的保留计数如何工作?

How does retain count with synchronous dispatch work?

我正在尝试解释对象的所有权以及 GCD 如何工作。 这些是我学到的东西:

class C {
    var name = "Adam"

    func foo () {
        print("inside func before sync", CFGetRetainCount(self)) // 3
        DispatchQueue.global().sync {
            print("inside func inside sync", CFGetRetainCount(self)) // 4
        }
        sleep(2)
        print("inside func after sync", CFGetRetainCount(self)) // 4 ?????? I thought this would go back to 3
    }
}

用法:

var c: C? = C()
print("before func call", CFGetRetainCount(c)) // 2
c?.foo()
print("after func call", CFGetRetainCount(c)) // 2

几个想法:

  1. 如果您对 ARC 在幕后保留和释放的确切位置有疑问,只需在“inside func after sync”之后添加断点,运行 它,以及何时停止使用“Debug” » “Debug Workflow” » “Always Show Disassembly”,您可以看到程序集,以准确了解发生了什么。我还建议使用 release/optimized 构建来执行此操作。

    查看程序集,发布在您的 foo 方法的末尾。

  2. 正如您所指出的,如果将 DispatchQueue.global().sync 调用更改为 async,您会看到预期的行为。

    此外,不出所料,如果您执行功能分解,将 GCD sync 调用移动到一个单独的函数中,您将再次看到您预期的行为。

  3. 你说:

    a function will increase the retain count of the object its calling against

    为了弄清楚发生了什么,我建议您参考 WWDC 2018 What’s New in Swift,关于 12:43 到视频中,他们在视频中讨论了编译器将 retain 插入的位置和 release 调用,以及它在 Swift 4.2.

    中的变化

    在Swift4.1中,它使用了“Owned”调用约定,调用者在调用函数之前保留对象,被调用函数负责在returning之前执行释放.

    在 4.2 中(如下面的 WWDC 屏幕快照所示),他们实现了“保证”调用约定,消除了大量冗余的保留和释放调用:

    至少在优化的构建中,这会产生更高效、更紧凑的代码。所以,做一个发布构建并查看程序集,你会看到它在运行。

  4. 现在,我们来到你问题的根源,关于为什么 GCD sync 函数的行为与其他场景不同(例如,它的 release 调用被插入到不同于其他具有非转义闭包的场景)。

    这似乎与 GCD 独有的优化有关sync。具体来说,当您同步分派到某个后台队列时,而不是停止当前线程然后 运行ning 指定队列的工作线程之一上的代码,编译器足够聪明地确定当前线程将空闲,如果可以的话,它只会 运行 当前线程上的分派代码。我可以很容易地想象这个 GCD sync 优化,可能在编译器插入发布调用的逻辑中引入了皱纹。

恕我直言,发布是在方法结束时完成而不是在关闭结束时完成这一事实在某种程度上是学术问题。我假设他们有充分的理由(或至少是实际的理由)将其推迟到函数的末尾。重要的是,当你从 foo return 时,保留计数是应该的。