swift - 同步调度的保留计数如何工作?
问题描述
我试图解释对象的所有权以及 GCD 是如何工作的。这些是我学到的东西:
- 函数将增加其调用对象的保留计数
- 一个调度块,除非它
self
弱捕获会增加计数。 - 在执行调度的块后,它会释放捕获的对象,因此保留计数
self
应该减少。但这不是我在这里看到的。这是为什么?
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
解决方案
几个想法:
如果您对 ARC 在幕后保留和释放的确切位置有疑问,只需在“同步后内部函数”之后添加断点,运行它,当它停止时使用“调试”»“调试工作流程”»“始终显示反汇编”你可以看到程序集,准确地看到发生了什么。我还建议使用发布/优化版本来执行此操作。
查看程序集,版本在您
foo
方法的末尾。正如您所指出的,如果您将
DispatchQueue.global().sync
呼叫更改为 beasync
,您会看到您期望的行为。此外,不出所料,如果您执行功能分解,将 GCD
sync
调用移动到一个单独的函数中,您将再次看到您所期望的行为。你说:
函数将增加其调用对象的保留计数
Just to clarify what’s going on, I’d refer you to WWDC 2018 What’s New in Swift, about 12:43 into the video, in which they discuss where the compiler inserts the
retain
andrelease
calls, and how it changed in Swift 4.2.In Swift 4.1, it used the “Owned” calling convention where the caller would retain the object before calling the function, and the called function was responsible for performing the release before returning.
In 4.2 (shown in the WWDC screen snapshot below), they implemented a “Guaranteed” calling convention, eliminating a lot of redundant retain and release calls:
This results, in optimized builds at least, in more efficient and more compact code. So, do a release build and look at the assembly, and you’ll see that in action.
Now, we come to the root of your question, as to why the GCD
sync
function behaves differently than other scenarios (e.g. where its release call is inserted in a different place than other scenarios with non-escaping closures).It seems that this is potentially related to optimizations unique to GCD
sync
. Specifically, when you dispatch synchronously to some background queue, rather than stopping the current thread and then running the code on one of the worker threads of the designated queue, the compiler is smart enough to determine that the current thread would be idle and it will just run the dispatched code on the current thread if it can. I can easily imagine that this GCDsync
optimization, might have introduced wrinkles in the logic about where the compiler inserted the release call.
IMHO, the fact that the release is done at the end of the method as opposed to at the end of the closure is a somewhat academic matter. I’m assuming they had good reasons (or practical reasons at least), to defer this to the end of the function. What’s important is that when you return from foo
, the retain count is what it should be.
推荐阅读
- php - 如何使用 InsertInto 将经过清理的用户输入存储在数据库中
- apache-spark - 如何根据另一个数据框过滤数据框?
- javascript - 如何在博览会中以编程方式启用/禁用文本输入?
- python - Scrapy Spider 不会刮任何东西
- c# - 如何知道点击了按钮数组的哪个按钮
- .net-core - Azure Function (v2, .NET Core):反序列化来自流分析作业的事件中心输入
- ubuntu - apt install libleptonica-dev 失败
- jenkins - 如何配置 webhooks.config?
- openlayers - 在 Openlayers 中加载 GPX 文件作为矢量源后如何获取特征集合(几何和元数据)
- postgresql - Spring Boot Hibernate 使用序列或身份生成实体 ID 的问题