iOS消息转发

2019-08-04  本文已影响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__有关系。我们可以试试,到底有没有关系?

发现第一个断点进入了,说明这个是消息转发中一个步骤。之后我们查阅了官方文档,
- (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的第一篇文章,尽我所能写出的东西不出错误,但是学习总有错的地方,有问题欢迎指出,感谢各位大佬。

上一篇下一篇

猜你喜欢

热点阅读