ARC Support Without Branching
When Apple announced support for ARC (Automatic Reference Counting), iOS developers jumped for joy at the prospect of no longer having to litter codebases with manual memory management code anymore. Finally, we can concentrate on logic without all the error-prone, time-consuming boilerplate. But, even moreso than most of the new features in iOS5, the transition period for ARC has been a bit painful.
The Problem
The decision to support ARC in your project is clear cut. Yes or no. Most iOS developers use ARC for all new projects that only require a minimum of 4.0 support. (ARC is not supported by iOS 3.1.3.) However, if you are the author of a framework or library, the decision to support ARC isn’t so easy and there are several choices you can make.
The easiest way to go about it is not to convert to ARC at all. Developers can still use your library source code by specifying the -fno-objc-arc option in their project’s Project Sources section.
A big problem with this approach is requiring that option at all. It has to be specified on a per-file basis and, if your library consists of many files such as Cocos2D, the developer would have to set the option for all of them. The XCode interface for this is pretty convoluted and many developers are unaware of the option or where to specify it. Also, if they later updates the library in their project, XCode removes the option and it has to be entered all over again. Pretty obnoxious, and a poor reflection on your code.
Another way to go is to just convert your library to ARC-only. Anyone who isn’t using ARC for their project will have memory leaks and likely a ton of warnings from their static analyzer. This isn’t a realistic option right now. Once a much larger percentage of iOS developers are using ARC it will be. There’s just too many non-ARC projects still out there, especially if iOS 3.1.3 support is required.
So, many developers have decided to add ARC support. A common way is to add a branch to their project that supports ARC. TouchXML, TouchJSON, XMLDocument, and Nimbus are just a small handful of projects that have taken this approach. SVProgressHUD even tried such a branch and abandoned it completely due to the complexity and undesirability of doing such a silly thing!
I highly dissuade you from this approach. Maintaining two branches is rarely a good idea in the real world, let alone for a simple, open-source iOS library. It adds a little complexity to grabbing your source code and will inevitably lead to some commit issues.
There’s a better way.
The Basic Solution
The solution to the problem is conditional compilation. Yes, it’s not only possible, but rather straightforward, to write your code so that it automatically detect the context it’s running in. You won’t have to maintain a second branch and you’ll be able to support ARC and non-ARC code right out of the box.
There’s two methods for doing this and both use the same basic check:
#if __has_feature(objc_arc) ...ARC code here... #else ...non-ARC code here... #endif
The __has_feature(objc_arc) check will not compile the enclosed code for any compiler that doesn’t support ARC, nor for the LLVM 3.0 compiler if ARC isn’t enabled. You can certainly use this code structure and separate your ARC and non-ARC code without reading any further, but if you do you might quickly discover your code will become a mess very quickly. In addition, XCode has historically done a pretty shitty job of automatically indenting code when you use conditional compilation.
How do we keep the code clean?
The Macro Solution
If you want your code to retain (no pun intended) a level of simple readability, macros are definitely the way to go. The idea is that instead of writing completely separate lines of code for ARC and non-ARC, you write macros that adapt to the environment instead.
This is best described by presenting the set of macros which you’re free to drop in to your own project (ARCMacros.h):
// // ARCMacros.h // // Created by John Blanco on 1/28/2011. // Rapture In Venice releases all rights to this code. Feel free use and/or copy it openly and freely! // #if !defined(__clang__) || __clang_major__ < 3 #ifndef __bridge #define __bridge #endif #ifndef __bridge_retain #define __bridge_retain #endif #ifndef __bridge_retained #define __bridge_retained #endif #ifndef __autoreleasing #define __autoreleasing #endif #ifndef __strong #define __strong #endif #ifndef __unsafe_unretained #define __unsafe_unretained #endif #ifndef __weak #define __weak #endif #endif #if __has_feature(objc_arc) #define SAFE_ARC_PROP_RETAIN strong #define SAFE_ARC_RETAIN(x) (x) #define SAFE_ARC_RELEASE(x) #define SAFE_ARC_AUTORELEASE(x) (x) #define SAFE_ARC_BLOCK_COPY(x) (x) #define SAFE_ARC_BLOCK_RELEASE(x) #define SAFE_ARC_SUPER_DEALLOC() #define SAFE_ARC_AUTORELEASE_POOL_START() @autoreleasepool { #define SAFE_ARC_AUTORELEASE_POOL_END() } #else #define SAFE_ARC_PROP_RETAIN retain #define SAFE_ARC_RETAIN(x) ([(x) retain]) #define SAFE_ARC_RELEASE(x) ([(x) release]) #define SAFE_ARC_AUTORELEASE(x) ([(x) autorelease]) #define SAFE_ARC_BLOCK_COPY(x) (Block_copy(x)) #define SAFE_ARC_BLOCK_RELEASE(x) (Block_release(x)) #define SAFE_ARC_SUPER_DEALLOC() ([super dealloc]) #define SAFE_ARC_AUTORELEASE_POOL_START() NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; #define SAFE_ARC_AUTORELEASE_POOL_END() [pool release]; #endif
These macros come packaged with InnerBand. In fact, the InnerBand project supports ARC and non-ARC using these same macros. It serves as an excellent reference of how to use them.
Your mindset should be to use these macros as if you’re programming with old-fashioned manual memory management. When you’re not using ARC, the macros behave as you’d expect with SAFE_ARC_AUTORELEASE calling autorelease and so on. When you’re using ARC, the call is ignored.
The bottom line is you don’t have to worry about it. Just use the macros as if you were coding the old way and everything will be taken care of for you. With just a few caveats, of course.
Notice that __bridge_tranfer isn’t present. It’s not something that can simply be ignored in a non-ARC context. Toll-free bridging is hairy with ARC. If you use it, I recommend sticking with the __has_feature(objc_arc) check and implementing the ARC and non-ARC completely separately.
Also, there may be some edge cases where you need to copy a block in ARC. This is done somewhat differently than using Block_copy. It’s rare, but know that if there is a problem with not calling the copy, you’ll know in the form of crashes.
Demonstration
Now I’ll show you how to use the macros. You can leave your project as non-ARC or convert it to ARC. I prefer leaving it as non-ARC, but that’s just me. You can even create a second target and have both. However you do it, just be sure to test both compiled versions of your code before releasing.
Here’s how you’d perform an autorelease:
- (User *)userWithName:(NSString *)name { return SAFE_ARC_AUTORELEASE([[User alloc] initWithName:name]); }
In non-ARC code, it performs the autorelease. In ARC code, it simply returns the value. Note that with this macro if you call SAFE_ARC_AUTORELEASE on a standalone line, you’ll get a warning that the value is not being used. In almost all cases, an autorelease is coupled with a return or at the end of an allocation, but if you do get the warning you can simply assign the autorelease back itself like this to fix it:
myName = SAFE_ARC_AUTORELEASE(myName);
Retains and releases are similar to autoreleasing:
- (void)setName:(NSString *)name { if (name_ != name) { SAFE_ARC_RELEASE(name_); name_ = SAFE_ARC_RETAIN(name); } ...
If you like to perform your setters with the three-step idiom (retain, release, set), you’ll get the same warning you did with SAFE_ARC_AUTORELEASE. The same fix applies. If you do the two-step idiom (release, retain with if conditional) as above then you won’t need to worry.
Blocks don’t generally need to be copied in ARC. Here’s how you’d handle it in your multi-context code:
- (id)initWithBlock:(void (^)(void))block if ((self = [super init])) { block_ = SAFE_ARC_BLOCK_COPY(block); } return self; } - (void)dealloc { SAFE_ARC_BLOCK_RELEASE(block_); SAFE_ARC_SUPER_DEALLOC(); }
The above also demonstrates how to call a dealloc on the super class, which is expressly forbidden in ARC. The code is removed by the compiler when using ARC, but calls [super dealloc] when compiled as older code.
Autorelease pools have changed, too. Although not strictly necessary, the two different autorelease pool declarations are supported. When you write code to work within an autorelease pool, wrap it like this:
- (id)complexMethodCall { SAFE_ARC_AUTORELEASE_POOL_START(); ...my code here... SAFE_ARC_AUTORELEASE_POOL_END(); }
In ARC, there’s a lot more options for specifying a property. While assign and copy can still be used, there’s a newer specifier called strong. This is essentially retain, so you can write your property like this:
@property(nonatomic, SAFE_ARC_PROP_RETAIN) User *user;
If you want to use any ARC-specific specifiers other than that, you’ll need to use code compilation with __has_feature(objc_arc) since there are no non-ARC equivalents.
And finally, there’s Toll-Free Bridging. This is one area that got more complex in ARC.
I usually resort to code compilation. See the InnerBand code for examples of this surgical procedure. Luckily, you won’t use it much unless you’re using Core Graphics.
Parting Words
These macros are not being offered as an alternative to knowing ARC. On the contrary, you should know ARC before attempting to convert any older code over. The spirit of these macros is to easily, and cleanly, allow library code to run in either context. If you’re not feeling so hot on your ARC knowledge, my favorite resources for ARC are Apple’s ARC Documentation, Mike Ash, and a hidden gem from the maker of Kobold2D.
Getting Started with Mogenerator I Have An Idea for an App! Now What?!
Comments are currently closed.