首页 > 解决方案 > Swift 中的内存管理和取消引用对象

问题描述

我正在创建一个对象,在其中调用它以在由按钮控制的布尔循环中运行一个函数。创建对象后,我调用它的两个函数,以便我可以获得返回值并将它们传递给下一个函数。一旦循环完成,对象仍然被引用,因此它不会被 ARC 设置为零,因此我创建了内存泄漏。我正在尝试使用weak引用类型来解决问题,但是当我尝试继续引用该对象时。我将在不更改变量类型或创建可选运算符的情况下粘贴泄漏的代码,以便您可以看到我从哪里开始。

self.buttonpress = true

DispatchQueue.global().async{
    while buttonpress == true{
        let varfind = find()
        let b = varfind.build()
        let yes = varfind.isInside(index: b,xaxis: locationViewModel.userXaxis, yaxis: locationViewModel.userYaxis)
        self.final = String(Int(locationViewModel.userZaxis)-yes)
    }
}

问题是,我如何让 while 循环中的内容执行和取消引用对象及其指针,以便它不会越来越多地占用内存并使用弱变量类型崩溃?

我知道我需要使用let varfind : find? = find(),然后deinit在 find 类中添加 a 以便将所有内容设置为 nil 但是,我认为在将 var yes 中的参数设置为 nil 时存在误解,例如索引是 a[String]所以它只能接受一个空数组?

标签: swiftmemory-leaksautomatic-ref-counting

解决方案


几点观察:

  1. 在我们讨论在while循环中创建的对象之前,我们应该认识到在将这段代码分派到全局队列的对象和正在分派的代码之间存在强引用循环。有人会[weak self]在闭包中使用模式来打破这个循环。您可以使用 Duncan 建议的模式,也可以只使用nil-coalescing 运算符:

    DispatchQueue.global().async { [weak self] in
        while self?.buttonPressed == true {
            let foo = Foo()
            foo.doSomething()
        }
    }
    
  2. 现在,让我们转向在while循环中创建的对象。你假设:

    一旦循环完成,对象仍然被引用

    这是极不可能的。即使这是问题所在,它是while循环内的局部变量这一事实绝对不是问题。如果您不小心在某处引入了强引用循环,您的对象可能不会被释放(参见下面的第 3 点)。或者它可能根本不是你的类,而是你的函数(或你的函数调用的 API)引入的自动释放对象(见下面的第 4 点)。它可能是底层 API 提供的缓存。它可以是许多事情中的任何一个。

  3. 但是,假设您想确认您自己的对象是否被释放。判断是否存在强引用循环的方法是“Debug Memory Graph”</a>。为了说明该技术,让我创建一个引入强引用循环的示例:

    DispatchQueue.global().async { [weak self] in
        while self?.buttonPressed == true {
            let foo = Foo()
            foo.bar(with: foo)
        }
    }
    

    在哪里

    class Foo {
        var foo: Foo?                       // bad: this introduces strong reference cycle
    
        func bar(with foo: Foo) {
            self.foo = foo
        }
    }
    

    然后我可以通过点击底部栏中的“Debug Memory Graph”按钮运行我的应用程序来查看内存图:

    在此处输入图像描述

    在左侧面板中查找您认为不应再存在于内存中的对象。更好的是,看看它们旁边是否有运行时错误三角形。

    在此示例中,我可以看到许多Foo实例旁边都有运行时错误三角形。当我点击一个时,我可以在主面板中直观地看到强引用循环。而且,因为我打开了“Product” » “Scheme” » “Edit Scheme...” » “Run” » “Diagnostics” » “Malloc Stack Logging”,我什至可以在右侧面板中看到强引用的建立位置.

    因此,运行您的应用程序,使用调试内存图,并查看您的实例是否在左侧面板中,如果有,有多少。真的是while循环的每次迭代都没有释放一个实例吗?是不是while循环没有停止(参见上面的第 1 点)?或者您根本没有看到您的对象,而是有其他东西正在消耗内存(请参阅以下几点)?

  4. 已经向您展示了如何检查您的内存图以确认对象是否没有被释放(例如通过强引用循环),这很有可能根本不是问题。(在没有MCVE的情况下,我们无法确定。)

    例如,另一个候选问题可能是自动释放对象。例如,考虑:

    DispatchQueue.global().async { [weak self] in
        while self?.buttonPressed == true {
            let foo = Foo()
            foo.bar()
        }
    }
    

    在哪里

    class Foo {
        var foo: Foo?
    
        func bar() {
            let string = NSString(format: "%d", Int.random(in: 0..<1_000_000))
            print(string)
        }
    }
    

    当你运行它时,它会产生一个像这样的内存图:

    在此处输入图像描述

    这里的问题不是Foo,而是该bar方法正在创建一个自动释放对象。这可以通过在循环autoreleasepool内部放置一个来解决:while

    DispatchQueue.global().async { [weak self] in
        while self?.buttonPressed == true {
            autoreleasepool {
                let foo = Foo()
                foo.bar()
            }
        }
    }
    

    值得注意的是,此autoreleasepool问题仅适用于您或您正在调用的 API 实际创建自动释放对象的情况。在 Swift 中,这种情况越来越少见,但有时我们会调用在底层使用自动释放对象的第三方库。

  5. 还有许多其他模式会导致内存肆无忌惮地增长。例如,可能存在URLSession永远不会失效的实例。或者可能正在发生一些缓存(由您明确显示或隐藏在您调用的任何 API 中)。或者,您可能有一些带有一些 CoreFoundation 泄漏或其他非托管对象的代码。

    如果没有可重现的问题示例,就不可能说出问题所在。Instruments 的“分配”工具是一种出色的(如果非常复杂)方法,可以准确识别哪些内容被泄露或遗弃,但如果您可以向我们展示问题的可重现示例并且我们可以指出您可能会更容易正确的方向。

最重要的是,问题不是由while循环内的局部变量引起的。要么你有一些强引用循环,要么有其他泄漏、废弃或缓存的内存。使用“Debug Memory Graph”将有助于确定是否实际上是您的对象导致了内存图,或者它是否是一些更微妙的内存问题。


与手头的问题无关,但是这个从多个线程访问buttonpress和访问的代码示例final不是线程安全的。线程清理程序 (TSAN)可能有助于识别这些问题。从多个线程访问这些属性时,您应该使用锁或其他同步机制。或者实施完全消除这些数据竞争的模式。但这超出了这个问题的范围。首先解决您的内存问题,然后解决线程安全问题。


推荐阅读