iOS:利用消息转发机制实现多播委托

1,932 阅读4分钟

iOS中消息回调Apple提供了如下几种方法:

  1. delegate

    delegate属于一对一的回调。这种方式在实际的开发中应用的最多。但是缺点是无法实现一对多的回调。

  2. NSNotification

    NSNotification属于全局广播。但无法指定回调方法,而且在实际的开发中应该尽量少用通知,因为这种方式很难管理。

  3. KVO

    KVO属于一对多的回调。但是仅仅适用于监听属性变更方面。

  4. Block

    Block 也算是一种回调方式,但是如果使用不当可能会引起循环引用问题。并且跟delegate一样,同样不具备一对多的功能,优点在于用起来方便。

在实际的开发过程中,我们可能需要即需要类似delegate那样的回调方式,又想要类似KVO那样的一对多的功能。这种需求在IM类应用中很普遍,甚至可以说这样的回调方式是IM类应用的核心。

这里介绍一种使用OC的消息转发机制来实现多播委托功能的方法。这里先直接贴出实现代码再一一解释。

@interface MulticastDelegate : NSObject
-(void)regisetDelegate:(id)delegate;
@end

@implementation MulticastDelegate{
    // delegate数组
    NSMutableArray *delegates;
}
-(id)init{
    self = [super init];
    delegates = [NSMutableArray array];
    return self;
}

// 注册delegate
-(void)regisetDelegate:(id)delegate{
    // 其实就是把delegate存入数组
    [delegates addObject:delegate];
}

// 方法签名
-(NSMethodSignature *)methodSignatureForSelector:(SEL)selector{
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

// 利用消息转发机制对delegate数组进行回调
-(void)forwardInvocation:(NSInvocation *)invocation{
    SEL sel = invocation.selector;
    [delegates enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if([obj respondsToSelector:sel]){
            [invocation invokeWithTarget:obj];
        }
    }];
}
@end

这里定义了一个叫做MulticastDelegate的类,这个类专门用于管理多播委托的,并且对外只提供了一个regisetDelegate:方法。而这个方法也很简单,就是将delegate加入数组中。

上面的代码中核心代码就是消息转发那块代码。也就是methodSignatureForSelectorforwardInvocation两个方法。当我们对一个对象调用了不存在的方法的时候就会触发消息转发机制,当消息转发进入到forwardInvocation的时候说明已经进入到最后一步,并且系统已经把方法的效用信息全部封装进了NSInvocation中了。这时候只需要将delegate数组中的delegate遍历执行即可。

整体的实现代码可以说很简单。下面是使用方式代码,既然叫做MulticastDelegate,那么用的时候肯定是要先定义个protocol了。那么第一步就是定义protocol

@protocol TestDelegate
-(void)print;
@end

然后就是注册回调

id mutiDelegate; // 注意是id类型。
mutiDelegate =[[MulticastDelegate alloc] init];
[mutiDelegate regisetDelegate:self];

最后就是调用。这一步的调用跟原来的一样,直接对mutiDelegate调用方法即可。

[mutiDelegate print];

在上面的MulticastDelegate的初始化过程中你应该注意到了,变量mutiDelegateid类型,而不是MulticastDelegate。之所以这样做,是因为只有id类型的变量才能调用任意方法而Xcode不会警告,否则XCode都不能编译。

其实看了上面的代码,你会发现一个,我们平常设置delegate的时候都是weak属性,但是上面的代码中存入数组中的delegate是strong的,这不是形成循环引用了吗?

另外,在实际的开发过程中,甚至对回调线程也有要求,比如在注册回调的时候指定回调队列。这样就需要修改回调数组的存放的内容了。

// 定义一个存放delete 和 dispatch_queue_t 的class
@interface MulticastDelegateNode : NSObject
@property (nonatomic,weak,readonly)id delegate;
@property (nonatomic,readonly)dispatch_queue_t queue;
@end

@implementation MulticastDelegateNode
-(id)initWithDelegate:(id)delegate delegateQueue:(dispatch_queue_t)queue{
    self = [super init];
    _delegate = delegate;
    _queue = queue;
    return self;
}
@end


@implementation MulticastDelegate{
    // delegate数组
    NSMutableArray<MulticastDelegateNode *> *delegates;
}
-(id)init{
    self = [super init];
    delegates = [NSMutableArray array];
    return self;
}

// 注册delegate
-(void)regisetDelegate:(id)delegate delegateQueue:(dispatch_queue_t)queue{
    // 其实就是把delegate存入数组
    MulticastDelegateNode *node = [[MulticastDelegateNode alloc] initWithDelegate:delegate delegateQueue:queue?:dispatch_get_main_queue()];
    [delegates addObject:node];
}

// 方法签名
-(NSMethodSignature *)methodSignatureForSelector:(SEL)selector{
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

// 利用消息转发机制对delegate数组进行回调
-(void)forwardInvocation:(NSInvocation *)invocation{
    SEL sel = invocation.selector;
    [delegates enumerateObjectsUsingBlock:^(MulticastDelegateNode * _Nonnull node, NSUInteger idx, BOOL * _Nonnull stop) {
        if([node.delegate respondsToSelector:sel]){
            dispatch_async(node.queue, ^{
                [invocation invokeWithTarget:node.delegate];
            });
        }
    }];
}
@end

上面的代码中额外定义了一个MulticastDelegateNode的class,主要是以weak的方式保存delegate,并且保存dispatch_queue_t。这样既解决了循环引用的问题,又能在指定的队列上执行回调。