Core Data: avoiding retain cycles in to-many relationships
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.
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.
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
My experience is that re-faulting only the department entity is enough to break the retain cycle. Profiling memory clearly shows that all related employees are then freed, unless they are retained elsewhere by your code.
精彩评论