美文网首页iOS Developer
代理可以一对多吗? ---使用开源库要谨慎

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

作者: 杭研融合通信iOS | 来源:发表于2018-06-21 11:17 被阅读111次

    开源库的使用我们需要注意其所属协议,比如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内部使用(内部合理的使用不会出现上述问题),我们自己将其抽离出来使用时,就需要根据自己的需求合理调用接口,才能规避问题。

    总结

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

    相关文章

      网友评论

        本文标题:代理可以一对多吗? ---使用开源库要谨慎

        本文链接:https://www.haomeiwen.com/subject/pnfqyftx.html