2

I am hoping someone can explain why associated objects in the following example do not get automatically deallocated when the source/host object is deallocated. This example code below is somewhat contrived (apologies in advance), but it explains the my issue.


The example assumes a CoreData entity Product with an string attribute sku and the default CoreData stack provided by the Xcode template:

import UIKit
import CoreData

class ViewController: UIViewController {

    @IBAction func createProduct(sender: AnyObject) {

        let context = CoreDataHelpers.vendBackgroundWorkerContext()
        let newProduct = CoreDataHelpers.newProduct(context: context)

        newProduct.sku = "8-084220001"

        do {
            try newProduct.managedObjectContext?.save()
            print("Product created [SKU: \(newProduct.sku ?? "NotDefined")]")
        } catch {
            print(error)
        }
    }
}


public class CoreDataHelpers {

    public static let mainContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext

    public class func vendBackgroundWorkerContext() -> NSManagedObjectContext {
        let managedObjectContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
        managedObjectContext.parentContext = self.mainContext

        return managedObjectContext
    }

    class func newProduct(context context: NSManagedObjectContext) -> Product {
        let newProduct = NSEntityDescription.insertNewObjectForEntityForName("Product", inManagedObjectContext: context) as! Product

        return newProduct
    }

}

when the createProduct function is executed, a new PrivateQueueConcurrencyType Managed Object Context (MOC) will be vended and used by the new Product Managed Object (MO). This above code works correctly - so far.

However! if I combine the first two lines of the createProduct function such that:

let newProduct = CoreDataHelpers.newProduct(context: CoreDataHelpers.vendBackgroundWorkerContext())

then the app will crash at try newProduct.managedObjectContext?.save() with a EXC_BAD_ACCESS.

At first glance, this appears a little strange - as all we have done is refactored the code. Digging into the documentation, the managedObjectContext property is declared as unowned(unsafe). This probably means that the created MOC has been deallocated and we have a dangling pointer (please correct me if my assumption is wrong).

In order to ensure that MOC does not get deallocated, I tried associating it with the MO itself. newProduct:

class func newProduct(context context: NSManagedObjectContext) -> Product {
    let newProduct = NSEntityDescription.insertNewObjectForEntityForName("Product", inManagedObjectContext: context) as! Product

    var key: UInt8 = 0
    objc_setAssociatedObject(newProduct, &key, context, .OBJC_ASSOCIATION_RETAIN)

    return newProduct
}

This appears to works wonderfully - until I check in Instruments. It would appear that when the Product MO is deallocated, the now associated MOC is not (shouldn't it be automatically deallocated when the source object is deallocated?)

My question is: Can someone explain where the additional reference is to the MOC that is preventing it from being deallocated? Have I created a retain cycle between the MO and the MOC?

enter image description here

So Over It
  • 3,668
  • 3
  • 35
  • 46

1 Answers1

1

You are probably creating a circular ownership (retain cycle).

Every managed object is owned by a managed context (the context owns the object) and setting the context as associated object means that the object now also owns the context.

Therefore, they won't get deallocated.

The real solution is to save the background context to a local property, the same you are doing with mainContext.

Sulthan
  • 128,090
  • 22
  • 218
  • 270
  • Why shouldn't worker contexts be destroyed/created on demand? Eg. two MOs that are being changed that are not yet persisted. If on the same context, a save would persist BOTH MOs - you may have just wanted to persist one of them). Notable CD stacks vending multiple worker contexts (eg. https://www.bignerdranch.com/blog/introducing-the-big-nerd-ranch-core-data-stack/). – So Over It Jul 15 '16 at 06:35
  • @SoOverIt You are right, I have removed the last part of my answer. However, you still have to make sure that you have a strong reference to any managed context that you are using. – Sulthan Jul 15 '16 at 06:40
  • You mention that 'every managed object is owned by a managed context'. This is not entirely true. You can still have a Managed Object where the Context has been deleted. See the documentation re the managedObjectContext in the NSManagedObject Class Reference (https://developer.apple.com/library/mac/documentation/Cocoa/Reference/CoreDataFramework/Classes/NSManagedObject_Class/#//apple_ref/occ/instp/NSManagedObject/managedObjectContext) - "May be nil if the receiver has been deleted from its context." – So Over It Jul 15 '16 at 06:40
  • @SoOverIt Well, yes, but the managed object becomes unusable. You cannot save it and read data from it any more. – Sulthan Jul 15 '16 at 06:43
  • Very true. It does become unusable. Which is why I'm wanting the managed object context to stay around. – So Over It Jul 15 '16 at 06:55