开源库的使用我们需要注意其所属协议,比如MIT、BSD等,注意这些协议不允许你做些什么。但这个不是本文重点。
本文结合一个多重代理的库的解析和使用,来讲一下使用开源库中使用部分代码时的问题。
我们都知道“协议” protocol可以用于对象之间的通信,并用于代码的解耦,通常被我们用在相关性较强的对象之间。但是直接使用protocol delegate有一个缺陷,因为它不支持一对多(多个对象同时作为一个对象A的代理, 当A处理一个事件时,这些对象都能接收到相关信息), 因为当我们设置了一个对象的.delegate时, 再去设置下一个对象.delegate, 就会把之前的赋值覆盖掉, 也就是说同一时刻只会有一个代理在生效。这种情况下我们就只能使用通知 notification 吗?
当然不是!
引入
我们可以使用多方代理, 并且有车轮子! 即时通信开源库xmpp中就有一个类:GCDMulticastDelegate 可以实现多重代理。我们来分析下核心代码(下面代码有删减,便于理解):
- (id)init { if ((self = [super init])) { delegateNodes = [[NSMutableArray alloc] init]; } return self; }
首先GCDMulticastDelegate的初始化,先创建一个数组,后面讲用于存放多重代理的对象信息,下面就是这个数组的add 和 remove方法:
- (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 (delegate == nodeDelegate) { if ((delegateQueue == NULL) || (delegateQueue == node.delegateQueue)) { node.delegate = nil; [delegateNodes removeObjectAtIndex:(i-1)]; } } } }
可以看出核心类GCDMulticastDelegate中,传入参数包含要添加代理的对象和代理回调时的线程,然后通过一个GCDMulticastDelegateNode的model来包含这两个信息,然后将这个node对象存到数组delegateNodes中。也就是说delegateNodes中存放的每个对象都包含两个信息:要作为代理的对象和相关代理的方法调用时想要处于的线程。
@interface GCDMulticastDelegateNode : NSObject { - (id)initWithDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue; @property (/* atomic */ readwrite, unsafe_unretained) id delegate; @property (nonatomic, readonly) dispatch_queue_t delegateQueue; @end
基于上述信息我们可以基本知道如何使用:
...
// 初始化时
multicastDelegate = (GCDMulticastDelegate
...
// 添加代理时
[multicastDelegate addDelegate:delegate delegateQueue:delegateQueue];
...
// 代理方法XXXX_Selector回调时
[multicastDelegate XXXX_Selector];
问题来了:当我们使用[multicastDelegate XXXX_Selector]; 时,源码内部如何帮我处理的呢?
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { for (GCDMulticastDelegateNode *node in delegateNodes) { id nodeDelegate = node.delegate; NSMethodSignature *result = [nodeDelegate methodSignatureForSelector:aSelector]; if (result != nil) { return result; } } 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 ([nodeDelegate respondsToSelector:selector]) { NSInvocation *dupInvocation = [self duplicateInvocation:origInvocation]; dispatch_async(node.delegateQueue, ^{ @autoreleasepool { [dupInvocation invokeWithTarget:nodeDelegate]; }}); } else if (nodeDelegate == nil) { foundNilDelegate = YES; } } if (foundNilDelegate) { NSMutableIndexSet *indexSet = [[NSMutableIndexSet alloc] init]; NSUInteger i = 0; for (GCDMulticastDelegateNode *node in delegateNodes) { id nodeDelegate = node.delegate; if (nodeDelegate == nil) { [indexSet addIndex:i]; } i++; } [delegateNodes removeObjectsAtIndexes:indexSet]; } } - (void)doesNotRecognizeSelector:(SEL)aSelector { // Prevent NSInvalidArgumentException }
可以看到其中利用了消息转发的原理,重写了methodSignatureForSelector:和forwardInvocation:两个方法。
methodSignatureForSelector:的作用在于为另一个类实现的消息创建一个有效的方法签名,必须实现,并且返回不为空的methodSignature,否则会crash。methodSignatureForSelector在找到相应方法的签名时,如果找到了就直接返回,如果找不到就返回 donothing的签名,这个donothing肯定是没有实现的,所以此时Nsobject就会调用doesNotRecognizeSelector,这样就可以避免crash。
我们知道OC中的方法调用都通过消息发送,消息经过转发时,都要调用forwardInvocation:,所以在forwardInvocation中遍历delegateNodes的每个node对象,根据node的代理对象和线程信息,在指定的线程中使用这个对象调用相应协议的方法。
以上就是这个类的核心思想,其他几个方法这里不做介绍。
注意 !
注意直接使用这个类有两个重要问题:
强引用
我们可以看到用于保存代理对象的容器是NSMutableArray类型delegateNodes,delegateNodes将强引用这些对象,如果我们没有在必要的时候将其从delegateNodes中删除,那么将一直引用着对象,导致对象不能释放,增加内存使用量,并存在着内存泄漏的风险。 所以解决这个问题的关键就是我们需要在合适的位置调用removeDelegate方法。
线程安全
我们可以看到核心类的方法中,多个方法中存在着对NSMutableArray类型delegateNodes的遍历、增加、删除,那么当处于多线程的环境中,这些方法如果恰好在不同线程中调用,例如一个线程正在调用removeDelegate , 另一个线程正在遍历delegateNodes,就会产生# <__nsarraym: 0xb550c30> was mutated while being enumerated. 这种类型的crash。
我们有两种方式可以解决:
第一种方法,可以仿照xmpp中调用GCDMulticastDelegate的形式,将每个接口的调用都协调到一个固定的线程中,将这些接口再封装一层接口,使我们调用接口时已经做好线程安全,实例代码如下:
@implementation MulticastDelegateObject - (id)init { return [self initWithDispatchQueue:NULL]; } - (id)initWithDispatchQueue:(dispatch_queue_t)queue { if ((self = [super init])) { if (queue) { moduleQueue = queue; } else { const char *moduleQueueName = [NSStringFromClass([self class]) UTF8String]; moduleQueue = dispatch_queue_create(moduleQueueName, NULL); } moduleQueueTag = &moduleQueueTag; dispatch_queue_set_specific(moduleQueue, moduleQueueTag, moduleQueueTag, NULL); multicastDelegate = (GCDMulticastDelegate*)[[GCDMulticastDelegate alloc] init]; } return self; } - (dispatch_queue_t)moduleQueue { return moduleQueue; } - (void *)moduleQueueTag { return moduleQueueTag; } - (void)addDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue { dispatch_block_t block = ^{ [multicastDelegate addDelegate:delegate delegateQueue:delegateQueue]; }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } - (void)removeDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue synchronously:(BOOL)synchronously { dispatch_block_t block = ^{ [multicastDelegate removeDelegate:delegate delegateQueue:delegateQueue]; }; if (dispatch_get_specific(moduleQueueTag)) block(); else if (synchronously) dispatch_sync(moduleQueue, block); else dispatch_async(moduleQueue, block); } @end
第二个方法,使用线程保护的方式对GCDMulticastDelegate中每个方法中对delegateNodes有操作的代码段,都进行保护起来,我们这里以信号量的形式来举例说明:
- (id)init { if ((self = [super init])) { delegateNodes = [[NSMutableArray alloc] init]; signal = dispatch_semaphore_create(1); overTime = dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC); } return self; } - (void)addDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue { if (delegate == nil) return; if (delegateQueue == NULL) delegateQueue = dispatch_get_main_queue(); GCDMulticastDelegateNode *node = [[GCDMulticastDelegateNode alloc] initWithDelegate:delegate delegateQueue:delegateQueue]; dispatch_semaphore_wait(signal, overTime); [delegateNodes addObject:node]; dispatch_semaphore_signal(signal); } - (void)removeDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue { if (delegate == nil) return; dispatch_semaphore_wait(signal, overTime); NSUInteger i; for (i = [delegateNodes count]; i > 0; i--) { GCDMulticastDelegateNode *node = [delegateNodes objectAtIndex:(i-1)]; id nodeDelegate = node.delegate; if (delegate == nodeDelegate) { if ((delegateQueue == NULL) || (delegateQueue == node.delegateQueue)) { node.delegate = nil; [delegateNodes removeObjectAtIndex:(i-1)]; } } } dispatch_semaphore_signal(signal); }
我们首先在实例化方法中,初始化了信号量和超时时间,然后在每个对delegateNodes有操作的方法相关代码段 的前后,使用dispatch_semaphore_wait 和 dispatch_semaphore_signal这对基友来保证所有处理都要先获得信号量,这样就保证对delegateNodes的操作都串行进行,即保证了线程安全。
p.s.
当然这两个问题并非这个源码本身的问题,因为这个类本身并非一个独立的库开源,而是作为xmpp内部使用(内部合理的使用不会出现上述问题),我们自己将其抽离出来使用时,就需要根据自己的需求合理调用接口,才能规避问题。
总结
综上,我们使用开源代码时,如果只使用其中一部分,要充分理解这部分代码的使用环境,如果需要在一些条件的前提下使用才可以,那么就需要看是否能在使用时保证条件满足。
作者:杭研融合通信iOS
链接:https://www.jianshu.com/p/bd92d054edf4