Monday, July 13, 2009

Memory Management for Cocoa composite objects

While working on 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 is some example code that I used to test my understanding of how to copy collection objects (NSArray, NSDictionary) as either a shallow copy or a deep copy. Also, how to implement proper copying in your own class for deep and shallow copying:



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