When it comes to software development using Objective-C frameworks, I like rules. Rules tell me what to do because… they are rules. That’s what rules do. Let’s review some Apple rules on memory management in a reference counted environment (Cocoa for example). First we have the fundamental rule:
- You take ownership of an object if you create it using a method whose name begins with “alloc” or “new” or contains “copy” (for example, alloc, newObject, or mutableCopy), or if you send it a retain message. You are responsible for relinquishing ownership of objects you own using release or autorelease. Any other time you receive an object, you must not release it.
Then you have the following rules derived from the fundamental rule:
- As a corollary of the fundamental rule, if you need to store a received object as a property in an instance variable, you must retain or copy it. (This is not true for weak references, described at “Weak References to Objects,” but these are typically rare.)
- A received object is normally guaranteed to remain valid within the method it was received in (exceptions include multithreaded applications and some Distributed Objects situations, although you must also take care if you modify an object from which you received another object). That method may also safely return the object to its invoker.
Use retain in combination with release or autorelease when needed to prevent an object from being invalidated as a normal side-effect of a message (see “Validity of Shared Objects”).
- autorelease just means “send a release message later” (for some definition of later—see “Autorelease Pools”).
Makes sense right? This is what I live my life by. I favor memory management over garbage collection because I want to make sure I understand it (and the iPhone OS doesn’t do garbage collection, so I am “thinking ahead” you could say).
In my trip down the road of ADC OCD, which entails reading the ADC documentation for a second time because I want to make sure I don’t end up looking like a fool when weaving the artistry that is programming, I came across the following in the “Threading Programming Guide” with respect to using NSMachPort objects to communicate between threads:
- (void)launchThread
{
NSPort* myPort = [NSMachPort port];
if (myPort)
{
// This class handles incoming port messages.
[myPort setDelegate:self];
// Install the port as an input source on the current run loop.
[[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode];
// Detach the thread. Let the worker release the port.
[NSThread detachNewThreadSelector:@selector(LaunchThreadWithPort:)
toTarget:[MyWorkerClass class] withObject:myPort];
}
}
This all seems pretty straight forward, but even using weak deduction powers one should immediately be concerned about the comment “Detach the thread. Let the worker release the port.” What? We’ve covered the reference counted memory management rules already, but let’s even pull this out from the same guide on memory management:
Many classes provide methods of the form +className… that you can use to obtain a new instance of the class. Often referred to as “convenience constructors”, these methods create a new instance of the class, initialize it, and return it for you to use. You do not own objects returned from convenience constructors, or from other accessor methods.
Note that the port: method of the NSMachPort class is really from the superclass NSPort and in general should fit that rule. Note that in these cases, as evidenced from the above, one does not own the object returned. One is not responsible for releasing it. Later the dispatched thread does release it:
+(void)LaunchThreadWithPort:(id)inData
{
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
// Set up the connection between this thread and the main thread.
NSPort* distantPort = (NSPort*)inData;
MyWorkerClass* workerObj = [[self alloc] init];
[workerObj sendCheckinMessage:distantPort];
[distantPort release];
// Let the run loop process things.
do
{
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
beforeDate:[NSDate distantFuture]];
}
while (![workerObj shouldExit]);
[workerObj release];
[pool release];
}
Note that workerObj does retain it however. Confused? I was. This doesn’t follow the rules. It might work, but is it really necessary? Is it that the rules don’t apply, or does that code just happen to work anyway even though it is not quite right? This does not bode well for my “reading the documentation for complete understanding of reference counted memory management in the Objective-C universe on Mac OS X” OCD issue. Of course, according to the rules, one should retain this thing if they need it. However, I feel there’s an extra release in there and it’s just sticking around due to the implementation details hidden in NSPort.
I decided to write a non-AppKit program to test this out. First, one needs to note the following about getting retain counts:
Important: Typically there should be no reason to explicitly ask an object what its retain count is (see retainCount). The result is often misleading, as you may be unaware of what framework objects have retained an object in which you are interested. In debugging memory management issues, you should be concerned only with ensuring that your code adheres to the ownership rules.
Well, I suppose there should be no reason unless you think the documentation example is wrong I guess. In fact, this example demonstrates the above point as well. Here is the test code:
#import <Foundation/Foundation.h>
int
main(void)
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// This should be autoreleased according to "the rules".
NSPort* myPort = [NSMachPort port];
// This should work the same way:
NSString *myString = [NSString stringWithUTF8String:"foobar"];
// This requires a release because we own it.
NSString *allocString = [[NSString alloc] initWithUTF8String:"barfoo"];
// According to the rules, the retain count should be 1 because it is
// autoreleased, but it's actually 2. This indicates to me that it is
// being retained (or was created with allocWithZone as the documentation
// seems to suggest). This is actually all right as long as, by following
// the rules, an autorelease pool drain (or release) would free it. Note
// that this is the reason that it is hard to ask for a retain count and
// know what's really going on (yet I am doing it, sue me).
NSLog(@"myPort retain count = %u\n", [myPort retainCount]);
// But wait, we are supposed to invalidate it before we are done though.
// Let's do that.
[myPort invalidate];
// Now its retain count is 1. Do we need to release this or will it be
// autoreleased? The example code I've seen shows it being explicitly
// released, but according to the rules, this should be an autorelease
// situation.
NSLog(@"myPort retain count = %u\n", [myPort retainCount]);
// myString does the right thing.
NSLog(@"myString retain count = %u\n", [myString retainCount]);
// allocString also has a retain count of 1, doing the right thing again.
NSLog(@"allocString retain count = %u\n", [allocString retainCount]);
// This will release the autoreleases. Note that one should really use
// [pool drain], but without garbabe collection (or the possibility
// thereof), this is the same thing.
[pool release];
// Now any autorelease calls should be dealt with and it should be
// gone. I am surprised this doesn't cause a segmentation fault, and
// the retain count is actually nonsense. If you try to call this again,
// it will cause a sementation fault. One should not rely on retainCount
// making too much sense (I mean, see the behavior here), but it is
// somewhat useful.
NSLog(@"myPort retain count = %u\n", [myPort retainCount]);
// But myString is gone. Calling this will lead to a segmentation fault!
//NSLog(@"myString retain count = %u\n", [myString retainCount]);
// allocString is still here though because we've not released it yet.
NSLog(@"allocString retain count = %u\n", [allocString retainCount]);
// Let's be sure to follow the rules, even though this process will clean
// itself up.
[allocString release];
// Oh yes, please exit appropriately people :-) Not sure I would consider
// this all a success though.
exit(EXIT_SUCCESS);
}
I’ve pretty much explained it in the comments, but let’s see what happens when it runs:
2010-05-23 17:35:01.103 main[965:903] myPort retain count = 2
2010-05-23 17:35:01.105 main[965:903] myPort retain count = 1
2010-05-23 17:35:01.106 main[965:903] myString retain count = 1
2010-05-23 17:35:01.106 main[965:903] allocString retain count = 1
2010-05-23 17:35:01.107 main[965:903] myPort retain count = 4294967295
2010-05-23 17:35:01.107 main[965:903] allocString retain count = 1
Ah, so this really seems to confirm that one does not need to release the NSMachPort object. One is supposed to invalidate it when it’s finished, which drops the retain count from 2 to 1, and then releasing the autorelease pool seems to do the rest (though the retain count is nonsense after that, I am surprised it did not segmentation fault). This seems to confirm the rules. So, I believe the original threading example code is just wrong. I’d love for someone to clean that up or double check (and explain things if I am actually wrong).
What’s also interesting about the results, if we assume that this is working according to the rules (which I claim it is), is that the retain count is originally 2 after creation using the port: class method. The object is actually retained somewhere in the method I believe. This is fine, as long as you do what you are supposed to do (invalidate it), the autorelease mechanism seems to really take care of cleanup – so you should not need to release it yourself. Doing so could be problematic I believe. It is in the autorelease pool, and then they release it? Of course, the example code does not call the invalidate: method either, so maybe that’s why it does not blow up.
This NSPort documentation states:
When you are finished using a port object, you must explicitly invalidate the port object prior to sending it a release message. Similarly, if your application uses garbage collection, you must invalidate the port object before removing any strong references to it. If you do not invalidate the port, the resulting port object may linger and create a memory leak. To invalidate the port object, invoke its invalidate method.
They should only mean using release (not invalidate, that’s needed) if you create it with a method that meets this requirement in the fundamental rule of memory management in reference counted environments. The port: method (which does not fall into the fundamental rule unless its return value is retained) is documented as:
port
Creates and returns a new NSPort object capable of both sending and receiving messages.
+ (NSPort *)port
Return Value
A new NSPort object capable of both sending and receiving messages.
Availability
Available in Mac OS X v10.0 and later.
See Also
+ allocWithZone:
Related Sample Code
SimpleThreads
TrivialThreads
Declared In
NSPort.h
There is nothing above that makes me believe the rules have changed. I don’t believe that the rules in this case are the same as trying to get App Store approval after all (who knows what magic behind the scenes rules are going on there at any give time – don’t get me wrong, I love the App Store as a customer though :-)
It is interesting that I only found one other person who asked about this. I found no answers to that question. I totally obsess over this type of thing because it is important. Writing a threaded implementation is hard enough, but to add any possible confusion about the rules does not help at all. If I were to implement communication between threads using an NSMachPort, I would try to do it the way I know should be correct according to the rules. Now the only thing I know is to do so, but be on the lookout for breakage because it might not work like the rules say. Still, I argue that it really does :-)
Man, someone straighten out that documentation! Of course, I’ll probably use an operation or GCD instead, but it’s still good to know the rules are rules one can live by. I forgot to mention this code example that’s further down in the “Threading Programming Guide” about using an NSMessagePort object:
NSPort* localPort = [[[NSMessagePort alloc] init] retain];
// Configure the object and add it to the current run loop.
[localPort setDelegate:self];
[[NSRunLoop currentRunLoop] addPort:localPort forMode:NSDefaultRunLoopMode];
// Register the port using a specific name. The name must be unique.
NSString* localPortName = [NSString stringWithFormat:@"MyPortName"];
[[NSMessagePortNameServer sharedInstance] registerPort:localPort
name:localPortName];
A retain after an alloc? At this point I have to assume the documentation is simply crazy: [sanity release];
Tags: