如何使用 dispatchQueue 创建引用循环?

Posted

技术标签:

【中文标题】如何使用 dispatchQueue 创建引用循环?【英文标题】:How can I create a reference cycle using dispatchQueues? 【发布时间】:2019-09-27 09:19:37 【问题描述】:

我觉得在创建引用循环时我总是误解了这一点。在我认为几乎任何你有一个块并且编译器强迫你写.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

队列只是块的外壳/包装器。这就是为什么即使我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 的内存图中我看到一个循环,而不是一条直线。我得到紫色指标。我认为这种设置非常类似于存储的闭包如何产生泄漏。这与您的两个示例不同,其中执行从未完成。在此示例中,执行已完成,但由于引用,它仍保留在内存中。

我认为参考是这样的:

┌─────────┐─────────────self.item──────────────▶┌────────┐
│   self  │                                     │workItem│
└─────────┘◀︎────item = DispatchWorkItem ...───└────────┘

【问题讨论】:

DispatchQueue 专门设计为不会导致保留周期。它包含一个控制行为的autoreleaseFrequency 属性。 知道这很有趣。你能在上面添加更多细节吗?但是,使用 [weak self] in 进行 dispatchQueues 的目的是什么?只是为了控制流量吗?我做了一个小编辑来详细说明我的意思 看看source code。根本没有捕获self 的目的。 我知道它不会捕获self,但如果是,那么源代码的哪一行可以捕获self? (我只是无法处理所有这些,所以我想缩小我应该处理的部分)另外我将队列更改为:var queue : DispatchQueue? = DispatchQueue(label: "mine", qos: .background, attributes: .concurrent, autoreleaseFrequency: .never, target: nil) 但它仍然被释放。 never 不是意味着它不会自动释放任何东西吗? autoreleaseFrequency 与强引用周期问题无关。那是关于在分派任务中创建的对象的自动释放池耗尽的时间。 【参考方案1】:

你说:

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

self ---> queue
self <--- block

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

即使我在其中启动上述计时器的视图控制器被解除,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 用例中,[weak self] 的选择并不是强引用循环之一,而仅仅是我们是否介意对 self 的强引用是否持续到任务完成与否。

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

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

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


无关,但游乐场是测试记忆行为的可怕场所,因为它们有自己的特质。在实际应用程序中执行此操作要好得多。另外,在实际应用程序中,您可以使用“调试内存图”功能查看实际的强引用。见https://***.com/a/30993476/1271826。

【讨论】:

读完后,我觉得 GCD 到队列就像 runloop 到计时器。这很有趣 非常感谢!我从未使用过DispatchSourceDispatchWorkItem,但您的示例足以理解。因此,虽然DispatchSourceDispatchWorkItem 都对self 有很强的引用,但self(与我的闭包示例不同)没有指向DispatchSourceDispatchWorkItem 的指针。这只是任务没有完成的问题,您可以取消或弱引用。使用闭包,即使块被执行。它不会发布,因为它仍然针对self 关闭。 PS我不知道如何阅读“调试内存图”我必须查看它 它是分析强引用、识别周期等的绝佳工具。请参阅 WWDC 2016 视频 Visual Debugging with Xcode。 我创建了一个新问题Does Xcode Memory graph offer any smart visual indicators for strong references that aren't memory cycles? 作为后续问题。可以看看吗? class C var item: DispatchWorkItem! var name: String = "Honey" func assignItem() item = DispatchWorkItem // Oops! print(self.name) func execute() DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: item) deinit print("deinit hit!") 使用以下代码,我能够在 Xcode 的内存图中创建一个 leak 即我看到一个循环,而不是一条直线。我得到紫色指标。我认为这种设置非常类似于存储的闭包如何造成泄漏

以上是关于如何使用 dispatchQueue 创建引用循环?的主要内容,如果未能解决你的问题,请参考以下文章

使用 DispatchGroup、DispatchQueue 和 DispatchSemaphore 按顺序执行带有 for 循环的 Swift 4 异步调用

如何在 Swift 中测量 DispatchQueue 并发异步中的代码块执行时间?

如何使用 DispatchQueue 从单例字典启动和停止 Firebase 性能跟踪

Spring如何处理循环引用

如何在后台线程swift中运行for循环

DispatchQueue.main.async 挂起后重复,但使用睡眠时不挂起