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

标签 swift memory-management memory-leaks closures grand-central-dispatch

我觉得在创建引用循环时我一直误解了这一点。在我过去认为几乎任何你有 block 的地方并且编译器强制你编写 .self 之前,这表明我正在创建一个引用循环并且我需要使用 [弱 self ]在

但以下设置不会创建引用循环。

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

该 block 仅保留 self 3 秒。然后释放它。如果我使用 async 而不是 asyncAfter 那么它几乎是立竿见影的。

据我了解这里的设置是:

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

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

那么有没有只使用队列并创建引用循环的设置?

据我了解,[weak self] 仅用于引用循环以外的原因,即控制 block 的流。例如

你想保留对象并运行你的 block 然后释放它吗?一个真实的场景是完成这个事务,即使 View 已经从屏幕上移除......

或者您想在 [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

因此它是一个引用循环并且不会解除分配,除非我将要捕获的 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 {...}───└────────┘

enter image description here

最佳答案

你说:

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 实例,在该 session 中的所有任务完成之前不会被释放。)

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


撇开这一点不谈,缺少 [weak self] 会给您带来麻烦的是 GCD 出于某种原因(例如调度源)保留对该 block 的引用。经典的例子是重复计时器:

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() { ... }
}

即使我在其中启动上述计时器的 View Controller 已关闭,GCD 仍会继续触发此计时器并且 Ticker 不会被释放。正如“调试内存图”功能所示,在 startTicker 例程中创建的 block 保持对 Ticker 对象的持久强引用:

repeating timer memory graph

如果我在该 block 中使用 [weak self] 作为该调度队列上调度的计时器的事件处理程序,这显然可以解决。

其他场景包括一个慢速(或不确定长度)的分派(dispatch)任务,您想要在其中取消它(例如,在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
    }
}

dispatch work item memory graph

综上所述,在绝大多数 GCD 用例中,[weak self] 的选择并不是强引用循环之一,而仅仅是我们是否介意强引用对 self 的引用一直持续到任务完成与否。

  • 如果我们只是在任务完成后更新 UI,那么如果 View Controller 已被关闭,则无需让 View Controller 及其层次结构中的 View 等待某些 UI 更新。

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

  • 通常,分派(dispatch)的任务不够重要,不必担心 self 的生命周期。例如,当请求完成时,您可能有一个 URLSession 完成处理程序将 UI 更新分派(dispatch)回主队列。当然,我们理论上会想要 [weak self](因为没有理由为已被解雇的 View Controller 保留 View 层次结构),但话又说回来,这又给我们的代码增加了噪音,通常很少物质利益。


无关,但 Playground 是测试内存行为的可怕场所,因为它们有自己的特质。最好在实际的应用程序中执行此操作。另外,在实际的应用程序中,您将拥有“调试内存图”功能,您可以在其中查看实际的强引用。参见 https://stackoverflow.com/a/30993476/1271826 .

关于swift - 如何使用 dispatchQueue 创建引用循环?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56061092/

相关文章:

c++ - 在 malloc_error_break 中设置断点以在 C++ 中进行调试

TWSDLLookup.Destroy 方法中的 Delphi XE 内存泄漏

java - 使用 pmap 和 gdb 查找 native 内存泄漏

swift - 将 s.frameworks 'XCTest' 添加到 podspec dyld : Library not loaded: rpath/XCTest. framework/XCTest 时出错

json - 在 Swift/iOS 中解析 JSON 响应

swift - 如何检查字典值是否为空

c++ - vector <T *> 析构函数

c++ - 是否可以在其范围之外访问局部变量的内存?

android - 按下后退按钮后如何解决 IntentReceiverLeaked

ios - 在 SceneKit 中,快速尝试通过触摸来移动角色