iOS 消息转发机制

作者: BlackStar暗星 | 来源:发表于2021-03-12 14:10 被阅读0次

    对于OC而言,我们调用一个方法,如

    [self foo:@"haha"]
    

    那么foo是怎么执行的呢?
    首先要知道OC的类对象实际上是一个结构体

    struct objc_class {
        Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    
    #if !__OBJC2__
        Class _Nullable super_class                              OBJC2_UNAVAILABLE;
        const char * _Nonnull name                               OBJC2_UNAVAILABLE;
        long version                                             OBJC2_UNAVAILABLE;
        long info                                                OBJC2_UNAVAILABLE;
        long instance_size                                       OBJC2_UNAVAILABLE;
        struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
        struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
        struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
        struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
    #endif
    
    } OBJC2_UNAVAILABLE;
    

    在这里可以看到两行关键的代码:

    struct objc_method_list * _Nullable * _Nullable methodLists  
    struct objc_cache * _Nonnull cache
    

    这里不提类方法,只说实例方法(类方法不存在这里,存在元类里边)

    第一个存放的是所有实例方法,第二个是常用方法的缓存,缓存的目的是为了提升查找消息的效率

    当我们执行 foo 的时候,系统首先会找到他的类对象,也就是这个结构体,然后通过 objc_cache 列表查找是否有这个方法,有就执行,没有再去查找 objc_method_list 列表,有就执行,没有再去查找父类的,以此类推,直到找到方法去执行。


    那么如果通过查找机制没有找到想要执行的方法呢?这就涉及到了消息转发机制

    消息转发机制

    消息转发机制分类三种情况

    • 动态方法解析
    • 快速消息转发
    • 完全消息转发(标准消息转发,发现叫啥的都有,名字无所谓了)

    首先调用

    [self performSelector:@selector(foo:) withObject:@"haha"];
    

    为什么不用 [self foo:@"haha"] 了,因为如果我们使用这种调用方式,并且没有实现 foo 方法,编译器就会直接报错,我们就没法继续往下进行试验了。

    一、动态方法解析

    动态方法解析主要是两个方法

    + (BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
    + (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
    

    从名称上可以看出,第一个是针对类方法的,第二个是实例方法,因为本次研究实例方法,所以不对第一个进行试验

    首先我们 实现 foo: 方法,然后实现第一个方法

    -(void)foo:(NSString *)charType{
        NSLog(@"foo == %@",charType);
    }
    
    +(BOOL)resolveInstanceMethod:(SEL)sel{
        NSLog(@"resolveInstanceMethod");
        return [super resolveInstanceMethod:sel];
    }
    

    发现 foo: 执行了,而 resolveInstanceMethod 未执行

    接着注释 foo: 方法 ,然后执行,发现 resolveInstanceMethod 执行了
    然后我们修改下代码

    //-(void)foo:(NSString *)charType{
    //
    //    NSLog(@"foo == %@",charType);
    //}
    
    +(BOOL)resolveInstanceMethod:(SEL)sel{
        NSLog(@"resolveInstanceMethod");
        if (sel == NSSelectorFromString(@"foo:")) {
            class_addMethod([self class], sel, class_getMethodImplementation([self class], @selector(fooMethod:)), "v:8");
            return NO;
        }
        return [super resolveInstanceMethod:sel];
    }
    
    -(void)fooMethod:(NSString *)charType{
        NSLog(@"fooMethod == %@",charType);
    }
    

    通过 runtimeclass_addMethod 方法,动态的添加一个实例方法 fooMethod: 然后发现 fooMethod: 执行了。

    结果表明:如果 foo 方法实现,不会进行动态方法解析,方法未实现,会通过 resolveInstanceMethod 进行消息转发,而我们通过这一点,可以对消息进行拦截,再执行我们自己想要执行的方法

    二、快速消息转发

    -(id)forwardingTargetForSelector:(SEL)aSelector;
    

    修改代码

    //这里也可以直接注释掉
    +(BOOL)resolveInstanceMethod:(SEL)sel{
        return [super resolveInstanceMethod:sel];
    }
    
    -(id)forwardingTargetForSelector:(SEL)aSelector{
        NSLog(@"forwardingTargetForSelector");
        return nil;//[BSMsgSendForwardingTemp alloc];
    }
    

    这里的 resolveInstanceMethod 也可以不写
    打印结果表明,无论resolveInstanceMethod return YES 还是 return NO 只要没有找到方法,就会进入到快速消息转发,而如果在 resolveInstanceMethod 通过一些手段如 class_addMethod 进行消息拦截转发,就不会进入到快速消息转发方法里

    再次修改代码

    -(id)forwardingTargetForSelector:(SEL)aSelector{
        NSLog(@"forwardingTargetForSelector");
        return [BSMsgSendForwardingTemp alloc];
    }
    

    这里 returnBSMsgSendForwardingTemp ,然后在 BSMsgSendForwardingTemp 实现 foo: 方法

    @implementation BSMsgSendForwardingTemp
    -(void)foo:(NSString *)charType{   
        NSLog(@"BSMsgSendForwardingTemp foo == %@",charType);   
    }
    @end
    

    发现 -(void)foo:(NSString *)charType执行了

    结果表明:无论 resolveInstanceMethod return YES or NO ,只要方法没有找到目标,就会进入快速消息转发。进入快速消息转发后,可以通过 forwardingTargetForSelector 来指定某一个对象去执行相应的方法。

    三、完全消息转发

    -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
        NSLog(@"methodSignatureForSelector");
        
        if (aSelector == @selector(foo:)) {
            return  [NSMethodSignature signatureWithObjCTypes:"v@:@"];
        }
        
        return [super methodSignatureForSelector:aSelector];
    }
    
    
    -(void)forwardInvocation:(NSInvocation *)anInvocation{
        NSLog(@"forwardInvocation");
        
        SEL sel = anInvocation.selector;
        
        BSMsgSendForwardingTemp *temp = [[BSMsgSendForwardingTemp alloc]init];
    
        if ([temp respondsToSelector:sel]) {
            [anInvocation invokeWithTarget:temp];
        }
    }
    

    methodSignatureForSelector 方法需要返回一个 方法选择器的 签名NSMethodSignature,然后会进入到 forwardInvocation ,通过此方法进行消息传递。

    如果到这里依然没有找到要执行的方法,那么会执行 doesNotRecognizeSelector

    -(void)doesNotRecognizeSelector:(SEL)aSelector{
        NSLog(@"doesNotRecognizeSelector");
    }
    


    总结

    从动态方法解析 到 快速消息转发 再到 完全消息转发这一过程中,如果在某一过程中,找到了需要执行的方法,就不会在向下执行。
    对于 resolveInstanceMethod 方法返回的 BOOL 值,目前能确定的是,他跟调试信息有关,和是否向下进行无关,具体还有没有其他用途,暂不确定。



    引申

    + (nullable NSMethodSignature *)signatureWithObjCTypes:(const char *)types; 
    

    参数 ObjCTypes 是一个是字符串数组,该数组包含了方法的类型编码
    官方文档 Type Encodings
    如上边例子 [NSMethodSignature signatureWithObjCTypes:"v@:@"]

    • v
      • 官方文档: A void
      • 理解:返回类型为void
    • 第一个@
      • 官方文档: An object (whether statically typed or typed id)
      • 理解:id类型的方法调用者(id类型的消息接受者)
    • :
      • 官方文档: A method selector (SEL)
      • 理解:调用的SEL
    • 第二个@ = An object (whether statically typed or typed id)
      • 官方文档: An object (whether statically typed or typed id)
      • 理解:id类型参数

    相关文章

      网友评论

        本文标题:iOS 消息转发机制

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