Posts Exploring ARC
Post
Cancel

Exploring ARC

In the good old days we managed memory directly - and by good old days, I really mean before mainstream garbage collection, shared_ptr<T> and its cohorts, and our beloved Objective-C retain/release. In those days, memory errors were a lot more common, but also a lot better understood by most than today - after all, if you were manually managing your memory you understood quite clearly how memory management operations work.

These days, most working programmers don’t have a full understanding of the memory allocation and management scheme used by their language of choice, and honestly, for the most part, this doesn’t impact them. They may produce code that makes lots of unnecessary temporary copies, and probably isn’t as efficient in terms of latency or memory footprint as it could be if they had a better understanding, but for most programmers, that doesn’t matter most of the time I’d wager. That said - for those willing there is a lot to be gained by jumping into the deep end of the memory management pool and having a look at how it all works under the covers. To that end, let’s discuss how ARC works in Objective-C, and look at some common pitfalls.

Reference Counting

One step above manually managing your allocations and deallocations is the concept of reference counting. Most of you are familiar with reference counting, but for those who aren’t - the concept is you store, along with a pointer, a counter that indicates how many references exist to this memory address, and when it reaches zero, you deallocate the object. This means that objects are only deallocated, at least in theory, when they are unreachable and unreferenced. The benefit of reference counting is you avoid the overhead associated with garbage collectors, and also retain a lot of control over object lifetimes by controling the reference count.

Let’s automate it!

Originally in Objective-C, you had to manually call retain and release to increment and decrement the reference count of objects, respectively, which led to numerous errors where people would over-release an object or under-release an object, which led to crashes and memory leaks.

With the switch to LLVM and the addition of a powerful static analyzer to Objective-C’s toolbox, it was realized that the compiler could warn programmers when they likely forgot to insert retains or releases in their code. Once this was achieved, it was wondered if it could be taken one step further, and if if the compiler could automatically insert retains and releases where necessary. And hence we arrive at Automatic Reference Counting (ARC).

Ownership

One thing I’m going to be repeating over and over throughout this article is that the key principle behind memory management, and ARC in particular, is ownership. If you don’t clearly understand which objects create and own each other you’re going to run into memory managment issues sooner or later. ARC is not a magic bullet, and it’s important to understand what ownership means for your codebase as well as how it determines ARC behavior.

Ownership Qualifiers

For ARC to be able to automatically manage retain and release calls, it must understand how objects are owned, and which objects own each other, in your application. For this purpose, it provides four ownership qualifiers. Keep in mind that while these influence the retain and release behavior of objects, it’s still best to think about them as denoting ownership.

Strong

Strong pointers denote a form of ownership. Strong pointers retain objects they point to during assignment and release them when the pointer is on the left side of the assignment operator again. Most people are very familiar with this qualifier as it is the most commonly used, so I won’t go into any more detail.

Weak

Weak pointers denote a non-ownership reference, the most common example probably being delegates. They are used to break retain cycles, which are discussed later, and make several guarantees that make them very useful. They are nilled out when the objects they point to are deallocated.

Autorelease

Autoreleased pointers have a similar effect to calling [obj autorelease] in non-ARC code - they guarantee that an object will live across a call boundary and are primarily used for out parameters.

Unsafe Unretained

Unsafe unretained pointers are ‘primitive’ pointers - they have no retain or release operations associated with them and should rarely be used. One thing to note is that several older Objective-C APIs, including KVO, use __unsafe_unretained to keep references to objects, so it is important to check the older APIs for that type of issue and read the documentation very carefully while using them.

Inferring Ownership

Generally speaking, we rarely explicitly qualify the ownership of objects, and so ARC uses inference to determine what ownership qualifier to use. There are a few special cases, but most of the time, you can assume ARC will use __strong unless you tell it otherwise. I’ve outlined the one case you most likely need to know about below.

Out Pointers

Indirect pointers, that is, pointers of the form T**, have a different inference rule for their ownership qualification - in the case of method parameters, indirect pointers are inferred to posess the ownership qualifier __autoreleasing. The big reason for this is to avoid deallocation of “out” parameters like the common NSError** before the caller can use them.

Corner Cases

Losing ownership while executing

One of the most common points of confusion I’ve found regards __weak pointers and if a __weak pointer, if used to invoke a method, can suddenly be deallocated while the method is running, thereby leaving a method mid-invocation and with an invalid self pointer.

In a word: No.

ARC explicitly tells us that __weak pointers are retained for the duration of the fully qualified expression they are used in - this means that a method call on a __weak pointer either has a valid self the entire time it executes, or it is invoked on the nil pointer and does nothing - but it cannot be nilled out halfway through execution. Let’s look at some sample code:

1
2
3
4
5
- (void) testWeak
{
  __weak TestClass* test = _test;
  [test callMethod];
}

So you can see here we’re sending a message to a weak pointer - let’s see how the compiler handles this and look at the assembly:

1
2
3
blx _objc_loadWeakRetained
blx _objc_msgSend
bl  _objc_release

I’ve trimmed the assembly to remove some superfluous code and make it more compact - but the important parts remain. The compiler calls objc_loadWeakRetained which loads and retains the weak pointer, to ensure that for the duration of the expression it remains valid, in this case, that expression being a method call. Afterwards, objc_release is called to balance the previous retain.

However - there is more to this.

__strong pointers do not make this guarantee - if you invoke a method on a __strong pointer, and then its retain count reaches zero from being nilled in another thread, the method invocation will suddenly have an invalid self pointer. This is an important thing to remember, and I’ll say it several times, __strong implies you know something about an object’s ownership. ARC counts on you to make sure this situation can’t occur.

Bridged Casts

Bridging casts should be used when you need to convert between object types that are reference counted and toll-free bridged - so between NS and CF variants of the same underlying object. This means moving between NSDictionary* and CFDictionaryRef. The way you do this is important, because core foundation types, while reference counted, are not managed through ARC, so when they are casted to or from a type that is managed using ARC we need to let ARC know what to do.

  • __bridge casts between CF and NS types without transfering ownership - so whichever object is currently the “owner” remains the owner. In other words, if you are casting from a CFDictionaryRef to an NSDictionary* if you release the CFDictionaryRef the object will be deallocated even though the NSDictionary* still references it.
  • __bridge_retained casts an NS object to a CF object, and increments the retain count by one prior to the cast - this transfers ownership so the original object pointer can be set to nil without destroying the object.
  • __bridge_transfer casts a CF object to an NS object and tells ARC that it now has ownership of the object - so the object will be released at the end of the current scope automatically - you don’t need to call CFRelease on the original object handle.

It’s also worth noting that core foundation has macros that do similar things - CFBridgingRelease and CFBridgingRetain.

Consumed Parameters and Retained Returns

As discussed earlier, ARC really centers around the concept of ownership, and to this end it provides several attributes to signal that a method will assume ownership of a parameter, or give ownership of a return back to the caller. These impact how ARC inserts retains, subject to its optimization rules, and have some other technical impacts we’ll get into, but I encourage you to think of these primarily as denoting ownership instead of just modifying the retain count of the given parameters.

Normally, when passing parameters, ARC uses the same inference rules we discussed earlier, so most non-explicitly qualified parameters end up with __strong. This results in the method prologue generally having retain operations inserted during assignment in the form of objc_storeStrong - you can see an example of this below

1
2
3
4
mov r0, r3
mov r1, r2
bl  _objc_storeStrong
.loc  1 15 0 prologue_end

From a high level, what this means, is that the original caller still owns the object that it passes in, it remains responsible for its lifetime and semantically speaking the function is simply using the parameter for an operation - not taking over ownership or lifetime determination.

At a lower level, the impact of this is that once a method is called, before the method body begins, parameters (with the exception of the implicit self parameter) are stored. One thing to note, though incredibly unlikely, it is possible that this could generate an invalid memory location should a race condition exist between the objc_storeStrong and a release on the original object. In practice, this should not occur, particularly if you manage object lifetimes well, but is worth mentioning.

To change this behavior, you can attribute a parameter with the ns_consumed attribute. At a high level, this attribute tells ARC that the parameter being passed in is actually being given to the function, with the function now having ownership. From a lower level perspective, while ARC may still insert the objc_storeStrong call, you are guaranteed the parameter will be retained before the method is called instead of during the prologue - in other words, the object will be retained before begin passed as a parameter. This guarantees this pointer is in a valid state, and means the method is entered with a retain count one higher than usual. ARC cleans this up at the end of the method with a release prior to leaving the method. A small example of the generated assembly is below:

1
2
3
mov r0, r1
bl  _objc_retain
[method invocation]

It’s worth noting, as we go into detail in the optimization section, ARC may actually not even perform the objc_storeStrong on a parameter that is consumed, because it can rely on the fact that it comes in already retained.

The most notable method that has a consumed parameter is init which also is special since it consumes the implicit parameter: self. To consume the self pointer you attribute the entire method with the ns_consumes_self attribute.

In addition to consumed parameters, methods can also have an attribute that denotes that they surrender ownership of the object they return - this has the converse effect to the one we went into before - an object will be retained prior to it being returned, and the caller will release it at the end of the full expression it is a part of. The attribute that denotes this behavior is ns_returns_retained. Once again, while it does influence the behavior of ARC, it’s most useful to remember this as surrendering ownership to the caller.

Retain Cycles

Probably the most common, and hopefully best understood, aspect of ARC, and indeed reference counting in general, is the retain cycle. The general concept is simple: we figure out when to deallocate objects based on how many references (the retain count) they have, so what happens if object A references object B which also references A. They will eventually only reference each other, and be unreachable, but cannot be deallocated since their reference counts never fall to zero.

The classic approach to this is to use __weak pointers for these sorts of cases - so A references B, but B references A only weakly, thereby not incrementing the reference count of A and permitting A, and subsequently B, to be deallocated. This solution is fine, and works great in the general case, but there’s a few specific points I’d like to bring up.

Using __weak everywhere to avoid having to manage your object lifetimes is not a solution: it’s an even worse problem. Abuse of __weak to avoid thinking about object lifetimes is probably one of the worst problems I’ve seen in a lot of Objective-C code in terms of code quality. So, why is this so important? Glad you asked.

__weak in Objective-C implies that the object holding the reference does not know, and in fact, can’t know about the pointed-to-object’s lifetime in a meaningful way, because in fact it isn’t an owner of the object. Given this ARC makes a different set of guarantees around it, including that method calls invoked on weak pointers will have a valid self parameter for the duration of the method, a guarantee not made on __strong pointers, which ARC assumes users of understand the lifetimes of.

To this end, generally think about where you’re using __weak, and ask not only are you breaking a retain cycle, but why are you having to break a retain cycle here. Are you using __weak because the object you’re passing the object to really shouldn’t be an owner, or are you doing it because it breaks a retain cycle that is being caused by poorly thought out or evolved architecture?

Imprecise Lifetimes and Interior Pointers

One thing that can occasionally get people is using interior pointers with imprecise lifetimes. Wow, that was a lot of strange words in one sentence.

Interior pointers are pointers to objects within another object - for instance, if you have an Objective-C object shown below

1
2
3
4
5
@interface MyObject
{
  int* myInt;
}
@end

myInt would be an interior pointer in this object. The lifetime of an interior pointer is determined by its enclosing object, so when the enclosing object that stores the interior pointer gets deallocated, the interior pointers generally will as well.

So how does this end up causing a problem? Let’s say I want to store that interior pointer…like…this:

1
2
3
4
5
6
7
8
- (int*) doSomeStuff
{
  MyObject* obj = [[MyObject alloc] init];
  [obj setupMyInt];
  int* importantInteger = obj.myInt;
  *importantInteger += 3
  return *importantInteger;
}

This shouldn’t be a problem - I have a strong pointer to a MyObject that I create and it, as well as its interior pointer, live until the end of the doSomeStuff method.

Nope. importantInteger may point to invalid memory. Note I said may, not will, as it all really depends on how ARC optimizes this.

ARC does not guarantee precise lifetime semantics for every strong pointer, it only guarantees that the object won’t be deallocated until at least the last usage. In this case, we aren’t using the object, we’re using an interior pointer, and ARC has no idea that the int* in question will become invalid memory when it deallocates the enclosing object.

So, how can we get around this? Well, there’s a few things we can do. The first is, we can request precise lifetime semantics using the objc_precise_lifetime attribute. Using this disables ARC’s optimization for this variable and will prevent the problem, but really, the issue is probably not with the caller, but with the callee not properly letting ARC know that an interior pointer is being exposed - which we can handily let ARC know about by using the objc_returns_inner_pointer annotation on the method returning the interior pointer.

One thing to note about this approach is that this is of particular concern for non-object pointer types - that is, types not managed by ARC. In the case of a type managed by ARC the object will be retained once it is returned, so it won’t be deallocated even if the object that owns it is.

Unexpected Optimizations

The last gotcha I really want to bring up is something people get caught up in when they forget that ARC, at its core, is always trying to be fast: operations may or may not be pruned based on guarantees ARC can make at compile time. I have alluded to this before. ARC makes guarantees about the lifetimes of your objects, it doesn’t really guarantee every “theoretical” retain/release pair will end up in your code, nor does it guarantee that autorelease will ever actually be called - indeed, we’ve seen in a lot of cases ARC explicitly avoids this for efficiency.

This isn’t a big deal, and doesn’t cause problems unless you assume ARC is going to call retain, or release, or autorelease at certain times, instead of just assuming that ARC is going to make sure your objects stay alive in the ways it promises.

So that’s it…

Hopefully, this little document has helped you understand how ARC works a bit more at a higher level, as well as in a bit more technical depth. If you have anything you think I’ve missed or gotten wrong, please let me know.

I know there’s also a lot I haven’t covered - including method families, and some details I’ve skimmed over how autorelease and weak pointers work, and plenty of other low level details we can get into. If you liked this post, let me know, and if enough people are interested I may write a second one going into more details.

References and Reading Materials