This setup actually does some very interesting things, and raises a couple good points about Objective-C memory management. Let's first reiterate the code:
// Testing.h
@interface Testing : NSObject {
NSMutableString *retainString;
NSMutableString *copyString;
}
@property(nonatomic,retain) NSMutableString *retainString;
@property(nonatomic,copy) NSMutableString *copyString;
// Testing.m
@implementation Testing
@synthesize retainString, copyString;
- (id)init {
if(self = [super init]) {
NSMutableString *test = [[NSMutableString alloc] init];
NSLog(@"test %d; retain %d; copy %d", [test retainCount], [retainString retainCount], [copyString retainCount]);
self.retainString = test;
NSLog(@"test %d; retain %d; copy %d", [test retainCount], [retainString retainCount], [copyString retainCount]);
self.copyString = test;
NSLog(@"test %d; retain %d; copy %d", [test retainCount], [retainString retainCount], [copyString retainCount]);
[self.copyString appendFormat:@"test"];
NSLog(@"test %d; retain %d; copy %d", [test retainCount], [retainString retainCount], [copyString retainCount]);
}
return self;
}
@end
This produces the log output:
2009-12-24 03:35:01.408 RetainCountTesting[1429:40b] test 1; retain 0; copy 0
2009-12-24 03:35:01.410 RetainCountTesting[1429:40b] test 2; retain 2; copy 0
2009-12-24 03:35:01.410 RetainCountTesting[1429:40b] test 2; retain 2; copy 2147483647
2009-12-24 03:35:01.413 RetainCountTesting[1429:40b] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Attempt to mutate immutable object with appendFormat:'
So what's going on here? The first two calls are fairly straightforward:
- The initial call to
alloc
/init
creates a new NSMutableString object with retain count 1, as expected. We have one object with one retain on it.
- The assignment to the
retain
ed property increments the retain count, as expected. We have one object with two retains on it.
Here's where it gets strange. The assignment to the copy
property does indeed make a copy, but not in the way you'd expect. NSString and NSMutableString are part of what's called a class cluster - when you create or modify a string, it may or may not be an instance of the class you're expecting. The language may mutate it to some other representation behind the scenes.
In this particular case, when the copy is performed, apparently the language decides that the string (since it contains no information) is to be considered immutable, and makes it so. This is often seen when people do something like [[NSString alloc] initWithString:@"hello"]
- it's a constant, static string, so no object need be allocated dynamically. Keeping it static helps the runtime perform better.
So now we have two objects: our original test
object that was retained twice, and the new object that is static and therefore has a retain count of INT_MAX
. Finally, since the new string is immutable, calling a mutator method on it kills the program.
As an aside, changing the original call from init
to initWithString:
does make the copy assignment perform (somewhat) as expected - you only get a retain count of 1 on the copied object, but you still can't mutate it. Again, this is probably due to some optimization magic inside the compiler that decided that string was static and saw no reason to make it mutable if it didn't have to.
To answer your final question: yes, you can call release
on either of these objects. It just won't do much. At best, you'll have destroyed the copied object (since it had a retain count of 1); at worst, it'll do nothing to a static string object. However, I'd recommend continuing to work through properties: rather than releasing the copied object, why not just do self.copyString = nil;
? Since it calls the property setter, it'll take care of the release as necessary, and then you don't have a pointer to the object still floating around.
For more information on all this, consider reading: