views:

338

answers:

5

Update:

iPhone OS 3.1 has associated objects. However, the iPhone simulator does not. If you want to test associated objects code in the simulator, you should file a bug.

See my SO question here.

rdar://7477326


Snow Leopard now has associated objects.

Is there a way to accomplish something similar without associated objects? (Specifically for the iPhone.)

I am pretty sure I saw something like this a while back, but I can't remember where. Something about turning any object into a KVC container.

+2  A: 

You could always have them stored in a singleton.

Jeff Kelley
+2  A: 

There are no good ways to do this in a generic category.

You can easily add data for an object by having a global NSMutableDictionary that maps from any arbitrary NSObject to whatever data you want. The problem is there is no way to know when the object is deallocated, so you cannot tell (in general) when the data goes stale.

The only generic way to solve this is to use method swizzling to replace the NSObject dealloc method to report the deallocation of the object and release your associated data. I'm sure someone has done this, but its such a hideous hack it would be very hard to recommend as a valid appropach.

Now, if your objects in questions have some other way to monitor life cycle (ie, some deallocation hook, like a delegate objectWillClose method of some sort), then you can hook in to that to release your associated data and that would make the technique quite straight forward and legitimate.

Peter N Lewis
iPhone obviously doesn't have GC, but wouldn't it be possible to use an NSMapTable with weak keys instead? http://developer.apple.com/documentation/Cocoa/Reference/NSMapTable_class/
Quinn Taylor
You actually can't because in general the key in that table is aactually the only thing rooting the associated object, so if it is weak then it will be immediately be eligible to be GCed and zeroed after setting it once it is no longer directly rooted via the stack.
Louis Gerbarg
+10  A: 

objc_setAssociatedObject() and friends were added to iPhone OS 3.1, so if you have the option of targetting just 3.1+ devices you can in fact do the exact same thing as on Snow Leopard...

If you can't you can create a static dictionary of associations and monkey patch out NSObjects dealloc method. For various technical reasons this solution cannot be made to work correctly in the presence of GC (which is why apple added the association stuff), but since iPhone does not support GC that is a non-issue.

If you are just starting work on this project I highly recommend using the runtime functions and targeting 3.1 plus, but if that is not an option here is an example of how you do it.

LGAssociativeStorage.h:

#import <pthread.h>
#import <Foundation/Foundation.h>

@interface NSObject (LGAssociativeStorage)
@property (retain) id associatedObject;
@end

LGAssociativeStorage.mm

#import <objc/runtime.h>
#import "LGAssociativeStorage.h"

/* We are using STL containers because:
   1) Using Objective C containers can cause deallocs which cause recursion issues
   2) STL containers are high perf containers that don't introduce external code dependencies
   Ideally one could include a thread safe map implementation, but I don't need one currently
*/

#include <map>

typedef std::map<id,id> idMap_t;
typedef std::pair<id,id> idPair_t;

static NSMutableDictionary * data = nil;
static pthread_mutex_t data_lock = PTHREAD_MUTEX_INITIALIZER;
static IMP gOriginalNSObjectDealloc = nil;
static idMap_t  associatedObjectMap;

static
void removeAssociatedObjectFromMap(id self) {
  idMap_t::iterator iter = associatedObjectMap.find(self);
    if( iter != associatedObjectMap.end() ) {
     [iter->second release];
     associatedObjectMap.erase(iter);
    }
}

static
id newNSObjectDealloc(id self, SEL deallocSelector, ...) {
    pthread_mutex_lock(&data_lock);
    removeAssociatedObjectFromMap(self);
    pthread_mutex_unlock(&data_lock);
    return gOriginalNSObjectDealloc(self, deallocSelector);
}

static void initIfNecessary(void) {
    if (!data) {
     data = [[NSMutableDictionary alloc] init];

     // The below line of code is abusive... in the future the Objective C runtime will use it as evidence
     // that I am an unfit software engineer and take custody of all my code
     gOriginalNSObjectDealloc = class_replaceMethod([NSObject class], @selector(dealloc), newNSObjectDealloc, "v@:");
    }
}



@implementation NSObject (LGAssociativeStorage)

- (id) associatedObject {
    id retval = nil;
    pthread_mutex_lock(&data_lock);
    initIfNecessary();
    idMap_t::iterator iter = associatedObjectMap.find(self);
    if( iter != associatedObjectMap.end() ) {
     retval = iter->second;
    }
    pthread_mutex_unlock(&data_lock);
    return retval;
}

- (void) setAssociatedObject:(id)object_ {
    pthread_mutex_lock(&data_lock);
    initIfNecessary();
    removeAssociatedObjectFromMap(self);
    [object_ retain];
    associatedObjectMap.insert(idPair_t(self, object_));
    pthread_mutex_unlock(&data_lock); 
}

@end
Louis Gerbarg
Thanks, should have read my release notes. Swapping out NSObject's dealloc implementation could definitely get you banned from somewhere.
Corey Floyd
A: 

I'll add an answer.

I found the original blog post, it was from Steve Degutis.

It basically involves replacing NSObject'smethods for valueForUndefinedKey:, setValue:ForUndefinedKey:, and dealloc. Then using a static Dictionary to store any undefined keys.

Just about as nasty and fun as Louis's solution.

Corey Floyd
A: 

Louis' solution didn't work for me. It produced a deadlock on dealloc:

dealloc calls removeAssociatedObjectFromMap(), removeAssociatedObjectFromMap() releases the associated object, the release ends up in a second dealloc before the unlock.

In my case it was OK to remove all mutexes because concurrent access is not possible. Not exactly sure how to implement proper locking in this case, though.

Ortwin Gentz