如何使用 dispatchQueues 创建引用循环?
How can I create a reference cycle using dispatchQueues?
我觉得我一直误解了创建引用循环的时候。在我过去认为几乎任何你有块的地方并且编译器强迫你写 .self
之前,这表明我正在创建一个引用循环并且我需要使用 [weak self] in
。
但以下设置不会创建引用循环。
import Foundation
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution
class UsingQueue {
var property : Int = 5
var queue : DispatchQueue? = DispatchQueue(label: "myQueue")
func enqueue3() {
print("enqueued")
queue?.asyncAfter(deadline: .now() + 3) {
print(self.property)
}
}
deinit {
print("UsingQueue deinited")
}
}
var u : UsingQueue? = UsingQueue()
u?.enqueue3()
u = nil
该块仅保留 self
3 秒。然后释放它。如果我使用 async
而不是 asyncAfter
那么它几乎是立竿见影的。
据我了解这里的设置是:
self ---> queue
self <--- block
队列仅仅是块的shell/wrapper。这就是为什么即使我 nil
队列,块也会继续执行。他们是独立的。
那么有没有只使用队列并创建引用循环的设置?
据我了解,[weak self]
仅用于引用循环以外的原因,即 控制块的流 。例如
你想保留对象和运行你的块然后释放它吗?一个真实的场景是即使视图已经从屏幕上移除也要完成这个事务...
或者您想使用 [weak self] in
以便在您的对象已被释放时提前退出。例如一些纯粹的 UI 不再需要停止加载微调器
FWIW 我知道如果我使用闭包那么事情就不同了,即如果我这样做:
import PlaygroundSupport
import Foundation
PlaygroundPage.current.needsIndefiniteExecution
class UsingClosure {
var property : Int = 5
var closure : (() -> Void)?
func closing() {
closure = {
print(self.property)
}
}
func execute() {
closure!()
}
func release() {
closure = nil
}
deinit {
print("UsingClosure deinited")
}
}
var cc : UsingClosure? = UsingClosure()
cc?.closing()
cc?.execute()
cc?.release() // Either this needs to be called or I need to use [weak self] for the closure otherwise there is a reference cycle
cc = nil
在闭包示例中,设置更像是:
self ----> block
self <--- block
因此它是一个引用循环并且不会解除分配,除非我将要捕获的块设置为 nil
。
编辑:
class C {
var item: DispatchWorkItem!
var name: String = "Alpha"
func assignItem() {
item = DispatchWorkItem { // Oops!
print(self.name)
}
}
func execute() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: item)
}
deinit {
print("deinit hit!")
}
}
使用以下代码,我能够创建一个泄漏,即在 Xcode 的内存图中我看到一个循环,而不是一条直线。我得到紫色指示器。我认为此设置非常类似于存储闭包如何产生泄漏。这与 您的 两个示例不同,其中执行 从未完成 。在此示例中,执行是 finished,但由于引用,它仍保留在内存中。
我觉得参考是这样的:
┌─────────┐─────────────self.item──────────────▶┌────────┐
│ self │ │workItem│
└─────────┘◀︎────item = DispatchWorkItem {...}───└────────┘
你说:
From what I understand the setup here is:
self ---> queue
self <--- block
The queue is merely a shell/wrapper for the block. Which is why even if I nil
the queue, the block will continue its execution. They’re independent.
self
恰好对队列有强引用这一事实是无关紧要的。更好的思考方式是 GCD 本身保留对所有队列的引用,其中有任何队列。 (它类似于自定义 URLSession
实例,在该会话中的所有任务完成之前不会被释放。)
因此,GCD 保持对队列的引用以及已分派的任务。队列保持对已调度 blocks/items 的强引用。排队的块保持对它们捕获的任何引用类型的强引用。当分派的任务完成时,它会解析对任何捕获的引用类型的任何强引用,并从队列中删除(除非您在其他地方保留自己对它的引用。),通常从而解决任何强引用循环。
撇开这一点,缺少 [weak self]
可能会给您带来麻烦的是 GCD 出于某种原因(例如调度源)保留对该块的引用。经典的例子是重复计时器:
class Ticker {
private var timer: DispatchSourceTimer?
func startTicker() {
let queue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".ticker")
timer = DispatchSource.makeTimerSource(queue: queue)
timer!.schedule(deadline: .now(), repeating: 1)
timer!.setEventHandler { // whoops; missing `[weak self]`
self.tick()
}
timer!.resume()
}
func tick() { ... }
}
即使我在其中启动上述计时器的视图控制器已关闭,GCD 仍会继续触发此计时器并且 Ticker
不会被释放。正如“调试内存图”功能所示,在 startTicker
例程中创建的块保持对 Ticker
对象的持久强引用:
如果我在该块中使用 [weak self]
作为该调度队列上调度的计时器的事件处理程序,这显然可以解决。
其他场景包括一个慢速(或不确定长度)的分派任务,您想要 cancel
它(例如,在 deinit
中):
class Calculator {
private var item: DispatchWorkItem!
deinit {
item?.cancel()
item = nil
}
func startCalculation() {
let queue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".calcs")
item = DispatchWorkItem { // whoops; missing `[weak self]`
while true {
if self.item?.isCancelled ?? true { break }
self.calculateNextDataPoint()
}
self.item = nil
}
queue.async(execute: item)
}
func calculateNextDataPoint() {
// some intense calculation here
}
}
综上所述,在绝大多数 GCD use-cases 中,[weak self]
的选择并不是强引用循环之一,而仅仅是我们是否介意强引用self
一直持续到任务完成与否。
如果我们只是在任务完成后更新 UI,则无需让视图控制器及其视图在层次结构中等待 UI 如果视图控制器已被关闭则更新。
如果我们需要在任务完成时更新数据存储,那么如果我们想确保更新发生,我们绝对不想使用 [weak self]
。
通常情况下,分派的任务不够重要,不足以担心 self
的寿命。例如,当请求完成时,您可能有一个 URLSession
完成处理程序分派 UI 更新返回主队列。当然,我们理论上会想要 [weak self]
(因为没有理由为已被解雇的视图控制器保留视图层次结构),但话又说回来,这又给我们的代码增加了噪音,通常几乎没有 material 好处.
无关,但游乐场是测试记忆行为的可怕场所,因为它们有自己的特质。最好在实际的应用程序中执行此操作。另外,在实际的应用程序中,您将拥有“调试内存图”功能,您可以在其中查看实际的强引用。参见 。
我觉得我一直误解了创建引用循环的时候。在我过去认为几乎任何你有块的地方并且编译器强迫你写 .self
之前,这表明我正在创建一个引用循环并且我需要使用 [weak self] in
。
但以下设置不会创建引用循环。
import Foundation
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution
class UsingQueue {
var property : Int = 5
var queue : DispatchQueue? = DispatchQueue(label: "myQueue")
func enqueue3() {
print("enqueued")
queue?.asyncAfter(deadline: .now() + 3) {
print(self.property)
}
}
deinit {
print("UsingQueue deinited")
}
}
var u : UsingQueue? = UsingQueue()
u?.enqueue3()
u = nil
该块仅保留 self
3 秒。然后释放它。如果我使用 async
而不是 asyncAfter
那么它几乎是立竿见影的。
据我了解这里的设置是:
self ---> queue
self <--- block
队列仅仅是块的shell/wrapper。这就是为什么即使我 nil
队列,块也会继续执行。他们是独立的。
那么有没有只使用队列并创建引用循环的设置?
据我了解,[weak self]
仅用于引用循环以外的原因,即 控制块的流 。例如
你想保留对象和运行你的块然后释放它吗?一个真实的场景是即使视图已经从屏幕上移除也要完成这个事务...
或者您想使用 [weak self] in
以便在您的对象已被释放时提前退出。例如一些纯粹的 UI 不再需要停止加载微调器
FWIW 我知道如果我使用闭包那么事情就不同了,即如果我这样做:
import PlaygroundSupport
import Foundation
PlaygroundPage.current.needsIndefiniteExecution
class UsingClosure {
var property : Int = 5
var closure : (() -> Void)?
func closing() {
closure = {
print(self.property)
}
}
func execute() {
closure!()
}
func release() {
closure = nil
}
deinit {
print("UsingClosure deinited")
}
}
var cc : UsingClosure? = UsingClosure()
cc?.closing()
cc?.execute()
cc?.release() // Either this needs to be called or I need to use [weak self] for the closure otherwise there is a reference cycle
cc = nil
在闭包示例中,设置更像是:
self ----> block
self <--- block
因此它是一个引用循环并且不会解除分配,除非我将要捕获的块设置为 nil
。
编辑:
class C {
var item: DispatchWorkItem!
var name: String = "Alpha"
func assignItem() {
item = DispatchWorkItem { // Oops!
print(self.name)
}
}
func execute() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: item)
}
deinit {
print("deinit hit!")
}
}
使用以下代码,我能够创建一个泄漏,即在 Xcode 的内存图中我看到一个循环,而不是一条直线。我得到紫色指示器。我认为此设置非常类似于存储闭包如何产生泄漏。这与 您的 两个示例不同,其中执行 从未完成 。在此示例中,执行是 finished,但由于引用,它仍保留在内存中。
我觉得参考是这样的:
┌─────────┐─────────────self.item──────────────▶┌────────┐
│ self │ │workItem│
└─────────┘◀︎────item = DispatchWorkItem {...}───└────────┘
你说:
From what I understand the setup here is:
self ---> queue self <--- block
The queue is merely a shell/wrapper for the block. Which is why even if I
nil
the queue, the block will continue its execution. They’re independent.
self
恰好对队列有强引用这一事实是无关紧要的。更好的思考方式是 GCD 本身保留对所有队列的引用,其中有任何队列。 (它类似于自定义 URLSession
实例,在该会话中的所有任务完成之前不会被释放。)
因此,GCD 保持对队列的引用以及已分派的任务。队列保持对已调度 blocks/items 的强引用。排队的块保持对它们捕获的任何引用类型的强引用。当分派的任务完成时,它会解析对任何捕获的引用类型的任何强引用,并从队列中删除(除非您在其他地方保留自己对它的引用。),通常从而解决任何强引用循环。
撇开这一点,缺少 [weak self]
可能会给您带来麻烦的是 GCD 出于某种原因(例如调度源)保留对该块的引用。经典的例子是重复计时器:
class Ticker {
private var timer: DispatchSourceTimer?
func startTicker() {
let queue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".ticker")
timer = DispatchSource.makeTimerSource(queue: queue)
timer!.schedule(deadline: .now(), repeating: 1)
timer!.setEventHandler { // whoops; missing `[weak self]`
self.tick()
}
timer!.resume()
}
func tick() { ... }
}
即使我在其中启动上述计时器的视图控制器已关闭,GCD 仍会继续触发此计时器并且 Ticker
不会被释放。正如“调试内存图”功能所示,在 startTicker
例程中创建的块保持对 Ticker
对象的持久强引用:
如果我在该块中使用 [weak self]
作为该调度队列上调度的计时器的事件处理程序,这显然可以解决。
其他场景包括一个慢速(或不确定长度)的分派任务,您想要 cancel
它(例如,在 deinit
中):
class Calculator {
private var item: DispatchWorkItem!
deinit {
item?.cancel()
item = nil
}
func startCalculation() {
let queue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".calcs")
item = DispatchWorkItem { // whoops; missing `[weak self]`
while true {
if self.item?.isCancelled ?? true { break }
self.calculateNextDataPoint()
}
self.item = nil
}
queue.async(execute: item)
}
func calculateNextDataPoint() {
// some intense calculation here
}
}
综上所述,在绝大多数 GCD use-cases 中,[weak self]
的选择并不是强引用循环之一,而仅仅是我们是否介意强引用self
一直持续到任务完成与否。
如果我们只是在任务完成后更新 UI,则无需让视图控制器及其视图在层次结构中等待 UI 如果视图控制器已被关闭则更新。
如果我们需要在任务完成时更新数据存储,那么如果我们想确保更新发生,我们绝对不想使用
[weak self]
。通常情况下,分派的任务不够重要,不足以担心
self
的寿命。例如,当请求完成时,您可能有一个URLSession
完成处理程序分派 UI 更新返回主队列。当然,我们理论上会想要[weak self]
(因为没有理由为已被解雇的视图控制器保留视图层次结构),但话又说回来,这又给我们的代码增加了噪音,通常几乎没有 material 好处.
无关,但游乐场是测试记忆行为的可怕场所,因为它们有自己的特质。最好在实际的应用程序中执行此操作。另外,在实际的应用程序中,您将拥有“调试内存图”功能,您可以在其中查看实际的强引用。参见