iOS Developer

代理可以一对多吗? ---使用开源库要谨慎

2018-06-21  本文已影响111人  杭研融合通信iOS

开源库的使用我们需要注意其所属协议,比如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的代理对象和线程信息,在指定的线程中使用这个对象调用相应协议的方法。

以上就是这个类的核心思想,其他几个方法这里不做介绍。

注意 !

注意直接使用这个类有两个重要问题:

  1. 强引用
    我们可以看到用于保存代理对象的容器是NSMutableArray类型delegateNodes,delegateNodes将强引用这些对象,如果我们没有在必要的时候将其从delegateNodes中删除,那么将一直引用着对象,导致对象不能释放,增加内存使用量,并存在着内存泄漏的风险。 所以解决这个问题的关键就是我们需要在合适的位置调用removeDelegate方法。

  2. 线程安全
    我们可以看到核心类的方法中,多个方法中存在着对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内部使用(内部合理的使用不会出现上述问题),我们自己将其抽离出来使用时,就需要根据自己的需求合理调用接口,才能规避问题。

总结

综上,我们使用开源代码时,如果只使用其中一部分,要充分理解这部分代码的使用环境,如果需要在一些条件的前提下使用才可以,那么就需要看是否能在使用时保证条件满足。

上一篇下一篇

猜你喜欢

热点阅读