Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

class tracking and limiting instances with an NSSet

I'd like my class to detect that a new instance is equivalent (vis a vis isEqual: and hash) to some existing instance, and create only unique instances. Here's code that I think does the job, but I'm concerned it's doing something dumb that I can't spot...

Say it's an NSURLRequest subclass like this:

// MyClass.h
@interface MyClass : NSMutableURLRequest
@end

// MyClass.m

@implementation MyClass

+ (NSMutableSet *)instances {

    static NSMutableSet *_instances;
    static dispatch_once_t once;

    dispatch_once(&once, ^{ _instances = [[NSMutableSet alloc] init];});
    return _instances;
}

- (id)initWithURL:(NSURL *)URL {

    self = [super initWithURL:URL];
    if (self) {
        if ([self.class.instances containsObject:self])
            self = [self.class.instances member:self];
        else
            [self.class.instances addObject:self];
    }
    return self;
}


// Caller.m
NSURL *urlA = [NSURL urlWithString:@"http://www.yahoo.com"];

MyClass *instance0 = [[MyClass alloc] initWithURL: urlA];
MyClass *instance1 = [[MyClass alloc] initWithURL: urlA];  // 2

BOOL works = instance0 == instance1;  // works => YES, but at what hidden cost?

Questions:

  1. That second assignment to self in init looks weird, but not insane. Or is it?
  2. Is it just wishful coding to think that second alloc (of instance1) gets magically cleaned up?
like image 639
danh Avatar asked Apr 05 '13 17:04

danh


2 Answers

  1. It's not insane, but in manual retain/release mode, you do need to release self beforehand or you'll leak an uninitialized object every time this method is run. In ARC, the original instance will automatically be released for you.

  2. See #1.

BTW, for any readers who usually stop at one answer, bbum's answer below includes a full working example of a thread-safe implementation. Highly recommended for anyone making a class that does this.

like image 66
Chuck Avatar answered Nov 15 '22 09:11

Chuck


Thought of a better way (original answer below the line) assuming you really want to unique by URL. If not, this also demonstrates the synchronization primitive use.

@interface UniqueByURLInstances:NSObject
@property(strong) NSURL *url;
@end

@implementation UniqueByURLInstances
static NSMutableDictionary *InstanceCache()
{
    static NSMutableDictionary *cache;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        cache = [NSMutableDictionary new];
    });
    return cache;
}

static dispatch_queue_t InstanceSerializationQueue()
{
    static dispatch_queue_t queue;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        queue = dispatch_queue_create("UniqueByURLInstances queue", DISPATCH_QUEUE_SERIAL);
    });
    return queue;
}

+ (instancetype)instanceWithURL:(NSURL*)URL
{
    __block UniqueByURLInstances *returnValue = nil;
    dispatch_sync(InstanceSerializationQueue(), ^{
        returnValue = [InstanceCache() objectForKey:URL];
        if (!returnValue)
        {
            returnValue = [[self alloc] initWithURL:URL];
        }
    });
    return returnValue;
}

- (id)initWithURL:(NSURL *)URL
{
    __block UniqueByURLInstances* returnValue = self;
    dispatch_sync(InstanceSerializationQueue(), ^{
        returnValue = [InstanceCache() objectForKey:URL];
        if (returnValue) return;

        returnValue = [super initWithURL:URL];
        if (returnValue) {
            [InstanceCache() setObject:returnValue forKey:URL];
        }

        _url = URL;
    });

    return returnValue;
}

- (void)dealloc {
    dispatch_sync(InstanceSerializationQueue(), ^{
        [InstanceCache() removeObjectForKey:_url];
    });
    // rest o' dealloc dance here 
}
@end

Caveat: Above was typed into SO -- never been run. I may have screwed something up. It assumes ARC is enabled. Yes, it'll end up looking up URL twice when using the factory method, but that extra lookup should be lost in the noise of allocation and initialization. Doing that means that the developer could use either the factory or the initializer and still see unique'd instances but there will be no allocation on execution of the factory method when the instance for that URL already exists.

(If you can't unique by URL, then go back to your NSMutableSet and skip the factory method entirely.)


What Chuck said, but some additional notes:

Restructure your code like this:

+(NSMutableSet*)instances
{
    static NSMutableSet *_instances;
    dispatch_once( ...., ^{ _instances = [[NSMutableSet alloc] init];});
    return instances;
}

Then call that method whenever you want access to instances. It localizes all the code in one spot and isolates it from +initialize (which isn't really a big deal).

If your class may be instantiated from multiple threads, you'll want to surround the check-allocate-or-return with a synchronization primitive. I would suggest a dispatch_queue.

like image 41
bbum Avatar answered Nov 15 '22 09:11

bbum