iOS消息转发

笔者也是一名菜鸟,从头开始学习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的第一篇文章,尽我所能写出的东西不出错误,但是学习总有错的地方,有问题欢迎指出,感谢各位大佬。

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

推荐阅读更多精彩内容

  •   最近看了『神奇的 BlocksKit』系列,里面说到动态代理是BlocksKit的精华部分,对于使用block...
    foreverSun_122阅读 1,137评论 1 7
  • Objective-C 是一个动态语言,可以通过运行时系统来动态得创建类和对象、进行消息传递和转发。 在Objec...
    大卫石阅读 240评论 0 0
  • 消息转发(Message Forwarding)是在查找 IMP 失败后执行一系列转发流程的慢速通道,如果不作转发...
    二猪哥阅读 3,871评论 0 7
  • 前言我们在开发过程中,可能遇到服务端返回数据中有null的情况,当取到null值,并且对null发送消息的时候,就...
    Lucky_Man阅读 812评论 0 2
  • 级别: ★★☆☆☆标签:「iOS」「消息转发」「null([NSNull null])」作者: WYW[http...
    QiShare阅读 3,937评论 1 27