多代理

多代理与代理/通知比较

代理模式在Cocoa编程中使用非常普遍。代理模式通常是一对一的关系,一个事件只能通过代理方法通知给一个另外的对象。

想象一下A用户跟B用户聊天应用的场景:A接收到B用户的新消息,首先要将消息显示在聊天窗口中,其次要更新联系人列表将最新消息显示在对应的用户的列表中。

ChatWindow

假设联系人列表的Controller是BuddyListController,聊天窗口的Controller是ChatViewController。
消息接收由MessageManager统一管理。如果说收到聊天消息是一个事件的话,对于每个聊天消息,都需要MessageManager将这个消息事件同时通知到2个Controller(BuddyListController和ChatViewController),由各自Controller完成页面的更新。

1.代理

如果使用代理来实现消息通知,首先需要定义代理协议接口

@protocol MessageDelegate
@optional
-(void)recieveMessage:(Message *)message;
@end

MessageManager中定义2个代理属性delegate1,delegate2,本别对应BuddyListController,ChatViewController。

@interface MessageManager : NSObject
@property(nonatomic,weak) id<MessageDelegate> delegate1;
@property(nonatomic,weak) id<MessageDelegate> delegate2;
@end

这样当MessageManager接受到消息时,可以通过如下方式通知到不同的Controller

if ([self.delegate1 respondsToSelector:@selector(recieveMessage:)]){
       ....
}
if ([self.delegate2 respondsToSelector:@selector(recieveMessage:)]){
        ....
}

2.通知

通知支持实现一对多的事件处理。

定义消息名称kChatMessage,BuddyListController 和 ChatViewController 都可以注册为聊天消息kChatMessage事件的接收者。当有消息到达了,MessageManager发送kChatMessage消息事件,BuddyListController 和 ChatViewController 接收处理即可。

[[NSNotificationCenter defaultCenter] addObserverForName:kChatMessage  object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification* note){
   Message *message = note.object ;
}];

3.多代理

代理只适合一对一关系的处理,要处理一对多的场景需要添加多个代理属性,代码结构繁琐不够清晰。

通知支持一对多关心的处理,每次处理都需要从消息通知类NSNotification中拆解数据,不支持调用需要有返回值的场景。通知是全局的,任何类都可以注册接收消息。

多代理融合代理和通知的优点,支持

1)一对多处理,
2)执行代理方法同时可以获取返回值
3)可以控制事件的接收范围

多代理实现

实现技术

既然是多代理,首先考虑的用数组delegateNodes将多个代理存储起来,然后当事件发生的时候,遍历数组对各个代理对象进行方法回调通知。

基本的伪代码如下:

- (void)recieveMessage:(Message *)message{
     for(id delegate in self.delegateNodes){
           if ([delegate respondsToSelector:@selector(recieveMessage:sender:)]){
                [delegate recieveMessage:message sender:self];
           }
     }
}

如果代理协议定义了多个方法,则对每个代理方法都要写上述类似的代码。利用oc中runtime提供的消息转发机制可以实现在对象A上执行对象B的方法,因此我们可以考虑使用消息转发机制来简化代码实现。

使用消息转发机制优化

使用NSMethodSignature和NSInvocation实现消息转发。

实现消息转发,必须实现两个方法: methodSignatureForSelector 和 forwardInvocation。

methodSignatureForSelector的作用在于为另一个类实现的消息创建一个有效的方法签名;forwardInvocation:将方法调用转发给一个真正实现了该消息的对象。

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    for (id delegate in self.delegateNodes){        NSMethodSignature *result = [delegate methodSignatureForSelector:aSelector];
        if (result != nil){
            return result;
        }
    }
}
    
- (void)forwardInvocation:(NSInvocation *)origInvocation{
    SEL selector = [origInvocation selector];
    for (id delegate in self.delegateNodes){    
        if ([delegate respondsToSelector:selector]){
             [origInvocation invokeWithTarget:delegate];
        }
    }
}

这样我们就可以方便的在多代理的管理类上面简单的执行代理方法,而不需要循环遍历对每个代理执行一遍。

[multicastDelegate recieveMessage:message sender:self];

我们来看看单代理的方法调用

if ([delegate respondsToSelector:@selector(recieveMessage:sender:)]){
      [delegate recieveMessage:message sender:self];
}

可以看出多代理的调用方法和参数跟单代理基本是一样的,而实际上是multicastDelegate多代理管理类内部又做了一次透明的多个代理转发。

delegateVSMultiDelegate

XMPPFramework

第三方开源的XMPPFramework XMPPFramework 框架中实现了一个通用的多代理类GCDMulticastDelegate,我们的例子基于GCDMulticastDelegate来实现。

GCDMulticastDelegate中增加删除代理的接口定义

@interface GCDMulticastDelegate : NSObject
-(void)addDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue;
-(void)removeDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue;
@end

代理存储数组变量delegateNodes

@interface GCDMulticastDelegate () {
    NSMutableArray *delegateNodes;
}

同时实现了methodSignatureForSelector 和 forwardInvocation 2个用于消息转发的方法。

(具体实现请参考XMPPFramework源代码)

聊天列表实例

我们来实现一个简单的联系人列表界面如下:

左边是联系人列表,每一行显示用户昵称和是否在线的状态,如果在线为绿色小圆,不在线的话为灰色。
当点击左边每一行时右边页面会显示联系人的详细资料。

当用户上线或者下线时,左边的状态小圆要实时更新。同时左边页面上的头像也要更新,如果离线的话头像需要灰色显示。

BuddyWindowDemo

新建一个工程BuddyMultiDelegateDemo。

页面主体Controller

创建3个Controller:

  1. MainViewController,继承NSSplitViewController 做为主体分割视图框架类。
  2. BuddyListViewController 继承NSViewController 做为联系人列表
  3. BuddyDetailViewController 继承NSViewController 显示联系人详细信息

MainViewController.m实现

#import "MainViewController.h"
#import "BuddyListViewController.h"
#import "BuddyDetailViewController.h"
#import "Masonry.h"

@interface MainViewController ()
@property(nonatomic,strong)BuddyListViewController *buddyListViewController;
@property(nonatomic,strong)BuddyDetailViewController *buddyDetailViewController;
@end

@implementation MainViewController

- (void)viewDidLoad {
    [super viewDidLoad];
   
    NSSplitViewItem *item1 = [[NSSplitViewItem alloc]init];
    item1.viewController = self.buddyListViewController;
    item1.canCollapse =YES;
    [item1 setHoldingPriority:250];
    
    NSSplitViewItem *item2 = [[NSSplitViewItem alloc]init];
    item2.viewController = self.buddyDetailViewController;
    item2.canCollapse = YES;
    [item2 setHoldingPriority:270];
    
    //水平方向
    self.splitView.vertical = YES;
    
    //增加NSSplitViewItem对象
    [self addSplitViewItem:item1];
    [self addSplitViewItem:item2];
    
    //设置第一个视图的宽度最小100,最大200
    [self.view mas_makeConstraints:^(MASConstraintMaker *make) {
        make.width.greaterThanOrEqualTo(@700);
        make.height.greaterThanOrEqualTo(@260);
    }];
    
    //设置第一个视图的宽度最小100,最大200
    [item1.viewController.view mas_makeConstraints:^(MASConstraintMaker *make) {
        make.width.greaterThanOrEqualTo(@100);
        make.width.lessThanOrEqualTo(@200);
    }];
    
    //设置第二个视图的宽度最小100,最大800
    [item2.viewController.view mas_makeConstraints:^(MASConstraintMaker *make) {
        make.width.greaterThanOrEqualTo(@100);
        make.width.lessThanOrEqualTo(@600);
    }];
    // Do view setup here.
}


- (BuddyListViewController*)buddyListViewController{
    if(!_buddyListViewController){
        _buddyListViewController = [[BuddyListViewController alloc]init];
    }
    return _buddyListViewController;
}

- (BuddyDetailViewController*)buddyDetailViewController{
    if(!_buddyDetailViewController){
        _buddyDetailViewController = [[BuddyDetailViewController alloc]init];

    }
    return _buddyDetailViewController;
}
@end

联系人模型类

Buddy:联系人基本信息类

#import "DBModel.h"
@interface Buddy : DBModel
@property (nonatomic, assign) NSInteger ID;
@property (nonatomic, strong) NSString *nickName;
@property (nonatomic, assign) BOOL status;
/*init property member var by parsing NSDictionary parameter*/
-(id)initWithDictionary:(NSDictionary *)dictionary;

@end

BuddyDetail: 联系人详细信息类

#import "DBModel.h"
@interface BuddyDetail : DBModel
@property (nonatomic, assign) NSInteger ID;
@property (nonatomic, strong) NSString *firstName;
@property (nonatomic, strong) NSString *lastName;
@property (nonatomic, strong) NSString *address;
@property (nonatomic, strong) NSString *birthDay;
@property (nonatomic, strong) NSString *image;
/*init property member var by parsing NSDictionary parameter*/
- (id)initWithDictionary:(NSDictionary *)dictionary;
/*save new model object*/
@end

多代理管理类

多代理协议定义类BuddyStatusMulticastDelegate

1.定义了BuddyStatusDelegate协议,申明了2个代理方法。

2.实现代理方法对应的广播方法:具体是使用multicastDelegate广播代理方法

BuddyStatusMulticastDelegate.h

#import "XXXMultiDelegateModule.h"
#import "GCDMulticastDelegate.h"
@class   Buddy;
@interface BuddyStatusMulticastDelegate : XXXMultiDelegateModule
-(void)buddyOfflineRequest:(Buddy *)buddy;
-(void)buddyOnlineRequest:(Buddy *)buddy;
@end

@protocol BuddyStatusDelegate
@optional
-(void)buddyStatus:(BuddyStatusMulticastDelegate *)sender didReceiveBuddyOfflineRequest:(Buddy *)buddy;
-(void)buddyStatus:(BuddyStatusMulticastDelegate *)sender didReceiveBuddyOnlineRequest:(Buddy *)buddy;
@end

BuddyStatusMulticastDelegate.m

#import "BuddyStatusMulticastDelegate.h"
@implementation BuddyStatusMulticastDelegate
- (void)buddyOfflineRequest:(Buddy *)buddy{
    [multicastDelegate buddyStatus:self didReceiveBuddyOfflineRequest:buddy];
}

- (void)buddyOnlineRequest:(Buddy *)buddy{
    [multicastDelegate buddyStatus:self didReceiveBuddyOnlineRequest:buddy];
}
@end

多代理协议管理类BuddyStatusManager

#import "BuddyStatusMulticastDelegate.h"
@interface BuddyStatusManager : NSObject
+(instancetype)sharedInstance;

@property(nonatomic,readonly)BuddyStatusMulticastDelegate *statusDelegate;
@end

#import "BuddyStatusManager.h"
#import "BuddyStatusMulticastDelegate.h"

@interface BuddyStatusManager()
@property(nonatomic,readwrite)BuddyStatusMulticastDelegate *statusDelegate;
@end

@implementation BuddyStatusManager
+ (instancetype)sharedInstance{
    static dispatch_once_t onceToken;
    static BuddyStatusManager *sharedInstance;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[BuddyStatusManager alloc] init];
    });
    return sharedInstance;
}

- (BuddyStatusMulticastDelegate*)statusDelegate{
  if(!_statusDelegate){
        _statusDelegate = [[BuddyStatusMulticastDelegate alloc]init];
    }
    return _statusDelegate;
}
@end

在Controller中注册多代理

在BuddyListViewController 的 viewDidLoad 和 dealloc中分别增加和删除多代理。

- (void)viewDidLoad{
    [super viewDidLoad];
    [self addBuddyMultiDelegate];
}
- (void)dealloc{
    [self removeBuddyMultiDelegate];
}
#pragma maek - multiDelegate
- (void)addBuddyMultiDelegate {
    [[BuddyStatusManager sharedInstance].statusDelegate addDelegate:self delegateQueue:dispatch_get_main_queue()];
}

- (void)removeBuddyMultiDelegate {
    [[BuddyStatusManager sharedInstance].statusDelegate removeDelegate:self delegateQueue:dispatch_get_main_queue()];
}

- (void)buddyStatus:(BuddyStatusMulticastDelegate *)sender didReceiveBuddyOfflineRequest:(Buddy *)buddy {
    [self upDateBuddyStatus:buddy];
}

- (void)buddyStatus:(BuddyStatusMulticastDelegate *)sender didReceiveBuddyOnlineRequest:(Buddy *)buddy {
    [self upDateBuddyStatus:buddy];
}

- (void)upDateBuddyStatus:(Buddy *)buddy {
    for(Buddy *aBubby in self.buddies) {
        if(aBubby.ID==buddy.ID){
            aBubby.status = buddy.status;
            break;
        }
    }
    [self.tableView reloadData];
}

在BuddyDetailViewController中使用也是类似,不再赘述。

多代理通知

我们在AppDelegate中实现一个测试方法,模拟用户在线状态变化,这样在列表BuddyListViewController 和 详细资料窗口BuddyDetailViewController 中都能收到用户状态变化的代理通知。

- (void)buddyTest {
    NSInteger ID = random() % 2 + 1;
    NSInteger status = random() % 10;
    Buddy *buddy = [[Buddy alloc]init];
    buddy.ID = ID;
    buddy.status = NO;
    if(status>5){
        buddy.status = YES;
    }
    if(!buddy.status){
        [[BuddyStatusManager sharedInstance].statusDelegate buddyOfflineRequest:buddy];
    }
    else{
        [[BuddyStatusManager sharedInstance].statusDelegate buddyOfflineRequest:buddy];
    }
    NSLog(@"ID %ld status %d ",ID,buddy.status);
}