Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SR-825] NSCoder object causes memory leak if assigned to class param #43437

Closed
dmcyk opened this issue Feb 26, 2016 · 6 comments
Closed

[SR-825] NSCoder object causes memory leak if assigned to class param #43437

dmcyk opened this issue Feb 26, 2016 · 6 comments
Assignees
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. memory leak bug: Memory leak

Comments

@dmcyk
Copy link
Contributor

dmcyk commented Feb 26, 2016

Previous ID SR-825
Radar rdar://problem/26922520
Original Reporter @dmcyk
Type Bug
Status Resolved
Resolution Done

Attachment: Download

Environment

OS X El Capitan 10.11.3, MacBook Air '13 2014
Xcode 7.3 beta from 25th February default toolchain as well as latest Swift development toolchain
Xcode 7.2 latest version with default toolchain
In both version error occurs with Debug and Release compilation

Additional Detail from JIRA
Votes 0
Component/s
Labels Bug, Leak
Assignee @dmcyk
Priority Medium

md5: 2c93b8bd59ce2e54384f45ea598132cc

Issue Description:

Having object that inherits from NSCoder and assigning it to a parameter of the object results in a memory leak.
While assigning it to a variable in a local context (current function) it doesn't result with a memory leak.

The resource I provide is a bit bigger, first I tried to recreate error from my project that's why I used NSOperation, but it doesn't seem to have any connection, you can see it in the #noOperation function, the leak still occurs.

Also while preparing it I found out that theres a warning that casting from NSMutableArray to Swift Array always fails, while it does not, the cast succeeds.

Hope my code and issue description are clear enough.

@dmcyk
Copy link
Contributor Author

dmcyk commented Feb 29, 2016

Was playing with JIRA (using it for the first time) and by mistake I put the issue to be in progress...
While it's currently not, so I will close and reopen it.

@jckarter
Copy link
Member

Here's a version of your example that I updated to Swift 3.0, and extracted out as a single-file standalone executable:

//
//  ViewController.swift
//  Bug
//
//  Created by Damian Malarczyk on 26.02.2016.
//

import Foundation

class ViewController: OperationDelegate {

    let queue = OperationQueue()
    
    struct DataWrapper {
        var data:[Info]?
    }
    
    var rawData:[Info]?
    var source:DataWrapper?
    static let documentsPath =  NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
    var operation:Operation? = Operation(withPath: ViewController.documentsPath + "/data")
    
    func viewDidLoad() {
        operation?.delegate = self
        queue.addOperation(operation!)
        noOperation()
    }

    
    func noOperation(){
        let runtimeData = NSMutableArray()
        
        //dummy data
        for _ in 0...10 {
            let info = Info()
            
            for _ in 0...10 {
                info.source?.append(Info.BaseInfo(name: "some name"))
            }
            runtimeData.add(info)
        }
        
        // another issue I guess, here I have warning saying that cast from NSMutableArray to [Info] always fails while it doesn't
        if let dataArray = (runtimeData as [AnyObject]) as? [Info] {
            
            //@ leak in both cases, DataWrapper doesnt have impact on that 
            
            self.source = DataWrapper(data:dataArray)
            //self.rawData = dataArray
            
            //let source = DataWrapper(data: dataArray)

        }
    }
    
    func finishedWithData(_ data: NSArray) {
        if let dataArray = data as? [Info] {
            let testLeak = true
            
            if testLeak {
                //memory leak occurs
                self.source = DataWrapper(data: dataArray)
                
            }else {
                //no leak
                let source = DataWrapper(data: dataArray)
            }
        }
        
        operation?.delegate = nil
        operation = nil
    }

}

protocol OperationDelegate {
    func finishedWithData(_ data:NSArray)
}

class Operation:Foundation.Operation {
    
    var path:String
    var delegate:OperationDelegate?
    
    init(withPath path:String){
        self.path = path
        
    }
    
    override func main() {
        let runtimeData = NSMutableArray()
        
        //dummy data
        for _ in 0...10 {
            let info = Info()
            
            for _ in 0...10 {
                info.source?.append(Info.BaseInfo(name: "some name"))
            }
            runtimeData.add(info)
        }
        
        // two cases - data is encoded from file and just normal data
        let encodeDataToFile = false
        
        if encodeDataToFile {
            saveData(runtimeData)
            
            let localData = readData()
            
            if let local = localData {
                delegate?.finishedWithData(local)
                
            }

        }else {
            delegate?.finishedWithData(runtimeData)

        }

    }
    
    deinit {
        print("no reference cycle")
    }
    func saveData(_ data:NSArray){
        
        let mutableData = NSMutableData()
        let archiver = NSKeyedArchiver(forWritingWith: mutableData)
        data.encode(with: archiver)
        archiver.finishEncoding()
        
        try? mutableData.write(toFile: path, options: .atomic)
        
    }
    
    func readData( )->NSArray? {
        let data = NSData.init(contentsOfFile: path)
        if let savedData = data {
            let unarchiver = NSKeyedUnarchiver(forReadingWith: savedData as Data)
            return NSArray(coder: unarchiver)
        }
        return nil
    }
}
class Info:NSCoder {
    static let SOURCE_KEY = "source_key"

    var source:[BaseInfo]?
    
    override init(){
        super.init()
        source = [BaseInfo]()
    }
    
    required  init?(coder aDecoder: NSCoder) {
        source = aDecoder.decodeObject(forKey: Info.SOURCE_KEY) as? [BaseInfo]
    }
    
    func encodeWithCoder(aCoder: NSCoder) {
        aCoder.encode(source, forKey: Info.SOURCE_KEY)
    }

    
    
    class BaseInfo:NSCoder {
        static let NAME_KEY = "name_key"
        
        var name:String

        init(name:String){
            self.name = name
        }
        
        func encodeWithCoder(aCoder: NSCoder) {
            aCoder.encode(name, forKey: BaseInfo.NAME_KEY)
        }
        required  init?(coder aDecoder: NSCoder) {
            name = aDecoder.decodeObject(forKey: BaseInfo.NAME_KEY) as! String
        }
        
    }
}

autoreleasepool {
  ViewController().viewDidLoad()
}

It doesn't appear to leak. I get "no reference cycle" as output at the end of the execution. It's not clear from your description what I need to do to reproduce the leak. Are you able to reproduce it in Xcode 8? Would it be possible to attach a complete project?

@dmcyk
Copy link
Contributor Author

dmcyk commented Aug 18, 2016

Attached complete project and simplified code to reproduce the leak, all the Operation code was kinda boilerplate as the leak occurs regardless of using Operation.

Basically if you launch the project as it is, you should encounter memory leak during:

 self.rawData = runtimeData

Xcode's Instruments will report memory leak for ContiguousArrayStorage and BaseInfo.
If you make the `Info` and `BaseInfo` to not subclass after `NSCoder` the leak doesn't occur.

Hope it will be clearer now.

Disclaimer:
I'm using Xcode b6 and experiencing some Instrument's issues, by trying to find leaks through Product->Profile->Leaks the analysis gets stuck during 'analyzing process' so leaks are not listed, I've found a workaround - during Xcode' debug session you have to use 'Profile in Instruments' option in Memory bookmark.

@dmcyk
Copy link
Contributor Author

dmcyk commented Dec 7, 2016

I added `NSCodingLeak.zip` file which contains a new highly simplified project.
The problem in this case is a bit different but I guess it's kinda related.
When using generic function leak occurs in contrast to using normal function where there's no memory leak.

Environment for this new project is: macOS 10.12.1, MacBook Air '13 2014 Xcode 8.2b2 beta. The memory leak when using generic function occurs in case of both, Debug and Release compilation.

Edit: updated the project again, apparently it actually can be even simpler

@jckarter
Copy link
Member

Sorry for not getting back to this for so long. I gave NSCodingLeak.zip a spin with Xcode 11, and I don't see any leak, so this probably got fixed somewhere along the way. Are you still able to reproduce?

@dmcyk
Copy link
Contributor Author

dmcyk commented Jul 13, 2019

Fixed indeed, can't reproduce it either 👍

@swift-ci swift-ci transferred this issue from apple/swift-issues Apr 25, 2022
This issue was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. memory leak bug: Memory leak
Projects
None yet
Development

No branches or pull requests

2 participants