【问题标题】:Swift memory leak when iterating array of Errors迭代错误数组时出现Swift内存泄漏
【发布时间】:2017-12-20 11:00:19
【问题描述】:

我对 Swift 比较陌生,所以我希望我不是在问一个愚蠢的问题。

我有一些代码实例化了一个Error 类型的数组,稍后将对其进行迭代并打印到控制台。当使用“Leaks”工具通过 Instruments 运行此代码时,它显示_SwiftNativeNSError 的泄漏。如果我将数组类型从[Error] 更改为[Any],泄漏就会消失,即使它实际上仍然持有符合Error 的对象。我尝试过的任何其他数据类型或协议都无法重现泄漏。

这里有一些示例代码:

class myLeak {
  lazy var errors = [Error]()
    enum err: Error {
        case myFirstError
    }

    func doSomething() {

        errors.append(err.myFirstError)
        for error in errors {
            print(String(describing: error))
        }
    }
}
// call with let myleak = myLeak(); myleak.doSomething()

调用 doSomething() 函数会立即产生泄漏。将[Error]() 切换为[Any]() 可以解决泄漏问题,但如果不了解潜在问题,我对此并不满意。通过将[Error]() 更改为实现Error 协议的我的枚举也解决了该问题:[err]()。我还尝试创建自己的自定义协议,以证明这是否是由Error 专门引起的,并且我只能在使用Error 时重现该问题;我自己的自定义协议没有表现出这种行为。

最初,我的代码使用forEach 循环来迭代数组,但后来我尝试重写它以使用标准for 循环,以防forEach 中的闭包导致问题,但这并没有不行。

我怀疑这可能是一个 Swift 错误(在这种情况下,我会为它打开一个问题),但我也有可能错过了一个关键的理解。如果我的做法是不好的做法,我想了解原因。

【问题讨论】:

  • 复制您的代码并将 deinit 添加到 myLeak 类以检查 deist 是否被调用。显然它被调用了,所以我没有看到任何泄漏,因为myLeak
  • 我已经添加了一个解释。我猜这个问题是由于Error.
  • 我添加了一个更新。
  • @LorenzoB 谢谢 - 我确实发现记录了该错误,但尽管它似乎相关,但这似乎是一个稍微不同的情况,因为这与可选错误有关。我同意它看起来确实相关,并且我在我提交的雷达中引用了那个错误。非常感谢您的跟进。

标签: ios arrays swift memory-leaks


【解决方案1】:

更新

在与 Apple 工程师 Joe Groff 交谈后,您可能会遇到以下错误:https://bugs.swift.org/browse/SR-6536

原答案

我对您的代码进行了一些尝试,我认为问题是由于Error 类型造成的。 事实上,使用 Josh 的代码,如果您使用 ErrorMyError 作为数组的类型,您会发现不同的行为。

我猜这个问题的出现是因为deinit 调用没有转发到CustomObject,因为Error 只是一个协议,它不知道底层的类。而MyError 是。我们可以等待其他人对此行为作出澄清。

为了简单起见,我在这里使用 Playground。看到我什至没有尝试打印错误值。

import UIKit

class ViewController: UIViewController {
    var errors: [Error] = [] // change to MyError to see it working

    enum MyError: Error {
        case test (CustomObject)
    }

    class CustomObject {
        deinit {
            print("deiniting")
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        let testerror = MyError.test(CustomObject())
        errors.append(testerror)
        errors.removeAll()
    }    
}

do {
    let viewController = ViewController()
    // just for test purposes ;)
    viewController.viewDidLoad()
}

【讨论】:

  • 谢谢洛伦佐。在您看来,您会说这是一个错误,还是预期的行为?您对未转发 deinit 呼叫的解释很有道理。
  • @CPR 我想你可以打开雷达。但是,我不敢说真话。
  • 谢谢洛伦佐。我已经提交了雷达,所以将等待回复。
【解决方案2】:

我测试了您的代码,看起来String(describing) 语句导致字符串保留错误,这很奇怪。我可以这样判断:我创建了一个关联对象,当它被取消初始化时会打印出来。

import UIKit

class ViewController: UIViewController {
    var errors = [Error]()
    override func viewDidLoad() {
        super.viewDidLoad()
        class CustomObject {
            deinit {
                print("deiniting")
            }
        }
        enum MyError: Error {
            case test (CustomObject)
        }
        let testerror = MyError.test(CustomObject())
        errors.append(testerror)
        for error in errors {
            //print(String(describing: error))
        }
        errors.removeAll()
    }

}

当打印没有运行时,当从数组中删除错误并且输出是:

deiniting

取消注释打印,输出变为:

test(CustomObject #1 in stack.ViewController.viewDidLoad() -> ())

起初我认为是打印问题,但如果我重构为:

errors.forEach{print($0)}

我得到了输出:

test(CustomObject #1 in stack.ViewController.viewDidLoad() -> ())
deiniting

但是如果我把它改成:

errors.map {String(describing:$0)}.forEach{print($0)}

那么 deinit 就不再被调用了:

test(CustomObject #1 in stack.ViewController.viewDidLoad() -> ())

奇怪。也许提交radar

【讨论】:

  • 感谢您的详细回复。我完全忽略了 String(describing:) 语句作为错误的可能来源。很高兴得到一些确认,我的代码本质上没有任何问题。稍后我将接受此作为解决方案,但现在将其保持打开状态,以防任何人能够提供更多见解。非常感谢!
  • 我已经添加了一个解释。我猜这个问题是由于Error.
【解决方案3】:

此错误已在 Xcode 9.3 中修复。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-08-02
    • 2012-09-12
    • 2014-01-29
    • 2020-03-19
    • 1970-01-01
    • 2015-02-08
    • 2017-02-14
    • 1970-01-01
    相关资源
    最近更新 更多