开源库的使用我们需要注意其所属协议,比如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 <MulticastDelegateBaseObjectDelegate>*)[[GCDMulticastDelegate alloc] init];
...
// 添加代理时
[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 <MulticastDelegateBaseObjectDelegate>*)[[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内部使用(内部合理的使用不会出现上述问题),我们自己将其抽离出来使用时,就需要根据自己的需求合理调用接口,才能规避问题。
总结
综上,我们使用开源代码时,如果只使用其中一部分,要充分理解这部分代码的使用环境,如果需要在一些条件的前提下使用才可以,那么就需要看是否能在使用时保证条件满足。
网友评论