Home > OS >  Swift How to solve a Memory "Leak"
Swift How to solve a Memory "Leak"

Time:09-25

I have encountered a memory "leak" that I don't understand. I have a caching class whose purpose is to limit the number of instances of heavy Data items. Even when it stores NO instances of those items, they are being retained somehow. If they are from a background thread, I believe they are released upon completion of the task. But, of course, as demonstrated by the code, on the main thread they persist.

This occurs on iPadOS 14.8, iPadOS 15 and MacCatalyst.

What am I missing or don't understand?

class DataCache {
    var id: String
    var data: Data? {
        get {
            let url = FileManager.default.temporaryDirectory
                .appendingPathComponent("\(id).txt")
            return try! Data(contentsOf: url)
        }
        
        set {
            let url = FileManager.default.temporaryDirectory
                .appendingPathComponent("\(id).txt")
            try! newValue!.write(to: url)
        }
    }

    init(id: String, data: Data) {
        self.id = id
        self.data = data
    }
}
class Item  {
    static var selection = [Item]()
    static var items = [String:Item]()
    
    var id: String = UUID().uuidString
    var itemData: DataCache
    
    init() {
        itemData = DataCache(id: id,
                            data: Data(String(repeating: "dummy", count: 4_000_000).utf8)
                            )
    }
    
    required init(_ other: Item) {
        self.itemData = DataCache(id: self.id, data: other.itemData.data!)
    }
    
    func duplicate(times: Int) {
        for index in 0..<times {
            print(index)
            Item.selection.append(Item(self))
        }
    }
    
}

@main struct Main {
    static func main() throws {
        let item = Item()
        
        perform(item)
        
        performOnSelection() { item in
            let _ = Item(item)
        }
        
        while (true) {}
    }
    
    static func perform(_ item: Item) {
        item.duplicate(times: 100)
    }
    
    static func performOnSelection(perform action: @escaping (Item)->Void) {
        var done = false
        DispatchQueue.global().async {
            for item in Item.selection {
                action(item)
            }
            done = true
        }
        
        while !done { sleep (1) }
    }
}

CodePudding user response:

You have autorelease objects being created. Insert an autoreleasepool to drain the pool periodically, e.g.:

func performOnSelection(perform action: @escaping (Item) -> Void) {
    ...

    autoreleasepool {
        perform(item)
    }

    performOnSelection() { item in
        autoreleasepool {
            let _ = Item(item)
        }
    }

    ...
}

and

func duplicate(times: Int) {
    for index in 0..<times {
        print(index)
        autoreleasepool { [self] in
            Item.selection.append(Item(self))
        }
    }
}

E.g. without autoreleasepool:

enter image description here

And with:

enter image description here

It turns off to be much faster when it is not dealing with all of those lingering autorelease objects, too. And peak memory usage has gone from 3.4gb to 47mb.

  • Related