Nick Harris

Retain Cycle with NSOperation Dependencies – Help Wanted

with 2 comments

I’ve been working on a proof of concept app to try my hand at both CloudKit and NSOperation dependencies discussed in the Advanced NSOperations session at WWDC 2015. I’m eventually going to blog about all the things I learned along with the challenges and decisions I made. In fact I have a very lengthy post thats a little over halfway done sitting in my drafts folder in MarsEdit. While writing it I’ve had another chance to look more closely at my code to make sure its something I want to share. Last night I decided to run the Leaks instrument on it just to be certain everything looks good. Glad I did because I have a lot of memory leaks!

I decided to use a pattern I found in this Apple Developer forum post by Quinn “The Eskimo!” for my NSOperations. I tweeted about it when I found it and judging from the response it seems many others think its a pretty cool pattern as well. But after chasing down the first leak I found, it looks like this pattern is to blame. Before I ask back on the developer forum I figured I would write up what I found here and see if anyone has any better ideas on how to solve it. I’m guessing I’m just missing something with ARC but for the life of me I can’t figure it out.

Comment here, tweet me or email me if you have a better answer.

The Problem

Initial sample code available on BitBucket

In my proof of concept app I have a manager class that creates NSOperations along with their dependencies and then uses NSBlockOperations to pass data between them. It owns its own NSOperationQueue. I simplified this by creating a little sample app with a similar setup. It has an operation that creates an array of strings, an operation that prints an array of strings and an NSBlockOperation that transfers the array between them in the manager:

class OperationOne: NSOperation {
 
    var createdStrings: [String]?
    
    override init() {
        createdStrings = nil
        super.init()
    }
    
    override func main() {
        print("OperationOne - main()")
        createdStrings = ["1", "2", "3", "4", "5"]
    }
    
    deinit {
        print("OperationOne - deinit")
    }
}
 
class OperationTwo: NSOperation {
    
    var passedInStrings: [String]?
 
    override init() {
        passedInStrings = nil
        super.init()
    }
 
    override func main() {
        print("OperationTwo - main()")
        if let passedInStrings = passedInStrings {
            for string in passedInStrings {
                print(string)
            }
        }
    }
    
    deinit {
        print("OperationTwo - deinit")
    }
}
 
class OperationManager {
    
    let operationQueue: NSOperationQueue
    
    init() {
        operationQueue = NSOperationQueue()
        operationQueue.maxConcurrentOperationCount = 1
        
        let operationOne = OperationOne()
        let operationTwo = OperationTwo()
        
        let transferOperation = NSBlockOperation() {
            print("transferOperation")
            if let createStrings = operationOne.createdStrings {
                operationTwo.passedInStrings = createStrings
            }
        }
        
        transferOperation.addDependency(operationOne)
        operationTwo.addDependency(transferOperation)
        
        operationQueue.addOperation(operationOne)
        operationQueue.addOperation(transferOperation)
        operationQueue.addOperation(operationTwo)
    }
}
 
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
 
    var window: UIWindow?
    var operationManager: OperationManager?
 
    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        operationManager = OperationManager()
        // Override point for customization after application launch.
        return true
    }
}

 

If I run this code as is (which you can too if you want), the deinit methods will never get called. Running this in instruments will also be reported as a leak since the operations stay in memory.

This strikes me as a retain cycle. With a little experimenting I found that if I don’t pass the data, everything works fine though obviously I have no strings to print.

let operationOne = OperationOne()
let operationTwo = OperationTwo()
        
let transferOperation = NSBlockOperation() {
    print("transferOperation")
    if let createStrings = operationOne.createdStrings {
//            operationTwo.passedInStrings = createStrings
    }
}
        
transferOperation.addDependency(operationOne)
operationTwo.addDependency(transferOperation)
        
operationQueue.addOperation(operationOne)
operationQueue.addOperation(transferOperation)
operationQueue.addOperation(operationTwo)

 

I also found that if I don’t add the dependency between operationTwo and the transferOperation then everything gets deallocated correctly but its very random if operationTwo has any strings to print when it runs:

let operationOne = OperationOne()
let operationTwo = OperationTwo()
        
let transferOperation = NSBlockOperation() {
    print("transferOperation")
    if let createStrings = operationOne.createdStrings {
        operationTwo.passedInStrings = createStrings
    }
}
      
transferOperation.addDependency(operationOne)
// operationTwo.addDependency(transferOperation)
        
operationQueue.addOperation(operationOne)
operationQueue.addOperation(transferOperation)
operationQueue.addOperation(operationTwo)

 

Pretty clear that one of those two lines is creating a retain cycle. My first thought to fix it was to add weak to my var in OperationTwo:

weak var passedInStrings: [String]?

 

Nope. This creates a compilation error:

OperationTwo.swift:13:14: ‘weak’ cannot be applied to non-class type ‘[String]’

 

Hmm. This seemed like the right path so just to follow up I gave up on using strings and decided to just go with a generic NSObject to pass between operations:

class OperationOne: NSOperation {
 
    var createdObject: NSObject?
    
    override init() {
        createdObject = nil
        super.init()
    }
    
    override func main() {
        print("OperationOne - main()")
        createdObject = NSObject()
    }
    
    deinit {
        print("OperationOne - deinit")
    }
}
 
class OperationTwo: NSOperation {
    
    weak var passedInObject: NSObject?
    
    override init() {
        passedInObject = nil
        super.init()
    }
 
    override func main() {
        print("OperationTwo - main()")
 
        if let passedInObject = passedInObject {
            print(passedInObject)
        }
    }
    
    deinit {
        print("OperationTwo - deinit")
    }
}
 
class OperationManager {
    
    let operationQueue: NSOperationQueue
    
    init() {
        operationQueue = NSOperationQueue()
        operationQueue.maxConcurrentOperationCount = 1
        
        let operationOne = OperationOne()
        let operationTwo = OperationTwo()
        
        let transferOperation = NSBlockOperation() {
            print("transferOperation")
            if let createdObject = operationOne.createdObject {
                operationTwo.passedInObject = createdObject
            }
        }
        
        transferOperation.addDependency(operationOne)
        operationTwo.addDependency(transferOperation)
        
        operationQueue.addOperation(operationOne)
        operationQueue.addOperation(transferOperation)
        operationQueue.addOperation(operationTwo)
    }
}

 

Nope. Same memory leak.

I tried setting the object and/or string array to nil at the end of main() in operationTwo. Nope.

I tried removing all the strings in the array with removeAll() at the end of main() in operationTwo. Nope.

I tried using a capture list in my transferOperation for operationOne and operationTwo:

let transferOperation = NSBlockOperation() {
    [weak operationOne, operationTwo] in
            
    print("transferOperation")
    if let createdObject = operationOne?.createdObject {
        operationTwo.passedInObject = createdObject
    }
}

 

Nope.

I tried using a capture list for just the createdObject:

let transferOperation = NSBlockOperation() {
    [weak createdObject = operationOne.createdObject] in
            
    print("transferOperation")
    operationTwo.passedInObject = createdObject
}

 

Nope. (actually it was worse as the object never made it to operationTwo)

What Does Work?

I did find two things that do work, but they’re horrible and have their own problems.

Create OperationTwo in TransferOperation

With a small change I moved the setting of the passed in createdObject to OperationTwo.init() then used the NSBlockOperation to created it and add it to the queue:

class OperationTwo: NSOperation {

    

    private let passedInObject: NSObject

    

    init(passedInObject: NSObject) {

        self.passedInObject = passedInObject

        super.init()

    }

 

    override func main() {

        print(“OperationTwo – main()”)

        print(passedInObject)

    }

    

    deinit {

        print(“OperationTwo – deinit”)

    }

}

 

class OperationManager {

    

    let operationQueue: NSOperationQueue

    

    init() {

        operationQueue = NSOperationQueue()

        operationQueue.maxConcurrentOperationCount = 1

        

        let operationOne = OperationOne()

        

        let transferOperation = NSBlockOperation() {

            print(“transferOperation”)

            if let createdObject = operationOne.createdObject {

                

                let operationTwo = OperationTwo(passedInObject: createdObject)

                self.operationQueue.addOperation(operationTwo)

            }

        }

        

        transferOperation.addDependency(operationOne)

        

        operationQueue.addOperation(operationOne)

        operationQueue.addOperation(transferOperation)

    }

}

 

I’m no longer creating the dependency so it breaks the retain cycle, but I lose a ton of flexibility. Not the fix I want.

Remove Dependencies with the Completion Block

This works for this simple demonstration but I really doubt it would work in all cases for my proof of concept app:

class OperationManager {
    
    let operationQueue: NSOperationQueue
    
    init() {
        operationQueue = NSOperationQueue()
        operationQueue.maxConcurrentOperationCount = 1
        
        let operationOne = OperationOne()
        let operationTwo = OperationTwo()
        
        operationTwo.completionBlock = {
            [weak operationTwo] in
            print("operationTwo.completionBlock")
            if let allDependencies = operationTwo?.dependencies {
                for dependency in allDependencies {
                    operationTwo?.removeDependency(dependency)
                }
            }
        }
        
        let transferOperation = NSBlockOperation() {
            print("transferOperation")
            if let createdObject = operationOne.createdObject {
                operationTwo.passedInObject = createdObject
            }
        }
        
        transferOperation.addDependency(operationOne)
        operationTwo.addDependency(transferOperation)
        
        operationQueue.addOperation(operationOne)
        operationQueue.addOperation(transferOperation)
        operationQueue.addOperation(operationTwo)
    }
}

Thoughts?

Any better ideas?

I’d love for this to work more seamlessly and I really hope I’m just missing something simple. 

Again – comment here, tweet me or email me if you have a better answer!

Advertisements

Written by Nick Harris

February 3, 2016 at 4:52 am

Posted in Uncategorized

2 Responses

Subscribe to comments with RSS.

  1. Looks like NSOperation takes a strong reference to dependencies. Just make the block operation take a weak reference to operationTwo and you should be okay? From your initial example just change:

    let transferOperation = NSBlockOperation() { [weak operationTwo] in
    print(“transferOperation”)
    if let createStrings = operationOne.createdStrings {
    operationTwo?.passedInStrings = createStrings
    }
    }

    Cody

    February 16, 2016 at 5:07 pm


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: