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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#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 类

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

1
2
3
4
5
6
7
8
9
10
11
- (id)initFromDelegateNodes:(NSMutableArray *)inDelegateNodes
{
if ((self = [super init]))
{
delegateNodes = [inDelegateNodes copy];

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

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

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

1
2
3
4
5
6
7
8
9
10
11
@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 的增删

1
2
3
4
5
- (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 的统计相关

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

消息转发实现

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
- (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 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
- (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];
}
}

最后一步,若还是没有:

1
2
3
4
- (void)doesNotRecognizeSelector:(SEL)aSelector
{
// 覆盖实现,防止抛出 NSInvalidArgumentException
}

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

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

总结

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