消息转发与多重代理

代理容器

在开发过程中,我们可能会遇到这种情况,某一处有变量被修改或某个动作执行完毕时,我们需要通知多个对象做出不同的响应。这种情况下,我们可能首先想的就是使用NSNotificationCenter来实现。
如果需求进一步加深,我们需要这些对象在执行完自己的响应之后有返回值回到发出“通知”的地方。这种情况下如果再使用“通知”就会变得很繁琐,我们需要额外的处理很多“通知”。
这种情况下,我们会期望使用“协议-代理”的方式来进行交互处理,但一般情况下“协议-代理”只被用在一对一的交互关系中,无法适用于此处的一对多关系。那么我们该怎么做呢?

我们来设想如果使用一个容器,来把所有“代理”都装在里面,当我们需要使用回调时,再把它们一个个拿出来进行回调,不就解决了这个问题吗。

    int result = 0;
    for (id<OneProtocol> delegate in self.delegateArray) {
      result += [delegate doSomething];
    }

这样当然可以解决我们最初的问题,但是我们肯定不想在每个需要代理的地方都维护一个数组,并且每次回调时都手写一个循环。所以,我们需要一个新的类来帮助我们管理这个容器。我们姑且叫这个代理管理类为代理容器。

代理容器帮助我们维护了代理数组,但是当我们需要回调时,代理容器如何找到数组里正确的代理,并告诉它调用哪个方法呢,这就需要进行’消息转发‘。

消息转发

我们知道Object—C的方法调用,其实就是依托objc_msgSend()给对象发送了一条消息。对象在收到消息之后,会通过isa找到类的结构体,然后读取method list寻找selector。如果在本类中找不到,则去他的父类的结构体寻找,一直到NSObject为止。如果在NSObject也没有找到需要的selector,这时候就需要开始进行消息转发了。

消息转发的过程被总结为三个步骤,分别是:

1.Method resolution 方法解析处理阶段
2.Fast forwarding 快速转发阶段
3.Normal forwarding 常规转发阶段
Method resolution 方法解析处理阶段
 //实例对象调用方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
 //类对象调用方法
+ (BOOL)resolveClassMethod:(SEL)sel

对象在method list中找不到对应selector时,会先调用这两个方法。你可以将它理解成,询问你是否添加动态方法来进行处理;如果YES则能接受消息表示你已经添加了方法,将会重新执行objc_msgSend;NO则表示不添加方法,进入消息转发第二步。

Fast forwarding 快速转发阶段
- (id)forwardingTargetForSelector:(SEL)aSelector

既然在第一步已经明确,此对象没有添加新方法响应aSelector,这个方法实际上是让你传递一个可以响应此方法的其它对象。如果你在此返回了一个对象,则会给这个对象发送一个objc_msgSend消息,如果这里返回nil或者self本身,那么则进入消息转发的第三个步骤。

Normal forwarding 常规转发阶段
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
- (void)forwardInvocation:(NSInvocation *)anInvocation

在第二步已经明确没有其他对象可以帮助我们响应这个方法,所以第三部Objec-C又做出了让步。你可以重新制定一个方法,但前提是你需要给出这个方法的方法签名。NSMethodSignature表示方法签名,手写创建比较麻烦,你可以找一下它的规则表。如果你在这里返回了一个方法签名,那么我们还需要在forwardInvocation中来定义如何使用这个方法签名。

至此我们完成了消息转发的所有步骤,如果在这三个步骤都没有处理好找个SEL,那么就会执行doesNotRecognizeSelector方法抛出一个异常。

NSMethodSignature && NSInvocation

这里出现了 NSMethodSignature 和 NSInvocation 这两个类。 这里也稍微聊一下NSInvocation这个类。

我们平时调用方法的写法可能有两种:

    [anObject doSomething];
    [anObject performSelector:@selector(doSomething) withObject:anArgument];

在不同的情况下,我们会选择合适的调用方法。在此之后,OC还为我们提供了另一种调用方法,就是使用NSInvocation来进行。

//    1.创建方法签名;
    NSMethodSignature *sig = [[self class] instanceMethodSignatureForSelector:@selector(doSomething:)];
//    2.通过方法签名生成NSInvocation
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
//    3.对invocation设置 方法调用者
    invocation.target = self;
//    4.对invocation设置 方法选择器
    invocation.selector = @selector(doSomething:);
//    5.对invocation设置 参数
    NSString *something = @"do something";
    //注意:设置的参数必须从2开始;因为0和1 已经被self ,_cmd 给占用了
    [invocation setArgument:&something atIndex:2];
//    6.执行invocation
    [invocation invoke];

NSInvocation相关的API还有许多,这里就不赘述了。

GCDMulticastDelegate

我们从XMPP框架里的GCDMulticastDelegate来看一下多重代理的具体实现。

GCDMulticastDelegate中共有 GCDMulticastDelegate、GCDMulticastDelegateEnumerator、GCDMulticastDelegateNode这三个类。我们只要重点看一下GCDMulticastDelegate这个类就可以了。

GCDMulticastDelegate

GCDMulticastDelegate维护了一个delegateNodes数组,当调用addDelegate方法时,就会把真正的代理’delegate‘和一个GCD线程构建成一个GCDMulticastDelegateNode对象添加到数组里面。

- (id)init
{
    if ((self = [super init]))
    {
        delegateNodes = [[NSMutableArray alloc] init];
    }
    return self;
}

- (void)addDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue
{
    if (delegate == nil) return;
    if (delegateQueue == NULL) return;
    
    GCDMulticastDelegateNode *node = [[GCDMulticastDelegateNode alloc] init];
    node.delegate = delegate;
    node.delegateQueue = delegateQueue;
    
    [delegateNodes addObject:node];
}

当我们需要调用代理的协议方法时,我们实际上就是让GCDMulticastDelegate的对象执行某方法。很明显,GCDMulticastDelegate是并没有实现这个方法的。所以接下来我们就要进入消息转发的三个阶段。

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    for (GCDMulticastDelegateNode *node in delegateNodes)
    {
        NSMethodSignature *result = [node.delegate methodSignatureForSelector:aSelector];
        
        if (result != nil)
        {
            return result;
        }
    }
    
    // This causes a crash...
    // return [super methodSignatureForSelector:aSelector];
    
    // This also causes a crash...
    // return nil;
    
    return [[self class] instanceMethodSignatureForSelector:@selector(doNothing)];
}

- (void)forwardInvocation:(NSInvocation *)origInvocation
{
    @autoreleasepool {
    
        SEL selector = [origInvocation selector];
        
        for (GCDMulticastDelegateNode *node in delegateNodes)
        {
            id delegate = node.delegate;
            
            if ([delegate respondsToSelector:selector])
            {
                // All delegates MUST be invoked ASYNCHRONOUSLY.
                
                NSInvocation *dupInvocation = [self duplicateInvocation:origInvocation];
                
                dispatch_async(node.delegateQueue, ^{ @autoreleasepool {
                    
                    [dupInvocation invokeWithTarget:delegate];
                    
                }});
            }
        }
    }
}

- (NSInvocation *)duplicateInvocation:(NSInvocation *)origInvocation
{
    NSMethodSignature *methodSignature = [origInvocation methodSignature];
    
    NSInvocation *dupInvocation = [NSInvocation invocationWithMethodSignature:methodSignature];
    [dupInvocation setSelector:[origInvocation selector]];
    
    NSUInteger i, count = [methodSignature numberOfArguments];
    for (i = 2; i < count; i++)
    {
        const char *type = [methodSignature getArgumentTypeAtIndex:i];
        
        if (*type == *@encode(BOOL))
        {
            BOOL value;
            [origInvocation getArgument:&value atIndex:i];
            [dupInvocation setArgument:&value atIndex:i];
        }
        else if (*type == *@encode(char) || *type == *@encode(unsigned char))
        {
            char value;
            [origInvocation getArgument:&value atIndex:i];
            [dupInvocation setArgument:&value atIndex:i];
        }
        else if (*type == *@encode(short) || *type == *@encode(unsigned short))
        {
            short value;
            [origInvocation getArgument:&value atIndex:i];
            [dupInvocation setArgument:&value atIndex:i];
        }
        else if (*type == *@encode(int) || *type == *@encode(unsigned int))
        {
            int value;
            [origInvocation getArgument:&value atIndex:i];
            [dupInvocation setArgument:&value atIndex:i];
        }
        else if (*type == *@encode(long) || *type == *@encode(unsigned long))
        {
            long value;
            [origInvocation getArgument:&value atIndex:i];
            [dupInvocation setArgument:&value atIndex:i];
        }
        else if (*type == *@encode(long long) || *type == *@encode(unsigned long long))
        {
            long long value;
            [origInvocation getArgument:&value atIndex:i];
            [dupInvocation setArgument:&value atIndex:i];
        }
        else if (*type == *@encode(double))
        {
            double value;
            [origInvocation getArgument:&value atIndex:i];
            [dupInvocation setArgument:&value atIndex:i];
        }
        else if (*type == *@encode(float))
        {
            float value;
            [origInvocation getArgument:&value atIndex:i];
            [dupInvocation setArgument:&value atIndex:i];
        }
        else if (*type == '@')
        {
            void *value;
            [origInvocation getArgument:&value atIndex:i];
            [dupInvocation setArgument:&value atIndex:i];
        }
        else
        {
            NSString *selectorStr = NSStringFromSelector([origInvocation selector]);
            
            NSString *format = @"Argument %lu to method %@ - Type(%c) not supported";
            NSString *reason = [NSString stringWithFormat:format, (unsigned long)(i - 2), selectorStr, *type];
            
            [[NSException exceptionWithName:NSInvalidArgumentException reason:reason userInfo:nil] raise];
        }
    }
    
    [dupInvocation retainArguments];
    
    return dupInvocation;
}

从这几个方法中,我们可以出其处理消息转发的逻辑

1. 常规消息转发阶段,在寻找NSMethodSignature方法签名时,会遍历储存Node的数组,寻找可以执行此方法的delegate,获取其方法签名。
2. 在forwardInvocation方法中,首先我们获取到selector,然后遍历数组,寻找可以实现此selector的对象。
3. 利用方法签名生成了一个新的NSNSInvocation对象,然后遍历原origInvocation的参数列表,将所有的参数及其类型押入新的NSInvocation当中。
4. 在Node内存储的GCD线程中,异步调用Invocation的invoke方法,传入参数delegate作为执行这个方法的对象。

这样我们就完成了将一个GCDMulticastDelegate对象无法响应的方法转发给其持有的所有能影响此方法的对象,并在其指定的线程中进行回调。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350