Swift memory leak when iterating array of Errors

て烟熏妆下的殇ゞ 提交于 2019-12-24 00:46:29

问题


I'm relatively new to Swift, so I hope I'm not asking a stupid question.

I have some code that instantiates an array of type Error, which will later be iterated and printed to the console. When running this code through Instruments using the "Leaks" instrument, it shows a leak of _SwiftNativeNSError. If I change the array type from [Error] to [Any], the leak disappears, even though it is still actually holding an object conforming to Error. The leak is not reproducible with any other data types or protocols that I've tried.

Here's some sample code:

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()

Calling the doSomething() function immediately creates a leak. Switching [Error]() to [Any]() resolves the leak, but I'm not happy with this as a solution without understanding the underlying problem. The problem is also solved by changing [Error]() to my enum implementing the Error protocol: [err](). I've also tried creating my own custom protocol just to prove if this is being caused specifically by Error, and I'm only able to reproduce the problem when using Error; my own custom protocol did not exhibit this behaviour.

Originally, my code used a forEach loop to iterate the array, but I then tried re-writing it to use a standard for loop in case the closure in forEach was causing the issue, but this didn't work.

I'm suspicious that this may be a Swift bug (in which case, I will open an issue for it), but there's also a chance that I'm missing a key piece of understanding. If what I'm doing is bad practice, I'd like to understand why.


回答1:


Update:

After speaking with Joe Groff, an Apple engineer, this is the bug you could have encountered: https://bugs.swift.org/browse/SR-6536

Original Answer

I've played a bit with your code and I think the problem is due to Error type. In fact, taking the code by Josh, you can find a different behaviour if you use Error or MyError as the type of your array.

I guess the problem arises since the deinit call is not forwarded to CustomObject since Error is just a protocol and it's not aware of the underlying class. While, MyError is. We can wait for other people to have clarifications on this behaviour.

Just for simplicity, I'm using a Playground here. See that I'm not even trying to print the error value.

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



回答2:


I tested your code and it looks like the String(describing) statement is causing the string to retain the error, which is just weird. Here is how I can tell: I created an associated object that prints out when its being deinitialized.

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

}

When the print doesn't run, sure enought he associated object is deinitialized at when the error is removed from the array and the output is:

deiniting

Uncomment the print and the output becomes:

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

At first I thought it was the print that's the problem but If I refactor to:

errors.forEach{print($0)}

I get the output:

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

But if I change it to:

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

Then deinit is no longer called:

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

Weirdness. Maybe file a radar?




回答3:


This bug was Fixed in Xcode 9.3.



来源:https://stackoverflow.com/questions/47904498/swift-memory-leak-when-iterating-array-of-errors

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!