美文网首页
iOS消息转发

iOS消息转发

作者: 有梦想的程序员 | 来源:发表于2019-08-04 14:20 被阅读0次

    笔者也是一名菜鸟,从头开始学习Runtime,所以有些东西可能不正确。而且这个简书排版也不怎么会,有问题各位大佬可以直接指出,不用留情面。

    关于消息转发有几个问题,带着问题去寻找答案我觉得更高效。

    1、消息转发是如何触发的?

    2、消息转发都分为几步?

    3、消息转发有什么作用?

    先思考一下这3个问题,然后带着疑问去看看Runtime中发生了什么。

    1、消息转发是如何触发的?

    当前创建了一个类,类名BookBook.h声明了一个方法- (void)sell;但是没有实现该方法。Xcode会友好的提示我们Method definition for 'sell' not found

    //
    //  Book.h
    //  消息转发
    //
    //  Created by -- on 2019/8/2.
    //  Copyright © 2019 --. All rights reserved.
    //
    
    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface Book : NSObject
    
    - (void)sell;
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    //
    //  Book.m
    //  消息转发
    //
    //  Created by -- on 2019/8/2.
    //  Copyright © 2019 --. All rights reserved.
    //
    
    #import "Book.h"
    #import <objc/runtime.h>
    
    @implementation Book
    
    //Method definition for 'sell' not found
    
    @end
    

    接下来在 ViewController.mviewDidLoad 中调用 [book sell] ,很显然这个会崩溃的。

    //
    //  ViewController.m
    //  消息转发
    //
    //  Created by -- on 2019/8/1.
    //  Copyright © 2019 --. All rights reserved.
    //
    
    #import "ViewController.h"
    #import "Book.h"
    
    @interface ViewController ()
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        //Class Book
        
        Book *book = [Book new];
        [book sell];
      
    }
    
    @end
    

    然后控制台打印如下:

    2019-08-02 09:57:30.059674+0800 消息转发[1205:43257] -[Book sell]: unrecognized selector sent to instance 0x600002ff0fc0
    2019-08-02 09:57:30.067159+0800 消息转发[1205:43257] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Book sell]: unrecognized selector sent to instance 0x600002ff0fc0'
    *** First throw call stack:
    (
        0   CoreFoundation                      0x000000010e5888db __exceptionPreprocess + 331
        1   libobjc.A.dylib                     0x000000010db2bac5 objc_exception_throw + 48
        2   CoreFoundation                      0x000000010e5a6c94 -[NSObject(NSObject) doesNotRecognizeSelector:] + 132
        3   CoreFoundation                      0x000000010e58d623 ___forwarding___ + 1443
        4   CoreFoundation                      0x000000010e58f418 _CF_forwarding_prep_0 + 120
        5   消息转发                        0x000000010d25571a -[ViewController viewDidLoad] + 106
    

    控制台打印出来了找不到方法实现的崩溃的栈,但是有意思的是在[ViewController viewDidLoad]之后接连发生了_CF_forwarding_prep_0___forwarding___的方法调用。

    网上一番搜索之后发现了一片文章Objective-C 消息发送与转发机制原理(二)对这此讲解。

    文章之后有这么一段:
    **__CF_forwarding_prep_0和 forwarding_prep_1函数都调用了forwarding只是传入参数不同。
    forwarding有两个参数,第一个参数为将要被转发消息的栈指针(可以简单理解成 IMP),第二个参数标记是否返回结构体。__CF_forwarding_prep_0第二个参数传入 0
    ,forwarding_prep_1传入的是 1,从函数名都能看得出来。下面是这两个函数的伪代码:

    image
    从图中可以看出来,但我们调用[book sell]的时候,Runtime找不到方法实现,之后进行了消息转发。转发之后给我们抛出了异常(-[Book sell]: unrecognized selector sent to instance 0x600002ff0fc0');

    那么Runtime是怎么寻找方法实现的?

    Runtime有一张这样的图是我们需要牢记在心的图。图如下:

    image.png
    上图实线是 Superclass 指针,虚线是 isa 指针。 Runtime先在我们的类Book中寻找我们的sell方法,如果Book中找不到方法实现,就会一层一层沿着父类寻找,最后找不到会调用doesNotRecognizeSelector方法,如果该方法不处理,Runtime就会抛出异常。

    既然知道了Runtime的查找方式图,那具体的查找方式呢?

    查阅了Runtime ObjC2.0源码,找到了如下代码:

    struct objc_object {
    private:
        isa_t isa;
        ...
    }
    
    struct objc_class : objc_object {
        // Class ISA;
        Class superclass;
        cache_t cache;             // formerly cache pointer and vtable
        class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    
        class_rw_t *data() { 
            return bits.data();
        }
        ...
    }
    

    class_rw_t下找到了我们想要的东西method_array_t

    struct class_rw_t {
        // Be warned that Symbolication knows the layout of this structure.
        uint32_t flags;
        uint32_t version;
    
        const class_ro_t *ro;
    
        method_array_t methods;
        property_array_t properties;
        protocol_array_t protocols;
        ...
    }
    
    struct method_t {
        //方法名
        SEL name;
        //返回类型
        const char *types;
       //方法实现的指针地址
        MethodListIMP imp;
    };
    

    看了method_array_t中存储的method_t这就是我们的方法在类中的存储位置,根据上方关系图,沿着父类寻找,最终因为Book及其父类没有 - (void)sell;的方法实现而崩溃并打印了异常信息。

    2、消息转发都分为几步?

    了解了上面Runtime底层的底层源码,对方法查找有初步的了解了。留意到一个特别有意思的方法__forwarding__,然后看看NSObject.h中相关的OC方法了。

    - (id)forwardingTargetForSelector:(SEL)aSelector OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
    - (void)forwardInvocation:(NSInvocation *)anInvocation OBJC_SWIFT_UNAVAILABLE("");
    

    发现这两个方法貌似和__forwarding__有关系。我们可以试试,到底有没有关系?

    • 我们断点一下


      image.png

    发现第一个断点进入了,说明这个是消息转发中一个步骤。之后我们查阅了官方文档,
    - (id)forwardingTargetForSelector:(SEL)aSelector 返回首先应将无法识别的消息定向到的对象。就是说我们需要一个实现了 -(void)sell新的对象来接收消息。
    创建了个新的对象BookStoreBookStore.h中没有声明方法,BookStore.m实现了- (void)sell方法

    //
    //  BookStore.h
    //  消息转发
    //
    //  Created by -- on 2019/8/2.
    //  Copyright © 2019 --. All rights reserved.
    //
    
    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface BookStore : NSObject
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    
    //
    //  BookStore.m
    //  消息转发
    //
    //  Created by -- on 2019/8/2.
    //  Copyright © 2019 --. All rights reserved.
    //
    
    #import "BookStore.h"
    
    @implementation BookStore
    
    - (void)sell{
        NSLog(@"卖书了~");
    }
    
    @end
    

    我们重写了- (id)forwardingTargetForSelector:(SEL)aSelector方法

    - (id)forwardingTargetForSelector:(SEL)aSelector {
        NSString *selName = NSStringFromSelector(aSelector);
        if ([selName isEqualToString:@"sell"]) {
            return [BookStore new];
        }
        return [super forwardingTargetForSelector:aSelector];
    }
    

    看一下控制台打印,这个步骤正确。

    2019-08-02 12:31:57.238626+0800 消息转发[4032:301841] 书店卖书了~
    
    • 我们把实现注释掉
    image.png

    接着又报了-[Book sell]: unrecognized selector sent to instance 0x6000032dc6d0相同的错误,难道- (void)forwardInvocation:(NSInvocation *)anInvocatio;不是消息转发中的一个步骤吗?

    忽然发现forwardInvocation方法中有一个NSInvocation对象。

    @interface NSInvocation : NSObject {
    @private
        void *_frame;
        void *_retdata;
        id _signature;
        id _container;
        uint8_t _retainedArgs;
        uint8_t _reserved[15];
    }
    
    + (NSInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;
    
    @property (readonly, retain) NSMethodSignature *methodSignature;
    

    然后点进去看发现了一个+ (NSInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;这样的方法,原来NSInvocation需要一个方法签名。

    + (nullable NSMethodSignature *)signatureWithObjCTypes:(const char *)types;就是生成方法签名的方法。

    当看NSObject.h的方法时,也看到了- (void)forwardInvocation:(NSInvocation *)anInvocation下方有一个- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;看来这个就是方法签名生成后返回给Runtime的实现了。接下来尝试一下:

    image.png
    const char *types需要什么呢?继续查看官方文档这里看到了关于const char *types生成方法:
    image.png

    Book.m先写一个方法实现,这方法实现等价于OC- (void)sell的方法。

    void sell(id self, SEL _cmd){
         NSLog(@"Book把自己卖了~");
    }
    

    根据上方规则,生成const char *types@"v@:"

    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
        NSString *methodName = NSStringFromSelector(aSelector);
        if ([methodName isEqualToString:@"sell"]) {
            return [NSMethodSignature signatureWithObjCTypes:"v@:"];
        }
        return [super methodSignatureForSelector:aSelector];
    }
    
    - (void)forwardInvocation:(NSInvocation *)anInvocation{
        SEL sel = [anInvocation selector];
        BookStore *bookStore = [BookStore new];
        if ([bookStore respondsToSelector:sel]) {
            [anInvocation invokeWithTarget:bookStore];
        } else {
            //走继承树
            [super forwardInvocation:anInvocation];
        }
    }
    

    可以看到消息转发成功了。

    2019-08-02 13:20:53.789565+0800 消息转发[4032:301841] 书店卖书了~
    

    到这里,有一个疑问。Runtime所有的工作都在运行期发生,那能不能在运行的时候动态添加方法呢?继续查看NSObject.h文件, 发现有一个+ (BOOL)resolveInstanceMethod:(SEL)sel;的方法,这个看样子就是动态解析实例方法。

    重写改方法,然后断点。

    image.png

    看来就是我们需要的方法,那这个方法里面都该实现点什么?查阅官方文档resolveInstanceMethod

    image.png

    这个太清晰了,照抄~ 哈哈哈。Book.m实现如下:

    void sell(id self, SEL _cmd){
         NSLog(@"Book把自己卖了~");
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)sel{
        NSString *methodName = NSStringFromSelector(sel);
        if ([methodName isEqualToString:@"sell"]) {
            class_addMethod(self, sel, (IMP)sell, "v@:");
            return YES;
        }
        //走继承树
        return [super resolveInstanceMethod:sel];
    }
    

    运行结果:

    2019-08-02 13:49:16.766800+0800 消息转发[6926:541823] Book把自己卖了~
    

    当一步步研究发现OC的消息转发实现方式后,接下来屡一下消息转发的顺序,图如下:

    image.png

    整理:
    1、动态消息转发resolveInstanceMethod:,动态添加一个方法实现;
    2、快速消息转发forwardingTargetForSelector:,转发给一个实现了方法的类对象;
    3、完整消息转发,首先先获取方法签名methodSignatureForSelector:然后在forwardInvocation:中设置消息转发的对象。

    • 完整实现代码如下:
    #import "Book.h"
    #import <objc/runtime.h>
    #import "BookStore.h"
    
    void sell(id self, SEL _cmd){
         NSLog(@"Book把自己卖了~");
    }
    
    @implementation Book
    
    + (BOOL)resolveInstanceMethod:(SEL)sel{
        NSString *methodName = NSStringFromSelector(sel);
        if ([methodName isEqualToString:@"sell"]) {
            class_addMethod(self, sel, (IMP)sell, "v@:");
            return YES;
        }
        //走继承树
        return [super resolveInstanceMethod:sel];
    }
    
    - (id)forwardingTargetForSelector:(SEL)aSelector {
        NSString *methodName = NSStringFromSelector(aSelector);
        if ([methodName isEqualToString:@"sell"]) {
            return [BookStore new];
        }
        return [super forwardingTargetForSelector:aSelector];
    }
    
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
        NSString *methodName = NSStringFromSelector(aSelector);
        if ([methodName isEqualToString:@"sell"]) {
            return [NSMethodSignature signatureWithObjCTypes:"v@:"];
        }
        return [super methodSignatureForSelector:aSelector];
    }
    
    - (void)forwardInvocation:(NSInvocation *)anInvocation{
        SEL sel = [anInvocation selector];
        BookStore *bookStore = [BookStore new];
        if ([bookStore respondsToSelector:sel]) {
            [anInvocation invokeWithTarget:bookStore];
        } else {
            //走继承树
            [super forwardInvocation:anInvocation];
        }
    }
    
    - (void)doesNotRecognizeSelector:(SEL)aSelector{
        NSLog(@"找不到方法实现:%@",NSStringFromSelector(aSelector));
    }
    
    @end
    

    3、消息转发的作用

    1>崩溃日志的搜集

    2>增强程序的健壮性

    3>实现多重代理

    利用消息转发机制可以无代码侵入的实现多重代理,让不同对象可以同时代理同个回调,然后在各自负责的区域进行相应的处理,降低了代码的耦合程度。

    https://blog.csdn.net/kingjxust/article/details/49559091

    结束语:Runtime慢慢开始研究了,这是Runtime的第一篇文章,尽我所能写出的东西不出错误,但是学习总有错的地方,有问题欢迎指出,感谢各位大佬。

    相关文章

      网友评论

          本文标题:iOS消息转发

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