如何使用 dispatchQueues 创建参考循环?

Hon*_*ney 5 closures memory-leaks memory-management grand-central-dispatch swift

我觉得我一直误解了何时创建引用循环。在我过去认为几乎任何你有一个块并且编译器强迫你写的地方之前,.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
Run Code Online (Sandbox Code Playgroud)

该块仅保留self3 秒。然后释放它。如果我使用async而不是asyncAfter那么它几乎是立即的。

据我了解,这里的设置是:

self ---> queue
self <--- block
Run Code Online (Sandbox Code Playgroud)

队列只是块的外壳/包装器。这就是为什么即使我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
Run Code Online (Sandbox Code Playgroud)

在闭包示例中,设置更像是:

self ----> block
self <--- block
Run Code Online (Sandbox Code Playgroud)

因此,这是一个引用循环,除非我将 block 设置为 capture to ,否则不会解除分配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!")
    }
}
Run Code Online (Sandbox Code Playgroud)

使用以下代码,我能够创建泄漏,即在 Xcode 的内存图中我看到一个循环,而不是一条直线。我得到紫色指标。我认为这种设置非常类似于存储闭包如何产生泄漏。这与您的两个示例不同,其中执行永远不会完成。在此示例中,执行已完成,但由于引用,它仍保留在内存中。

我认为参考是这样的:

????????????????????????self.item?????????????????????????
?   self  ?                                     ?workItem?
?????????????????item = DispatchWorkItem {...}?????????????
Run Code Online (Sandbox Code Playgroud)

在此处输入图片说明

Rob*_*Rob 6

你说:

据我了解,这里的设置是:

self ---> queue
self <--- block
Run Code Online (Sandbox Code Playgroud)

队列只是块的外壳/包装器。这就是为什么即使我nil排队,块也会继续执行。他们是独立的。

self恰好对队列有强引用的事实是无关紧要的。一种更好的思考方式是 GCD 本身保留对所有排队的调度队列的引用。(它类似于URLSession在该会话上的所有任务完成之前不会释放的自定义实例。)

因此,GCD 保持对已调度任务队列的引用。队列保持对分派块/项目的强引用。排队块保持对它们捕获的任何引用类型的强引用。当分派的任务完成时,它会解析对任何捕获的引用类型的任何强引用,并从队列中删除(除非您在其他地方保留对它的引用。),通常从而解决任何强引用循环。


撇开这一点不谈,缺少[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() { ... }
}
Run Code Online (Sandbox Code Playgroud)

即使我启动上述计时器的视图控制器被解除,GCDTicker也会继续触发这个计时器并且不会被释放。正如“调试内存图”功能所示,在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
    }
}
Run Code Online (Sandbox Code Playgroud)

调度工作项内存图

话虽如此,在绝大多数 GCD 用例中, 的选择[weak self]不是强引用循环之一,而只是我们是否介意强引用是否self持续到任务完成。

  • 如果我们只是在任务完成后更新 UI,那么如果视图控制器已被解除,则无需让视图控制器及其层次结构中的视图等待某些 UI 更新。

  • 如果我们需要在任务完成后更新数据存储,那么[weak self]如果我们想确保更新发生,我们肯定不想使用。

  • 通常,分派的任务的后果不足以担心self. 例如,URLSession当请求完成时,您可能有一个完成处理程序将 UI 更新分派回主队列。当然,我们理论上会想要[weak self](因为没有理由为已被解雇的视图控制器保留视图层次结构),但是这又给我们的代码增加了噪音,通常没有什么实质性的好处。


无关,但游乐场是测试记忆行为的可怕场所,因为它们有自己的特质。在实际应用程序中执行此操作要好得多。另外,在实际的应用程序中,您可以使用“调试内存图”功能,您可以在其中查看实际的强引用。请参阅/sf/answers/2169543351/