Multicast Delegate 实现分析

Multicast Delegate?

传统 Delegate 大家都知道是一对一关系,一个对象只能给对应 Delegate 对象做回调,而 Multicast Delegate 顾名思义就是能向多个 Delegate 对象进行回调。

Multicast Delegate 与 KVO

对于某些属性值我们可以采用 KVO 的方法进行监听获得最新值(比如 contentOffsetcontentSize 等)。但某些属性值却无法进行 KVO (比如 isDraggingisTracking 等),原因很简单,可以参考下 KVO 的实现原理,对 readonly 的属性,即不带有 setter 的属性进行 KVO 是无效的。因此 Multicast Delegate 可以很好地解决这种情况。

NSNotification?

溜了

Multicast 原理

为了实现同时给多个对象进行回调,也就是执行对应方法,不难想到 Runtime 中消息发送的流程之一:

消息转发

message-flow

那么 Multicast Delegate 的核心就是 methodSignatureForSelector: -> forwardInvocation: -> doesNotRecognizeSelector: 的重新实现。

NSMethodSignature

记录了返回值和方法参数的类型信息。

NSInvocation

记录了 target、selector、parameters 等消息发送所必需的元素。

直接调用某个对象的方法有两种:

  • performSelector:withObject
  • NSInvocation

其中第一种方法能实现简单的调用,但无法处理多参数或者返回值。那么使用 NSInvocation 可以解决相对复杂的操作。

GCDMulticastDelegate

GCDMulticastDelegate 为 XMPP 框架中的一个工具类,同时也是 Multicast Delegate 的一个典范。

其中里面包含着这几样东西:

  • GCDMulticastDelegateNode
  • GCDMulticastDelegateEnumerator
  • GCDMulticastDelegate

GCDMulticastDelegateNode 类

GCDMulticastDelegateNode 主要为我们提供一个将原 Delegate 对象包装成 DelegateNode 的初始化方法,同时可以绑定指定的 dispatch queue

#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;

  static BOOL SupportsWeakReferences(id delegate)
  {
    if ([delegate isKindOfClass:[NSATSTypesetter class]])    return NO;
    ......
    return YES;
  }

其中比较有趣的是,源码中有两种源 Delegate,一个是 __weak delegate,另一个是 __unsafe_unretained unsafeDelegate,原因在注释中也说得很清楚,某些类不支持 weak 的引用方式,比如:NSWindowController。同时往下看会有 SupportsWeakReferences 用于判断所有不支持 weak 的类。

同时注意如果传入了 delegateQueuedispatch_retaindispatch_release 是成对出现的。

GCDMulticastDelegateEnumerator 类

相当简单的一个类,首先看初始化方法:

- (id)initFromDelegateNodes:(NSMutableArray *)inDelegateNodes
{
    if ((self = [super init]))
    {
        delegateNodes = [inDelegateNodes copy];

        numNodes = [delegateNodes count];
        currentNodeIndex = 0;
    }
    return self;
}

传入 DelegateNodes 数组并复制一份,同时初始化相关参数。

再根据 @interface 中声明的方法:

@interface GCDMulticastDelegateEnumerator : NSObject

@property (nonatomic, readonly) NSUInteger count;
- (NSUInteger)countOfClass:(Class)aClass;
- (NSUInteger)countForSelector:(SEL)aSelector;

- (BOOL)getNextDelegate:(id _Nullable * _Nonnull)delPtr delegateQueue:(dispatch_queue_t _Nullable * _Nonnull)dqPtr;
- (BOOL)getNextDelegate:(id _Nullable * _Nonnull)delPtr delegateQueue:(dispatch_queue_t _Nullable * _Nonnull)dqPtr ofClass:(Class)aClass;
- (BOOL)getNextDelegate:(id _Nullable * _Nonnull)delPtr delegateQueue:(dispatch_queue_t _Nullable * _Nonnull)dqPtr forSelector:(SEL)aSelector;

@end

可以看出提供的是遍历相关的便利方法。

GCDMulticastDelegate 类

Multicast Delegate 的核心实现,持有一个 DelegateNodes 数组,负责储存 Delegate 对象,内部通过 Runtime 特性实现多 Delegate 的消息转发功能。

Delegate 的增删

- (void)addDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue;
- (void)removeDelegate:(id)delegate delegateQueue:(nullable dispatch_queue_t)delegateQueue;
- (void)removeDelegate:(id)delegate;

- (void)removeAllDelegates;

Delegate 的统计相关

@property (nonatomic, readonly) NSUInteger count;
- (NSUInteger)countOfClass:(Class)aClass;
- (NSUInteger)countForSelector:(SEL)aSelector;

消息转发实现

回到消息转发中 Normal Forwarding 的第一步,创建 method Signature :

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    // 首先从存放 Delegate Node 的数组中开始遍历
    for (GCDMulticastDelegateNode *node in delegateNodes)
    {
        id nodeDelegate = node.delegate; // 获得 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; // 不为空就直接返回
        }
    }

    // 若最终方法签名为 `nil`,按照正常情况则必定会闪退

    // 这个方法回造成崩溃,因为结果同样为 `nil`
    // return [super methodSignatureForSelector:aSelector];

    // 这个方法也会造成崩溃
    // return nil;

    // 最终是通过一个空实现的函数(doNothing)来创建函数签名
    // 其实这里还可以写成 return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    return [[self class] instanceMethodSignatureForSelector:@selector(doNothing)];
}

- (void)doNothing {} // 用于创建函数签名的最后保障

然后第二步,转发 Invocation :

- (void)forwardInvocation:(NSInvocation *)origInvocation
{
    SEL selector = [origInvocation selector]; // 需要调用的方法的 SEL
    BOOL foundNilDelegate = NO; // 标记空 Delegate 对象

    for (GCDMulticastDelegateNode *node in delegateNodes)
    {
        // 同样的,先遍历获得 Node 中的 delegate 对象
        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])
        {
            // 所有的 Delegate 必须异步调用

            NSInvocation *dupInvocation = [self duplicateInvocation:origInvocation];

            dispatch_async(node.delegateQueue, ^{ @autoreleasepool {

                [dupInvocation invokeWithTarget:nodeDelegate];

            }});
        }
        else if (nodeDelegate == nil)
        {
            foundNilDelegate = YES;
        }
    }

    // 若 Delegate 已被释放,为 nil,那么就要从数组中移除
    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
{
    // 覆盖实现,防止抛出 NSInvalidArgumentException
}

一个有趣的方法:- (NSInvocation *)duplicateInvocation:(NSInvocation *)origInvocation;,用于复制 invocation 对象。

为什么要这么做呢。因为每个 Delegate 调用的时候都会去设置 invocation 的 target,同时是异步触发,那么就有可能造成 target 被意外替换掉,会造成 crash,所以复制一个 invocation 还是很有必要的。

总结

GCDMulticastDelegate 的代码量不多,并且可以直接从 GCDMulticastDelegate 直接拿出来使用。其实现考虑的情况相当完善,包括有是否支持 weak 引用的判断、delegate 对应的 dispatch queue 等。