views:

80

answers:

2

I'm still learning my way through iOS development and working with Core Data and have just come across retain cycles.

It is my understanding from reading the Core Data Programming Guide that after you're done working with a relationship, you use the managed object context method refreshObject:mergeChanges to ensure that the retain cycle is broken.

So lets say I have a to-many relationship between a Department and its Employees, and in my code I access the employees relationship from department, does that mean I'll now need to loop through each employee object and call refreshObject:mergeChanges method? In code this would be

for (Employee *anEmployee in department.employees) {
  //some code that accesses an employee's properties

  [context refreshObject:enEmployee mergeChanges:NO];
}

It seems that if I don't do that, each employee object I access will now contain a reference to the department and I will end up with retain cycles.

Is my understanding correct here? Is this a standard approach when dealing with to-many relationships in Core Data? Thanks.

+1  A: 

As you can check at Breaking Relationship Retain Cycles, the retain cycles are necessary to prevent deallocation of unwanted objects. It means that you keep the the object retained while you are using it.

The refreshObject:mergeChanges should be used if you are done with that object and you want to turn it into fault, to dispose memory if possible. It won't necessarily release the object in the other end of the relationship, it will only set a flag to core data that the object can be turned into fault if necessary.

vfn
So this means that everytime I work with the objects in the relationship I'll need to call that method on the object?
Bart Jedrocha
It's not mandatory to do that, although it's highly recommended on the case that you have a considerably big store, and frequently access distinct `NSManagedObjects`, and don't have the need to keep them in memory (You can let them became faults). Imagine CoreData as a "garbage collector system", on a garbage collected environment you only dispose memory if you are concerned about memory usage. The `refreshObject:mergeChanges` would release the object instead to let it to the garbage collector system. This garbage collector stuff is only an analogy to make you understand the process.
vfn
A: 

I've written a couple of helper methods (see below) to break the retain loops for a whole graph of objects by using introspection of the Entity model. You can use it after receiving a memory warning notification to release any memory held by the part of your core data model accessible through that particular object.

@interface CoreDataHelper(Private)

+ (void)faultObjectImpl:(NSManagedObject *)managedObject mergeChanges:(FaultChangeBehaviour)mergeChanges;
+ (void)faultObjectGraphForObject:(NSManagedObject *)managedObject handledObjects:(NSMutableArray *)handledObjects mergeChanges:(FaultChangeBehaviour)mergeChanges;

@end

@implementation CoreDataHelper

typedef enum FaultChangeBehaviour {
    FaultChangeBehaviourIgnore,
    FaultChangeBehaviourReapply,
    FaultChangeBehaviourMerge
} FaultChangeBehaviour;



+ (void)faultObjectGraphForObject:(NSManagedObject *)managedObject keepChanges:(BOOL)keepChanges {
    NSMutableArray *handledObjects = [NSMutableArray arrayWithCapacity:64];
    FaultChangeBehaviour mergeBehaviour = keepChanges ? FaultChangeBehaviourReapply : FaultChangeBehaviourIgnore;
    [self faultObjectGraphForObject:managedObject handledObjects:handledObjects mergeChanges:mergeBehaviour];
}

+ (void)refreshObject:(NSManagedObject *)managedObject {
    [self faultObjectImpl:managedObject mergeChanges:FaultChangeBehaviourMerge];
}

+ (void)refreshObjectGraphForObject:(NSManagedObject *)managedObject {
    NSMutableArray *handledObjects = [NSMutableArray arrayWithCapacity:64];
    [self faultObjectGraphForObject:managedObject handledObjects:handledObjects mergeChanges:FaultChangeBehaviourMerge];
}

@end

@implementation CoreDataHelper(Private)

+ (void)faultObjectImpl:(NSManagedObject *)managedObject mergeChanges:(FaultChangeBehaviour)mergeChanges {
    //Only fault if the object is not a fault yet and is not in a modified state or newly inserted (not saved yet)
    BOOL isFault = [managedObject isFault];
    BOOL isTemporary = [[managedObject objectID] isTemporaryID];
    BOOL isUpdated = [managedObject isUpdated];

    NSDictionary *changedValues = [managedObject changedValues];

    if (isUpdated && (mergeChanges == FaultChangeBehaviourIgnore)) {
        NSLog(@"Warning, faulting object of class: %@ with changed values: %@. The changes will be lost!", 
              NSStringFromClass([managedObject class]), changedValues);
    }

    if (!isFault && !isTemporary) {
        [[managedObject managedObjectContext] refreshObject:managedObject mergeChanges:(mergeChanges == FaultChangeBehaviourMerge)];
        if (mergeChanges == FaultChangeBehaviourReapply) {
            for (NSString *key in changedValues) {
                id value = [changedValues objectForKey:key];
                @try {
                    [managedObject setValue:value forKey:key];
                } @catch (id exception) {
                    NSLog(@"Could not reapply changed value: %@ for key: %@ on managedObject of class: %@", value, key, NSStringFromClass([managedObject class]));
                }

            }
        }
    }
}

+ (void)faultObjectGraphForObject:(NSManagedObject *)managedObject handledObjects:(NSMutableArray *)handledObjects mergeChanges:(FaultChangeBehaviour)mergeChanges {

    if (managedObject != nil && ![managedObject isFault] && ![handledObjects containsObject:[managedObject objectID]]) {
        [handledObjects addObject:[managedObject objectID]];
        NSEntityDescription *entity = [managedObject entity];

        NSDictionary *relationShips = [entity relationshipsByName];
        NSArray *relationShipNames = [relationShips allKeys];

        for (int i = 0; i < relationShipNames.count; ++i) {
            NSString *relationShipName = [relationShipNames objectAtIndex:i];
            if (![managedObject hasFaultForRelationshipNamed:relationShipName]) {
                id relationShipTarget = [managedObject valueForKey:relationShipName];
                NSRelationshipDescription *relationShipDescription = [relationShips objectForKey:relationShipName];

                if ([relationShipDescription isToMany]) {
                    NSSet *set = [NSSet setWithSet:relationShipTarget];
                    for (NSManagedObject* object in set) {
                        [self faultObjectGraphForObject:object handledObjects:handledObjects mergeChanges:mergeChanges];
                    }
                } else {
                    NSManagedObject *object = relationShipTarget;
                    [self faultObjectGraphForObject:object handledObjects:handledObjects mergeChanges:mergeChanges];
                }
            }
        }

        [self faultObjectImpl:managedObject mergeChanges:mergeChanges];
    }
}

@end
Werner Altewischer
Thanks! This looks very helpful. Doesn't answer my initial question so I won't accept this as an answer but I thank you anyway.
Bart Jedrocha
Is this really necessary? I thought that core data would responds automatically to memory warning. Aren't these helper methods only creating an overhead when a memory warning is raised?
vfn
Core data unfortunately cannot automatically release objects that are part of a retain loop (that is without automatic garbage collection, which is not available on the iPhone), you have to break the retain loops first by either resetting the context or using refreshObject:mergeChanges:NO calls.
Werner Altewischer
See: http://developer.apple.com/library/ios/#documentation/cocoa/Conceptual/CoreData/Articles/cdMemory.html
Werner Altewischer