Cocoa Touch has this concept of basic functionality being immutable and then there is a bigger brother of most classes that adds mutability. One effect of this pradadigm is that often you reuse a single NSString throughout your code because you pass around a pointer to it, retaining it wherever you are interested in it’s continued existence in memory.
Only very rarely you would see the use of the [receiver copy] approach which is like an init but creates a copy of the object. As I said this rarely makes sense because if the original is non-mutable in the first place why do I need to clone it? If it is used in another place and then there it is released and some other static value retained it does not have any adverse effect on other places where it is used, provided that proper retaining occurs.
But this feature is there because there might really BE some esotheric cases. There is even the “copy” attribute for properties which sets up your setters to copy the incoming objects, as opposed to the “retain” attribute which simply retains it.
@property (nonatomic, retain) NSString *myString; @property (nonatomic, copy) NSMutableString *myOtherString; @property (nonatomic, assign) BOOL isCool; |
For retain and copy you shall not forget to add a release in the dealloc method. Otherwise this will leak if the containing object is deallocated from memory. But never release something that was only assigned because then this will cause over-releasing at a different place and can cause weird exceptions outside of your code, making it that much harder to track.
- (void)dealloc { [myString release]; [myOtherString release]; [super dealloc]; } |
Having said that there is a more complex scenario when dealing with container objects. Those are NSArray and NSDictionary primarily, and their mutable descendants NSMutableArray and NSMutableDictionary. For a recent project I needed to make a copy of a mutable dictionary and found that it actually only copies the pointers but does not copy the objects being referenced.
What I needed was called a “deep copy” essentially creating a complete copy of the whole tree contained in the dictionary. Not just the pointers which would continue to point to the same objects, but also clone the objects and their sub-objects and so on and so forth.
The fastest way I found via Google was a recommendation to use NSArchiver and NSUnarchiver to archive and then unarchive the object in one line. Unfortunately those are not available on the Cocoa Touch Framework, but instead there are NSKeyedArchiver and NSKeyedUnarchiver. With a bit of docu-digging I came up with this approach which appears to be doing exactly what I need:
// theTreeDict is a mutable dictionary NSMutableDictionary *deepCopy = [NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject: theTreeDict]]; |
The inner part packs the dictionary to be copied into an NSData, the outer part unpacks it right away creating a totally new copy. Most likely this approach will only work with standard contents like NSString, NSData, NSDate, NSArray, NSNumber etc. To be able to encode/decode your custom classes you will need to provide the initWithCoder and encodeWithCoder methods.
Of course this lends itself to creating a category for NSMutableDictionary and NSDictionary to provide such functionality with less writing. Now creating such a copyDeep extension method raises an interesting question: would we return an autoreleased object or one with retain count 1? To maximize utility and reduce necessary code I would probably opt for the former. But while I was researching this I stumbled on the official method of deep-copying, again buried in CoreFoundation.
- (NSMutableDictionary *)dictionaryByDeepCopying { CFPropertyListRef plist = CFPropertyListCreateDeepCopy(kCFAllocatorDefault, self, kCFPropertyListMutableContainersAndLeaves); // we check if it is the correct type and only return it if it is if ([(id)plist isKindOfClass:[NSMutableDictionary class]]) { return [(NSMutableDictionary *)plist autorelease]; } else { // clean up ref CFRelease(plist); return nil; } } |
So you either use the Cocoa high level approach or you use the richer CoreFoundation method. In both cases you end up with an autoreleased new deep copy of the dictionary. Now changes to “leaves” don’t modify the original.
If you find a scenario where it makes sense to copy dictionaries all the time, let me know.
Categories: Recipes
Hi Oliver,
While working on my first app (Pathology), I had a lot of trouble understanding memory management of composite data structures such as arrays and dictionaries. In particular, I had need to implement a deep copy of heavily nested data structures (e.g. a dictionary item that is an array of arrays of custom objects, etc.) in order to take a “snapshot” of my game state at known stable points in the game loop.
Here’s some code from a test I created to help me understand how to handle copying of container objects:
int DEEP_COPY = 1;
// This class has no distinction between mutable and immutable so I do not need to implement
@interface MyClass : NSObject
{
NSMutableDictionary *myDict;
int myID;
}
@property (nonatomic, retain) NSMutableDictionary *myDict;
@propertyintmyID;
-(id) initWithID: (int) _ID;
– (id)copyWithZone:(NSZone *)zone;
@end
@implementation MyClass
@synthesize myDict, myID;
-(id) initWithID: (int) ID {
if (self = [super init]) {
self.myDict = [NSMutableDictionary dictionaryWithObjectsAndKeys:@”one”,@”first”,@”two”,@”second”,nil];
myID = _ID;
}
returnself;
}
– (id)copyWithZone:(NSZone *)zone {
MyClass *copy;
if (DEEP_COPY) {
// Deep copy
NSLog(@”Performing deep copy in copyWithZone (MyClass)”);
copy = [[MyClass allocWithZone:zone] initWithID:myID]; // This sets the value field
// Continue the copy chain on dictionary elements
copy.myDict = [[NSMutableDictionary allocWithZone:zone] initWithDictionary:myDict copyItems:YES];
} else {
// Shallow copy
NSLog(@”Performing shallow copy in copyWithZone (MyClass)”);
copy = NSCopyObject(self, 0, zone);
}
return [copy autorelease];
}
-(void) dealloc {
[myDict release];
[super dealloc];
}
@end
#import
@interface MemoryTestAppDelegate : NSObject {
UIWindow *window;
NSArray *firstArray;
NSMutableArray *secondArray;
}
@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) NSArray *firstArray;
@property (nonatomic, retain) NSMutableArray *secondArray;
@end
@implementation MemoryTestAppDelegate
@synthesize window, firstArray, secondArray;
– (void)applicationDidFinishLaunching:(UIApplication *)application {
// * Convenience method used, so should use self-dot accessor to retain.
// * Each element is created, then retained by the array, then should be autoreleased so their retain count is balanced.
self.firstArray = [NSArray arrayWithObjects:[[[MyClass alloc] initWithID:1] autorelease], [[[MyClass alloc] initWithID: 2] autorelease], nil];
[[[firstArray objectAtIndex:0] myDict] setValue:@”Initialized” forKey:@”first”];
NSMutableArray *outerArray = [[NSMutableArray alloc] init];
[outerArray addObject:firstArray]; //firstArray retain count incremented
// Test 1 – using copy
// * New memory to hold a second array is allocated, but each object in the array points to the same elements
// as firstArray. Each element’s retain count in the first array is incremented
// * Should use mutableCopy if copying into a mutable array
// * If copy is used here, the following line would cause an exception
// * Since a copy function is called, do not use self dot, which would cause an additional retain
secondArray = [firstArray mutableCopy];
[secondArray addObject :[[[MyClassalloc] initWithID: 2] autorelease]];
// * New memory to hold a second array is allocated, but no new elements created. firstArray retain count incremented.
// * Elements of firstArray are not affected.
NSArray *secondOuterArray = [outerArray copy];
// * After changing the value in the original array, I expect to see the same change in the copied arrays
// because only shallow copies have been made so far.
NSLog(@”Changing first array element to 3″);
[[firstArray objectAtIndex:0] setMyID:3];
NSLog(@”Second array element ID is %0d (Expected value: 3)”, [[secondArray objectAtIndex:0] myID]);
NSLog(@”Second outer array element ID is %0d (Expected value: 3)”, [[[secondOuterArray objectAtIndex:0] objectAtIndex:0 ] myID]);
// ***** test 2 – using initWithArray:copyItems
[secondArray release]; // Sends a release to each element, balancing the retain from the copy
[secondOuterArray release]; // Sends a release to firstArray, firstArray’s elements are unaffected.
// * This creates memory for secondArray and sends a copy message to each element in the first array.
// * The copy message then calls copyWithZone on each element, so the elements must have that method defined.
// * This will create a new MyClass object for each element in secondArray, but how pointers in that object are
// treated will depend on the copyWithZone implementation.
// * Value fields will be independent copies.
secondArray = [[NSMutableArray alloc] initWithArray: firstArray copyItems: YES];
[secondArray addObject :[[[MyClassalloc] initWithID: 2] autorelease]];
// * The only array element in this case is a NSMutableArray, so the copy message is sent to NSMutableArray.
// * It creates a new NSArray element, but the child elements are not copied. No new MyClass objects are created.
// * NSMutableArrays and NSArrays always perform a shallow copy.
secondOuterArray = [[NSArray alloc] initWithArray: outerArray copyItems: YES];
NSLog(@”Changing first array element to 4″);
[[firstArray objectAtIndex:0] setMyID: 4];
// ***** Test the value object
// * In the case of secondArray, new MyClass objects were created by the copy, and the myID field is a value field.
// * So, I expect to see no effect from the change to the original array.
NSLog(@”Second array element ID after initWithArray:copyItems is %0d (Expected value: 3)”, [[secondArray objectAtIndex:0] myID]);
// * Here, no new MyClass objects were created in secondOuterArray, so I expect to see the change reflected here
NSLog(@”Second outer array element ID after initWithArray:copyItems is %0d (Expected value:4)”, [[[secondOuterArray objectAtIndex:0] objectAtIndex:0 ] myID]);
// ***** Test a pointer object
// * The results here depend on how copyWithZone is implemented in MyClass.
[[[firstArray objectAtIndex:0] myDict] setValue:@”Changed” forKey:@”first”];
if (DEEP_COPY) {
NSLog(@”Second array element dict value after initWithArray:copyItems is %@ (Expected value: \”Initialized\”)”, [[[secondArray objectAtIndex:0] myDict] objectForKey:@”first”]);
} else {
NSLog(@”Second array element dict value after initWithArray:copyItems is %@ (Expected value: \”Changed\”)”, [[[secondArray objectAtIndex:0] myDict] objectForKey:@”first”]);
}
// * Here, as in the value object case, no new MyClass elements are in the copy, so the result will always
// match the firstArray
NSLog(@”Second outer array element ID after initWithArray:copyItems is %@ (Expected value: \”Changed\”)”, [[[[secondOuterArray objectAtIndex:0] objectAtIndex:0 ] myDict] objectForKey: @”first”]);
[secondOuterArray release];
[outerArray release];
[window makeKeyAndVisible];
}
– (void)dealloc {
[firstArray release];
[secondArray release];
[window release];
[super dealloc];
}
@end