There’s a neat feature in the Objective-C runtime that very few people know about and even less dare to use them. Undeservingly so, because they are very useful. I’m referring to Associated Objects.
In this recipe I’ll show you how to use them and have a great example of where I used them myself … for the first time.
An associated object is like an instance variable that you add to running code. You know IVARs you usually put in the header or in curly braces below the @implementation. But there’s one disadvantage, you cannot add them in a category. But the compiler will reject if you try to specify new ivars in category extensions.
One way around that I’ve seen myself from using/abusing was the use of global variables. Those are not IVARs because they are not attached to an individual instance, but are truly global. So they are of little use to store information that is related to the instances.
The optimal solution for this dilemma are Associated Objects.
Think of them as Objective-C objects that you attach to an existing instance of a class by a given key, sort of like a dictionary, but since this is a C-API the key is a char *. You specify a memory management policy that tells the runtime how to manage the memory for this object. This is called the “storage policy” and possible values are:
- OBJC_ASSOCIATION_ASSIGN
- OBJC_ASSOCIATION_RETAIN_NONATOMIC
- OBJC_ASSOCIATION_COPY_NONATOMIC
- OBJC_ASSOCIATION_RETAIN
- OBJC_ASSOCIATION_COPY
These policies are used when assigning as well as when the host object is deallocated. If something was only assigned then it will not be released. If it was copied or retained then it will. And you even have a choice of doing the copy/retain atomically or non atomically. This is especially useful when dealing with multi-threaded code accessing the associated objects from multiple threads.
So, to attach (“associate”) an object with another all you need is a single line of code:
static char myKey; objc_setAssociatedObject(self, &myKey, anObject, OBJC_ASSOCIATION_RETAIN); |
In this instance the self object will get a new associated object anObject and the storage policy will be to retain it atomically on assigning and releasing it when self goes away. The same is true if you associate a different object for the same myKey. Then the previous associated object will be properly disposed of and the new one will take its place.
id anObject = objc_getAssociatedObject(self, &myKey); |
You can remove an association by using objc_removeAssociatedObjects or by passing nil for the object for objc_setAssociatedObject.
UPDATE: I was under the mistaken impression that the key would actually be a C-string. But that is not the case, any constant memory address (const void *) will do. The Apple documentation mentions defining a static char (it’s contents does not matter) and then using this variable’s memory address via the & operator.
Making a Block-based Action Handler
The example that I was referring to early uses two associated objects to hold onto a reference to a gesture recognizer as well as a copy of a block. The idea is that you want to attach some actions to perform to any UIView. Since there might be other tap gesture recognizers you want to be able to check if you have already added yours. And the block needs to be copied or else it would be discarded at the end of the current scope when the stack is freed. (Remember, blocks are created on the stack for performance reasons. If you want to keep a block you need to copy it which transfers it to the heap)
So there are two pieces for this to work. One, we have to create the gesture recognizer if necessary and we need to add it and the block as associated objects.
- (void)setTapActionWithBlock:(void (^)(void))block { UITapGestureRecognizer *gesture = objc_getAssociatedObject(self, &kDTActionHandlerTapGestureKey); if (!gesture) { gesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(__handleActionForTapGesture:)]; [self addGestureRecognizer:gesture]; objc_setAssociatedObject(self, &kDTActionHandlerTapGestureKey, gesture, OBJC_ASSOCIATION_RETAIN); } objc_setAssociatedObject(self, &kDTActionHandlerTapBlockKey, block, OBJC_ASSOCIATION_COPY); } |
This retrieves the associated object for the gesture recognizer. If there is none yet, it creates it and establishes the association. Then it associates the block with another key, notice the copy store policy.
The gesture recognizers needs a target and action, so the second part of the scheme is this handler method, following right on its heels.
- (void)__handleActionForTapGesture:(UITapGestureRecognizer *)gesture { if (gesture.state == UIGestureRecognizerStateRecognized) { void(^action)(void) = objc_getAssociatedObject(self, &kDTActionHandlerTapBlockKey); if (action) { action(); } } } |
it is necessary to check the state of the gesture recognizer because this method will be invoked for all the various stages that the gesture recognizer can go through. We only want to fire off the actions when the tap was recognized.
We retrieve the block and as an extra safety measure we only execute it if it is indeed non-nil. This check is smart because while blocks behave like ObjC objects in most circumstances you will get a crash if you are bring to C-function-call it if it is NULL.
Conclusion
As you have seen, Associated Objects are terrifyingly simple to use. They allow you to enhance existing classes also for scenarios where you need to store something that is relevant only to single instances of objects.
One great use would be to define @property properties in a category and write the setter/getter to set/get the value to/from an associated object as opposed to an IVAR to back it.
I’ll definitely have many uses for them, now that I figured out how they work. The source code of this example is part of my DTFoundation project on GitHub, look for UIView+DTActionHandlers.
Categories: Recipes
Great tip, thanks!
Would this be a memory leak, since the retain and copy happen at runtime?
Where do you think there is a leak?
Associated Objects’ lifetimes are controlled by their host object.
I was thinking there might be a leak here, for example:
objc_setAssociatedObject(self, kDTActionHandlerTapBlockKey, block, OBJC_ASSOCIATION_COPY);
I guess I’m confused about when the block would be released. It can’t be handled by ARC since the copy happens at runtime, right? Is there an implicit release when the host object is dealloc’d?
Oh! Nevermind! I see now… You’re not actually doing the copy there. The block is owned by the object that passed it on. Duh.