Understanding Manual Retain Release in Objective-C
You might often hear programmers claiming Automatic Reference Counting (ARC) is one of the best things that happened to Objective-C. It relieves you, the programmer, from typing out redundant code. It’s a convenience if you’ve ever written any Manual Retain Release (MRR) code. But if you joined the club later and haven’t actually every written any MRR code, you might get a wrong impression that MRR is verbosely spammed with calls to retain
and release
. One factor could be that people assume MRR code is equivalent to C style memory management, where you need to call malloc
and free
and then figure out complex ways to manage the ownership. Another factor could be because a lot of ARC vs MRR discussion overlook the fact that MRR already has reference counting, something equivalent of what was finally introduced in C++11 with smart pointers. Another confusion might have to do with the fact that ARC is not really clear on memory ownership rules. So some people might assume every assignment operation is either a transfer of ownership or claiming ownership, which isn’t entirely false, but not in the sense of how retain
and release
is designed to work.
I think every new Objective-C programmer should at least write some MRR code to understand what are the redundancies that ARC is trying to get rid of. Also if you look beyond ARC, you might realize the clear pattern of memory ownership, which honestly isn’t that complicated. The data isn’t randomly allocated or randomly transferred from an instance to another. There’s a very clear pattern of when that happens and MRR highlights them clearly.
In real life, a well architected code has a clean vision of memory ownership. There’s a concept of object graph which should ideally be an acyclic directed graph with clear parent child relationships. When, a parent claims the ownership then any reference to the data passed to the children can almost always assume that the data won’t get deallocated out of nowhere, since parent should always outlive the children. In all of these cases there’s no transfer of ownership, so all those reference could be a weak
reference. In other cases where there’s a temporary factory class involved who’s only job is to allocated the data, the ownership has to be transferred at some point.
Then there is the amazing idea of autorelease pool, where the ownership of data is temporary transferred to the run loop, for data that can be disposed at the end of the run loop cycle or should be claimed by somebody before the end of the loop cycle.
And finally there are rules around transfer of ownership when working with an external library.
In almost all cases there are only a finite set of scenarios possible:
1. Transfer of ownership from client to library
In library calls, it might happen quite often that the library is designed such that it delegates the responsibility of memory allocation to the client and then transfer the ownership back to library. An example is NSArray
Foo *foo = [[Foo alloc] init];
[array addObject:foo]; // transfer of ownership
[foo release];
2. Transfer of ownership from library to client
In other scenarios the library might transfer the ownership to the client. Most of the class factory methods that wrap initializer with autorelease
do this
@implementation Person
+ (instancetype)personWithName:(NSString *)name
{
return [[[Person alloc] initWithName:name] autorelease]; // transfer ownership to autorelease pool
}
- (instancetype)initWithName:(NSString *)name
{
// ...
}
Which is a way to say that for the time being the ownership has been transferred to the autorelease pool. Now it’s up to the client on how they wish to manage the memory. And depending on the usage, the clients can call [person retain]
to claim the ownership otherwise the data would be deallocated at the end of the run loop cycle.
Person *tmpPerson = [Person personWithName:@"John"];
_person = [[Person personWithName:@"John"] retain]; // transfer of ownership
3. Dealing with asynchronous callbacks
In cases where an asynchronous calls are being made, there’s a chance that caller might get disposed by the time the callback happens. These scenarios have to be carefully designed for. Again, there’s no magic trick here, just make sure the listener object lives long enough to handle the callback.
4. Multiple owners of the data
Another situation is when multiple instanced deliberately want to acquire ownership of the same data. For example say multiple children want to work with same image data and the parent doesn’t want to claim ownership of the image data. In these scenarios, we can rely on the reference counting and every child can claim ownership. Later when the last child relinquishes the ownership the shared data would get deallocated.
@implementation Child
- (instancetype)initWithImage:(UIImage *)image
{
self = [super init];
if (self) {
_image = [image retain];
}
return self;
}
- (void)dealloc
{
[_image release];
[super dealloc];
}
@end
Once you understand these basic rules and the complexities around them, you might think for yourself if you like ARC or not. Maybe you don’t like the magical aspect of ARC, or you don’t like how ARC interacts with plain C struct
types which also relates to how ARC interacts with CoreFoundation. Also since ARC can be enabled and disabled per file basis, one might have it enabled for parts of code that are simpler and disabled for complex ones.