#import "GCDMulticastDelegate.h" #import #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE #import #endif #if ! __has_feature(objc_arc) #warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). #endif /** * How does this class work? * * In theory, this class is very straight-forward. * It provides a way for multiple delegates to be called, each on its own delegate queue. * * In other words, any delegate method call to this class * will get forwarded (dispatch_async'd) to each added delegate. * * Important note concerning thread-safety: * * This class is designed to be used from within a single dispatch queue. * In other words, it is NOT thread-safe, and should only be used from within the external dedicated dispatch_queue. **/ @interface GCDMulticastDelegateNode : NSObject { @private #if __has_feature(objc_arc_weak) __weak id delegate; #if !TARGET_OS_IPHONE __unsafe_unretained id unsafeDelegate; // Some classes don't support weak references yet (e.g. NSWindowController) #endif #else __unsafe_unretained id delegate; #endif dispatch_queue_t delegateQueue; } - (id)initWithDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue; #if __has_feature(objc_arc_weak) @property (/* atomic */ readwrite, weak) id delegate; #if !TARGET_OS_IPHONE @property (/* atomic */ readwrite, unsafe_unretained) id unsafeDelegate; #endif #else @property (/* atomic */ readwrite, unsafe_unretained) id delegate; #endif @property (nonatomic, readonly) dispatch_queue_t delegateQueue; @end @interface GCDMulticastDelegate () { NSMutableArray *delegateNodes; } - (NSInvocation *)duplicateInvocation:(NSInvocation *)origInvocation; @end @interface GCDMulticastDelegateEnumerator () { NSUInteger numNodes; NSUInteger currentNodeIndex; NSArray *delegateNodes; } - (id)initFromDelegateNodes:(NSMutableArray *)inDelegateNodes; @end //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @implementation GCDMulticastDelegate - (id)init { if ((self = [super init])) { delegateNodes = [[NSMutableArray alloc] init]; } return self; } - (void)addDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue { if (delegate == nil) return; if (delegateQueue == NULL) return; GCDMulticastDelegateNode *node = [[GCDMulticastDelegateNode alloc] initWithDelegate:delegate delegateQueue:delegateQueue]; [delegateNodes addObject:node]; } - (void)removeDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue { if (delegate == nil) return; NSUInteger i; for (i = [delegateNodes count]; i > 0; i--) { GCDMulticastDelegateNode *node = [delegateNodes objectAtIndex:(i-1)]; id nodeDelegate = node.delegate; #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE if (nodeDelegate == [NSNull null]) nodeDelegate = node.unsafeDelegate; #endif if (delegate == nodeDelegate) { if ((delegateQueue == NULL) || (delegateQueue == node.delegateQueue)) { // Recall that this node may be retained by a GCDMulticastDelegateEnumerator. // The enumerator is a thread-safe snapshot of the delegate list at the moment it was created. // To properly remove this node from list, and from the list(s) of any enumerators, // we nullify the delegate via the atomic property. // // However, the delegateQueue is not modified. // The thread-safety is hinged on the atomic delegate property. // The delegateQueue is expected to properly exist until the node is deallocated. node.delegate = nil; #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE node.unsafeDelegate = nil; #endif [delegateNodes removeObjectAtIndex:(i-1)]; } } } } - (void)removeDelegate:(id)delegate { [self removeDelegate:delegate delegateQueue:NULL]; } - (void)removeAllDelegates { for (GCDMulticastDelegateNode *node in delegateNodes) { node.delegate = nil; #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE node.unsafeDelegate = nil; #endif } [delegateNodes removeAllObjects]; } - (NSUInteger)count { return [delegateNodes count]; } - (NSUInteger)countOfClass:(Class)aClass { NSUInteger count = 0; for (GCDMulticastDelegateNode *node in delegateNodes) { id nodeDelegate = node.delegate; #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE if (nodeDelegate == [NSNull null]) nodeDelegate = node.unsafeDelegate; #endif if ([nodeDelegate isKindOfClass:aClass]) { count++; } } return count; } - (NSUInteger)countForSelector:(SEL)aSelector { NSUInteger count = 0; for (GCDMulticastDelegateNode *node in delegateNodes) { id nodeDelegate = node.delegate; #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE if (nodeDelegate == [NSNull null]) nodeDelegate = node.unsafeDelegate; #endif if ([nodeDelegate respondsToSelector:aSelector]) { count++; } } return count; } - (BOOL)hasDelegateThatRespondsToSelector:(SEL)aSelector { for (GCDMulticastDelegateNode *node in delegateNodes) { id nodeDelegate = node.delegate; #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE if (nodeDelegate == [NSNull null]) nodeDelegate = node.unsafeDelegate; #endif if ([nodeDelegate respondsToSelector:aSelector]) { return YES; } } return NO; } - (GCDMulticastDelegateEnumerator *)delegateEnumerator { return [[GCDMulticastDelegateEnumerator alloc] initFromDelegateNodes:delegateNodes]; } - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { for (GCDMulticastDelegateNode *node in delegateNodes) { id nodeDelegate = node.delegate; #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE if (nodeDelegate == [NSNull null]) nodeDelegate = node.unsafeDelegate; #endif NSMethodSignature *result = [nodeDelegate methodSignatureForSelector:aSelector]; if (result != nil) { return result; } } // This causes a crash... // return [super methodSignatureForSelector:aSelector]; // This also causes a crash... // return nil; return [[self class] instanceMethodSignatureForSelector:@selector(doNothing)]; } - (void)forwardInvocation:(NSInvocation *)origInvocation { SEL selector = [origInvocation selector]; BOOL foundNilDelegate = NO; for (GCDMulticastDelegateNode *node in delegateNodes) { id nodeDelegate = node.delegate; #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE if (nodeDelegate == [NSNull null]) nodeDelegate = node.unsafeDelegate; #endif if ([nodeDelegate respondsToSelector:selector]) { // All delegates MUST be invoked ASYNCHRONOUSLY. NSInvocation *dupInvocation = [self duplicateInvocation:origInvocation]; dispatch_async(node.delegateQueue, ^{ @autoreleasepool { [dupInvocation invokeWithTarget:nodeDelegate]; }}); } else if (nodeDelegate == nil) { foundNilDelegate = YES; } } if (foundNilDelegate) { // At lease one weak delegate reference disappeared. // Remove nil delegate nodes from the list. // // This is expected to happen very infrequently. // This is why we handle it separately (as it requires allocating an indexSet). NSMutableIndexSet *indexSet = [[NSMutableIndexSet alloc] init]; NSUInteger i = 0; for (GCDMulticastDelegateNode *node in delegateNodes) { id nodeDelegate = node.delegate; #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE if (nodeDelegate == [NSNull null]) nodeDelegate = node.unsafeDelegate; #endif if (nodeDelegate == nil) { [indexSet addIndex:i]; } i++; } [delegateNodes removeObjectsAtIndexes:indexSet]; } } - (void)doesNotRecognizeSelector:(SEL)aSelector { // Prevent NSInvalidArgumentException } - (void)doNothing {} - (void)dealloc { [self removeAllDelegates]; } - (NSInvocation *)duplicateInvocation:(NSInvocation *)origInvocation { NSMethodSignature *methodSignature = [origInvocation methodSignature]; NSInvocation *dupInvocation = [NSInvocation invocationWithMethodSignature:methodSignature]; [dupInvocation setSelector:[origInvocation selector]]; NSUInteger i, count = [methodSignature numberOfArguments]; for (i = 2; i < count; i++) { const char *type = [methodSignature getArgumentTypeAtIndex:i]; if (*type == *@encode(BOOL)) { BOOL value; [origInvocation getArgument:&value atIndex:i]; [dupInvocation setArgument:&value atIndex:i]; } else if (*type == *@encode(char) || *type == *@encode(unsigned char)) { char value; [origInvocation getArgument:&value atIndex:i]; [dupInvocation setArgument:&value atIndex:i]; } else if (*type == *@encode(short) || *type == *@encode(unsigned short)) { short value; [origInvocation getArgument:&value atIndex:i]; [dupInvocation setArgument:&value atIndex:i]; } else if (*type == *@encode(int) || *type == *@encode(unsigned int)) { int value; [origInvocation getArgument:&value atIndex:i]; [dupInvocation setArgument:&value atIndex:i]; } else if (*type == *@encode(long) || *type == *@encode(unsigned long)) { long value; [origInvocation getArgument:&value atIndex:i]; [dupInvocation setArgument:&value atIndex:i]; } else if (*type == *@encode(long long) || *type == *@encode(unsigned long long)) { long long value; [origInvocation getArgument:&value atIndex:i]; [dupInvocation setArgument:&value atIndex:i]; } else if (*type == *@encode(double)) { double value; [origInvocation getArgument:&value atIndex:i]; [dupInvocation setArgument:&value atIndex:i]; } else if (*type == *@encode(float)) { float value; [origInvocation getArgument:&value atIndex:i]; [dupInvocation setArgument:&value atIndex:i]; } else if (*type == '@') { void *value; [origInvocation getArgument:&value atIndex:i]; [dupInvocation setArgument:&value atIndex:i]; } else if (*type == '^') { void *block; [origInvocation getArgument:&block atIndex:i]; [dupInvocation setArgument:&block atIndex:i]; } else { NSString *selectorStr = NSStringFromSelector([origInvocation selector]); NSString *format = @"Argument %lu to method %@ - Type(%c) not supported"; NSString *reason = [NSString stringWithFormat:format, (unsigned long)(i - 2), selectorStr, *type]; [[NSException exceptionWithName:NSInvalidArgumentException reason:reason userInfo:nil] raise]; } } [dupInvocation retainArguments]; return dupInvocation; } @end //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @implementation GCDMulticastDelegateNode @synthesize delegate; // atomic #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE @synthesize unsafeDelegate; // atomic #endif @synthesize delegateQueue; // non-atomic #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE static BOOL SupportsWeakReferences(id delegate) { // From Apple's documentation: // // > Which classes don’t support weak references? // > // > You cannot currently create weak references to instances of the following classes: // > // > NSATSTypesetter, NSColorSpace, NSFont, NSFontManager, NSFontPanel, NSImage, NSMenuView, // > NSParagraphStyle, NSSimpleHorizontalTypesetter, NSTableCellView, NSTextView, NSViewController, // > NSWindow, and NSWindowController. // > // > In addition, in OS X no classes in the AV Foundation framework support weak references. // // NSMenuView is deprecated (and not available to 64-bit applications). // NSSimpleHorizontalTypesetter is an internal class. if ([delegate isKindOfClass:[NSATSTypesetter class]]) return NO; if ([delegate isKindOfClass:[NSColorSpace class]]) return NO; if ([delegate isKindOfClass:[NSFont class]]) return NO; if ([delegate isKindOfClass:[NSFontManager class]]) return NO; if ([delegate isKindOfClass:[NSFontPanel class]]) return NO; if ([delegate isKindOfClass:[NSImage class]]) return NO; if ([delegate isKindOfClass:[NSParagraphStyle class]]) return NO; if ([delegate isKindOfClass:[NSTableCellView class]]) return NO; if ([delegate isKindOfClass:[NSTextView class]]) return NO; if ([delegate isKindOfClass:[NSViewController class]]) return NO; if ([delegate isKindOfClass:[NSWindow class]]) return NO; if ([delegate isKindOfClass:[NSWindowController class]]) return NO; return YES; } #endif - (id)initWithDelegate:(id)inDelegate delegateQueue:(dispatch_queue_t)inDelegateQueue { if ((self = [super init])) { #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE { if (SupportsWeakReferences(inDelegate)) { delegate = inDelegate; delegateQueue = inDelegateQueue; } else { delegate = [NSNull null]; unsafeDelegate = inDelegate; delegateQueue = inDelegateQueue; } } #else { delegate = inDelegate; delegateQueue = inDelegateQueue; } #endif #if !OS_OBJECT_USE_OBJC if (delegateQueue) dispatch_retain(delegateQueue); #endif } return self; } - (void)dealloc { #if !OS_OBJECT_USE_OBJC if (delegateQueue) dispatch_release(delegateQueue); #endif } @end //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @implementation GCDMulticastDelegateEnumerator - (id)initFromDelegateNodes:(NSMutableArray *)inDelegateNodes { if ((self = [super init])) { delegateNodes = [inDelegateNodes copy]; numNodes = [delegateNodes count]; currentNodeIndex = 0; } return self; } - (NSUInteger)count { return numNodes; } - (NSUInteger)countOfClass:(Class)aClass { NSUInteger count = 0; for (GCDMulticastDelegateNode *node in delegateNodes) { id nodeDelegate = node.delegate; #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE if (nodeDelegate == [NSNull null]) nodeDelegate = node.unsafeDelegate; #endif if ([nodeDelegate isKindOfClass:aClass]) { count++; } } return count; } - (NSUInteger)countForSelector:(SEL)aSelector { NSUInteger count = 0; for (GCDMulticastDelegateNode *node in delegateNodes) { id nodeDelegate = node.delegate; #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE if (nodeDelegate == [NSNull null]) nodeDelegate = node.unsafeDelegate; #endif if ([nodeDelegate respondsToSelector:aSelector]) { count++; } } return count; } - (BOOL)getNextDelegate:(id *)delPtr delegateQueue:(dispatch_queue_t *)dqPtr { while (currentNodeIndex < numNodes) { GCDMulticastDelegateNode *node = [delegateNodes objectAtIndex:currentNodeIndex]; currentNodeIndex++; id nodeDelegate = node.delegate; // snapshot atomic property #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE if (nodeDelegate == [NSNull null]) nodeDelegate = node.unsafeDelegate; #endif if (nodeDelegate) { if (delPtr) *delPtr = nodeDelegate; if (dqPtr) *dqPtr = node.delegateQueue; return YES; } } return NO; } - (BOOL)getNextDelegate:(id *)delPtr delegateQueue:(dispatch_queue_t *)dqPtr ofClass:(Class)aClass { while (currentNodeIndex < numNodes) { GCDMulticastDelegateNode *node = [delegateNodes objectAtIndex:currentNodeIndex]; currentNodeIndex++; id nodeDelegate = node.delegate; // snapshot atomic property #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE if (nodeDelegate == [NSNull null]) nodeDelegate = node.unsafeDelegate; #endif if ([nodeDelegate isKindOfClass:aClass]) { if (delPtr) *delPtr = nodeDelegate; if (dqPtr) *dqPtr = node.delegateQueue; return YES; } } return NO; } - (BOOL)getNextDelegate:(id *)delPtr delegateQueue:(dispatch_queue_t *)dqPtr forSelector:(SEL)aSelector { while (currentNodeIndex < numNodes) { GCDMulticastDelegateNode *node = [delegateNodes objectAtIndex:currentNodeIndex]; currentNodeIndex++; id nodeDelegate = node.delegate; // snapshot atomic property #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE if (nodeDelegate == [NSNull null]) nodeDelegate = node.unsafeDelegate; #endif if ([nodeDelegate respondsToSelector:aSelector]) { if (delPtr) *delPtr = nodeDelegate; if (dqPtr) *dqPtr = node.delegateQueue; return YES; } } return NO; } @end