OC中的消息转发机制

作者: 欧阳铨 | 来源:发表于2017-01-05 16:58 被阅读201次

    在本文中,将为你解释在OC的动态机制中,一个对象是如何调用,并且在对象中找不到方法的情况下,如何将方法通过"发消息"的形式转发给其它接受者。

    在了解消息转发机制之间,要知道OC中方法如何调用,并且为什么需要消息转发。

    OC的语法中,我们调用这样调用一个方法:

    [receiver method:param];
    

    编译后的c代码是这样调用方法的:

    objc_msgSend(receiver, @selector(method), param);
    
    • 在OC的所有方法中,其实有两个隐藏参数,一个是id receiver规定了方法的接受者,另一个是SEL _cmd,方法的selector。

    究竟在msgSend方法中做了什么?

    在runtime源码中,msgSend方法在objc-msg-arm.s中,为了保证效率,使用了汇编实现。

    id objc_msgSend(id self, SEL _cmd,...);
    
         ENTRY objc_msgSend 
         MESSENGER_START  
         cbz    r0, LNilReceiver_f ;receiver为空则直接返回
         ldr    r9, [r0]           ;r9 = self->isa 
         CacheLookup NORMAL        ; calls IMP or LCacheMiss
                                 ; 首先在缓存中寻找IMP
    LCacheMiss:               ; 缓存中找不到IMP 
         MESSENGER_END_SLOW 
         ldr    r9, [r0, #ISA]      ; class = receiver->isa
         b  __objc_msgSend_uncached ;在父类中寻找IMP
    LNilReceiver:           ; 找不到IMP,标识IMP为消息转发 
                                ; r0 is already zero 
         mov    r1, #0 
         mov    r2, #0 
         mov    r3, #0 
         FP_RETURN_ZERO 
         MESSENGER_END_NIL 
         bx lr
    LMsgSendExit:
         END_ENTRY objc_msgSend
    

    在源码中有清晰的注释,让我们知道,msgSend方法实际上逻辑不复杂。

    • 第一步,判断receiver是否为空,若为空则直接返回,这就是给一个空对象调用方法不会奔溃的原因。
    • 第二步,在缓存中搜索是否已经缓存IMP,若已缓存,则直接返回。
    • 第三步,在父类中寻找IMP方法,找到则返回。* 第四步,标记IMP为_objc_msgForward,意思是调用这个方法直接走消息转发机制。

    在类中寻找IMP

    在上面的描述中,我们需要在一个类中寻找方法的实现,这个寻找方法,在runtime源码objc-class-old.mm中的方法:

    IMP lookUpImpOrForward(Class cls, 
                             SEL sel, 
                              id inst, 
                            bool initialize, 
                            bool cache,
                            bool resolver)
    

    主要执行了以下动作

    1. 解锁methodList,用于无锁查找,速度快。
    2. 在缓存中查找,如果找到,则返回缓存的方法。
    3. 查找cls是否被释放,如果是,则返回cls被释放的错误。
    4. 判断cls是否已经初始化,没有初始化则初始化。
    5. 加锁methodList,防止多线程操作。
    6. 如果是垃圾回收机制的方法,则忽略,并添加到类的忽略方法列表中。
    7. 尝试查找缓存,找到则返回。
    8. 尝试在这个类的方法列表methodList中查找,如果找到,则添加到缓存中。
    9. 如果还没有找到,就从父类的缓存和父类的方法列表中查找,找到就添加到缓存中,没找到就进入_class_resolveMethod方法。
    10. 调用_class_resolveMethod方法,给机会动态添加方法,然后重新调用msgSend查找,看看有没有添加方法。
    11. 将这个方法直接缓存为消息转发。
    12. 解锁methodList。

    到此,已经和msgSend没什么关系了,关键在于msgSend方法中,找不到方法的实现,则开始消息转发。

    消息转发

    上面我们提到,当实在找不到方法实现时,会走消息转发。你有3次机会添加方法的实现,如果还没有方法的实现,程序只能Crash了。接下来用例子分别说明这3次机会。新建一个Son和Father类,并在Son类中添加方法声明,不实现方法。

    // Son.h
    #import <Foundation/Foundation.h>
    @interface Son : NSObject
    - (void)method;
    @end
    
    // Son.m
    #import "Son.h"
    #import "Father.h"
    #import <objc/runtime.h>
    @implementation Son
    @end
    

    然后,调用son的method方法。


    son调用method奔溃.png

    结果可想而知,son的method方法没有实现,并且也没有在消息转发过程中添加实现,所以出现经典的奔溃:

    Terminating app due to uncaught exception 'NSInvalidArgumentException',
    reason: '-[Son method]: unrecognized selector sent to 
    instance 0x1003031e0'
    

    那在消息转发中,调用了哪些方法呢?我们给[son method]方法打上断点,并在gbd中输入命令call (void)instrumentObjcMessageSends(YES)

    断点调试.png
    然后在终端中打开文件夹open /private/tmp找到"msgSend-xxxx"之类的文件,双击打开。
    消息转发方法.png
    这些就是在消息转发的过程中调用的方法。
    1.resolveInstanceMethod: (或 resolveClassMethod:)方法。这是第一次机会让你添加方法实现。在这里,调用class_addMethod方法添加实现,并返回YES,然后会重新开始msgSend流程。如果没有实现,那么进入第二次机会。
    2.forwardingTargetForSelector:方法。这是第二次机会,但是这一次不是添加方法实现,而是将消息转发给能够响应此消息的对象,直接把消息发给它。否则返回nil。
    3.methodSignatureForSelector:方法。这是第三次机会,这个方法尝试获取方法的签名如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。刚刚奔溃的unrecognized selector sent to instance错误就出自于此。 如果能获取,则返回非nil:创建一个 NSlnvocation 并传给forwardInvocation:。
    4.forwardInvocation:方法。将第三步的方法签名添加一个方法实现。
    5.doesNotRecognizeSelector: 方法。抛出找不到方法签名的移除,程序Crash。###知道的消息转发的怎么回事,怎么去使用呢?

    第一次机会:
    +(BOOL)resolveInstanceMethod:(SEL)sel;在son.m中重写resolveInstanceMethod:方法,使用class_addMethod()增加method方法的实现:

    // son.m
    #import "Son.h"
    #import "Father.h"
    #import <objc/runtime.h>
    @implementation Son
    void method(id self, SEL _cmd){ 
         NSLog(@"son method");
    }
    +(BOOL)resolveInstanceMethod:(SEL)sel{
     if (sel == @selector(method)) { 
         class_addMethod(self, sel, (IMP)method, "v@:" ); 
         return YES;
     }
     return [super resolveInstanceMethod:sel];}@end
    

    可以看到已经成功执行method方法:

    第一次机会实现son method.png

    第二次机会:
    -(id)forwardingTargetForSelector:(SEL)aSelector;

    在son.m中重写forwardingTargetForSelector:方法,返回能相应method方法的实例,更换消息的接收者。下面就是让father实例接收method方法:

    // son.m
    #import "Son.h"
    #import "Father.h"
    #import <objc/runtime.h>
    @implementation Son
    -(id)forwardingTargetForSelector:(SEL)aSelector{ 
         return [[Father alloc] init];
    }
    @end
    

    然后在father.m中实现method方法:

    // father.m
    #import "Father.h"
    @implementation Father
    -(void)method{ 
           NSLog(@"Father method");
    }
    end
    

    惊讶地发现,son实例调用method方法,却是father实现去相应method方法:


    第二次机会实现son method.png

    第三次机会:
    -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
    -(void)forwardInvocation:(NSInvocation *)anInvocation

    在son.m中,重写-(NSMethodSignature *)methodSignatureForSelector方法,返回一个method方法的签名,并在forwardInvocation:方法中,指定方法的响应者:

    // Son.m
    #import "Son.h"
    #import "Father.h"
    #import <objc/runtime.h>
    @implementation Son
    -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{ 
                            NSString *sel = NSStringFromSelector(aSelector); 
                            if([sel isEqualToString:@"method"]){ 
                                //手动为method方法添加签名 
                                return [NSMethodSignature signatureWithObjCTypes:"v@:"]; 
                            } 
                            return [super methodSignatureForSelector:aSelector];
    }
    -(void)forwardInvocation:(NSInvocation *)anInvocation{ 
           SEL selector = [anInvocation selector]; //新建需要接受消息的对象 
           Father *father = [[Father alloc] init]; 
           if([father respondsToSelector:selector]){ 
              //接收对象唤醒方法 
             [anInvocation invokeWithTarget:father]; 
            }
    }
    @end
    

    最终的结果还是father响应了method方法:

    第三次机会实现son method.png

    那消息转发有什么用?

    在日常开发中,使用消息转发的场景并不多,大多数是防止程序Crash。当然,你也可以直接调用_objc_msgForward,这样会告诉msgSend方法,直接进入消息转发。著名的热修复工具JSPath,就是直接调用_objc_msgForward实现的。

    OC中的消息转发大致如此,有什么问题可以留言,我们共同探讨哦。

    相关文章

      网友评论

        本文标题:OC中的消息转发机制

        本文链接:https://www.haomeiwen.com/subject/cayzvttx.html