iOS消息转发研究---现在很多面试都会问到

. 前言

消息发送和转发流程可以概括为:

  • 消息发送(Messaging)是 Runtime 通过 selector 快速查找 IMP 的过程,有了函数指针就可以执行对应的方法实现;
  • 消息转发(Message Forwarding)是在查找 IMP 失败后执行一系列转发流程的慢速通道,如果不作转发处理,则会打日志和抛出异常。

**本文讲述消息发送和转发流程讲述原理能够很好地阅读本文的前提是你对 Objective-C Runtime 已经有一定的了解.

关于什么是消息,Class 的结构,selector、IMP、元类等概念将不再赘述**。本文用到的源码为 objc4-680 和 CF-1153.18,逆向 CoreFoundation.framework 的系统版本为 macOS 10.11.5,汇编语言架构为 x86_64。

1. 强大的 objc_msgSend

此函数是消息发送必经之路,但只要一提 objc_msgSend
,都会说它的伪代码如下或类似的逻辑,反正就是获取IMP 并调用:

id objc_msgSend(id self, SEL _cmd, ...) { 
    Class class = object_getClass(self); 
    IMP imp = class_getMethodImplementation(class, _cmd); 
    return imp ? imp(self, _cmd, ...) : 0;
 }

1.1 源码解析

为啥老用伪代码?因为 objc_msgSend是用汇编语言写的,针对不同架构有不同的实现。如下为 x86_64
架构下的源码,可以在 objc-msg-x86_64.s 文件中找到,关键代码如下:

       ENTRY    _objc_msgSend 
       MESSENGER_START 

       NilTest  NORMAL 

       GetIsaFast NORMAL // r11 = self->isa 
       CacheLookup NORMAL // calls IMP on success 

       NilTestSupport   NORMAL 

       GetIsaSupport     NORMAL

// cache miss: go search the method lists
LCacheMiss:   
       // isa still in r11 
       MethodTableLookup %a1, %a2 // r11 = IMP 
       cmp %r11, %r11 // set eq (nonstret) for forwarding 
       jmp  *%r11 // goto *imp 

       END_ENTRY    _objc_msgSend

这里面包含一些有意义的宏:

  • NilTest宏,判断被发送消息的对象是否为 nil的。如果为 nil,那就直接返回 nil。这就是为啥也可以对 nil发消息。
  • GetIsaFast宏, 可以『快速地』获取到对象的 isa指针地址(放到 r11寄存器,r10会被重写;在 arm 架构上是直接赋值到 r9)
  • CacheLookup这个宏是在类的缓存中查找 selector 对应的 IMP(放到 r10)并执行。如果缓存没中,那就得到 Class 的方法表中查找了。
  • MethodTableLookup宏是重点,负责在缓存没命中时在方法表中负责查找 IMP:
.macro MethodTableLookup 

       MESSENGER_END_SLOW  

       SaveRegisters 

       // _class_lookupMethodAndLoadCache3(receiver, selector, class) 

       movq $0, %a1 
       movq $1, %a2 
       movq %r11, %a3 
       call __class_lookupMethodAndLoadCache3 

       // IMP is now in %rax 
       movq %rax, %r11 

       RestoreRegisters
.endmacro

从上面的代码可以看出方法查找 IMP 的工作交给了 OC 中的 _class_lookupMethodAndLoadCache3函数,并将 IMP 返回(从 r11挪到 rax)。最后在 objc_msgSend中调用 IMP。

1.2 为什么使用汇编语言

其实在 objc-msg-x86_64.s 中包含了多个版本的 objc_msgSend方法,它们是根据返回值的类型和调用者的类型分别处理的:

  • objc_msgSendSuper:向父类发消息,返回值类型为 id
  • objc_msgSend_fpret:返回值类型为 floating-point,其中包含 objc_msgSend_fp2ret入口处理返回值类型为long double的情况
  • objc_msgSend_strut:返回值为结构体
    objc_msgSendSuper_strut:向父类发消息,返回值类型为结构体

1.当需要发送消息时,编译器会生成中间代码,根据情况分别调用 objc_msgSend, objc_msgSend_stret, objc_msgSendSuper, 或 objc_msgSendSuper_stret其中之一。

2.这也是为什么 objc_msgSend要用汇编语言而不是 OC、C 或 C++ 语言来实现,因为单独一个方法定义满足不了多种类型返回值,有的方法返回 id,有的返回 int
。考虑到不同类型参数返回值排列组合映射不同方法签名(method signature)的问题,那 switch 语句得老长了。。。

3.这些原因可以总结为 Calling Convention,也就是说函数调用者与被调用者必须约定好参数与返回值在不同架构处理器上的存取规则,比如参数是以何种顺序存储在栈上,或是存储在哪些寄存器上。除此之外还有其他原因,比如其可变参数用汇编处理起来最方便,因为找到 IMP 地址后参数都在栈上。要是用 C++ 传递可变参数那就悲剧了,prologue 机制会弄乱地址(比如 i386 上为了存储 ebp 向后移位 4byte),最后还要用 epilogue 打扫战场。而且汇编程序执行效率高,在 Objective-C Runtime 中调用频率较高的函数好多都用汇编写的。写在结构体里, 常用的方法列表里

4.总结:

  • 效率高,调用频率较高的函数方法封装到结构体中,大约20%经常调用, 不用多次判断去找
  • 返回值和参数可以多个, 且类型不同

2. 使用 lookUpImpOrForward 快速查找 IMP

上一节中说到的 _class_lookupMethodAndLoadCache3
函数其实只是简单的调用了 lookUpImpOrForward
函数:

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{ 
     return lookUpImpOrForward(cls, sel, obj,  
              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

注意 lookUpImpOrForward调用时使用缓存参数传入为 NO,因为之前已经尝试过查找缓存了。
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver)
实现了一套查找 IMP 的标准路径,也就是在消息转发(Forward)之前的逻辑。

2.1 优化缓存查找&类的初始化

先对 debug 模式下的 assert 进行 unlock:

runtimeLock.assertUnlocked();

RuntimeLock本质上是对 Darwin 提供的线程读写锁 pthread_rwlock_t的一层封装,提供了一些便捷的方法。lookUpImpOrForward接着做了如下两件事:

1、如果使用缓存(cache参数为 YES),那就调用 cache_getImp方法从缓存查找 IMP。
cache_getImp是用汇编语言写的,也可以在 objc-msg-x86_64.s 找到,其依然用了之前说过的 CacheLookup宏。因为_class_lookupMethodAndLoadCache3调用 lookUpImpOrForward时 cache参数为 NO,这步直接略过

2、如果是第一次用到这个类且 initialize(印尼手莱斯)参数为 YES(initialize && !cls->isInitialized()
),需要进行初始化工作,也就是开辟一个用于读写数据的空间。
先对 runtimeLock写操作加锁,然后调用 cls的 initialize方法。如果 sel == initialize也没关系,虽然 initialize还会被调用一次,但不会起作用啦,因为cls->isInitialized() 已经是 YES啦。

2.2 继续在类的继承体系中查找

考虑到运行时类中的方法可能会增加,需要先做读操作加锁,使得方法查找和缓存填充成为原子操作。添加 category 会刷新缓存,之后如果旧数据又被重填到缓存中,category 添加操作就会被忽略掉。

runtimeLock.read();

之后的逻辑整理如下:

  • 1、如果 selector 是需要被忽略的垃圾回收用到的方法,则将 IMP 结果设为 _objc_ignored_method,这是个汇编程序入口,可以理解为一个标记。对此种情况进行缓存填充操作后,跳到第 7 步;否则执行下一步。
  • 2、查找当前类中的缓存,跟之前一样,使用 cache_getImp
    汇编程序入口。如果命中缓存获取到了 IMP,则直接跳到第 7 步;否则执行下一步。
  • 3、在当前类中的方法列表(method list)中进行查找,也就是根据 selector 查找到 Method 后,获取 Method 中的 IMP(也就是 method_imp属性),并填充到缓存中。查找过程比较复杂,会针对已经排序的列表使用二分法查找,未排序的列表则是线性遍历。如果成功查找到 Method 对象,就直接跳到第 7 步;否则执行下一步。
  • 4、在继承层级中递归向父类中查找,情况跟上一步类似,也是先查找缓存,缓存没中就查找方法列表。这里跟上一步不同的地方在于缓存策略,有个 _objc_msgForward_impcache汇编程序入口作为缓存中消息转发的标记。也就是说如果在缓存中找到了 IMP,但如果发现其内容是 _objc_msgForward_impcache,那就终止在类的继承层级中递归查找,进入下一步;否则跳到第 7 步。
  • 5、当传入 lookUpImpOrForward的参数 resolver为 YES
    并且是第一次进入第 5 步时,进入动态方法解析;否则进入下一步。这步消息转发前的最后一次机会。
    此时释放读入锁(runtimeLock.unlockRead()),接着间接地发送+resolveInstanceMethod或 +resolveClassMethod
    消息。这相当于告诉程序员『赶紧用 Runtime 给类里这个 selector 弄个对应的 IMP 吧』,因为此时锁已经 unlock 了,所以不会缓存结果,甚至还需要软性地处理缓存过期问题可能带来的错误。这里的业务逻辑稍微复杂些,后面会总结。因为这些工作都是在非线程安全下进行的,完成后需要回到第 1 步再次查找 IMP。
  • 6、 此时不仅没查找到 IMP,动态方法解析也不奏效,只能将 _objc_msgForward_impcache当做 IMP 并写入缓存。这也就是之前第 4 步中为何查找到 _objc_msgForward_impcache就表明了要进入消息转发了。
  • 7、读操作解锁,并将之前找到的 IMP 返回。(无论是正经 IMP 还是不正经的 _objc_msgForward_impcache
    )这步还偏执地做了一些脑洞略大的 assert,很有趣。

对于第 5 步,其实是直接调用 _class_resolveMethod函数,在这个函数中实现了复杂的方法解析逻辑。
1)如果 cls是元类,则会发送 +resolveClassMethod,然后根据lookUpImpOrNil(cls, sel, inst, NO/initialize/, YES/cache/, NO/resolver/)函数的结果来判断是否发送 +resolveInstanceMethod;

  1. 如果不是元类,则只需要发送 +resolveInstanceMethod
    消息。这里调用+resolveInstanceMethod或 +resolveClassMethod时再次用到了 objc_msgSend,而且第三个参数正是传入lookUpImpOrForward的那个 sel。在发送方法解析消息之后还会调用lookUpImpOrNil(cls, sel, inst, NO/initialize/, YES/cache/, NO/resolver/)来判断是否已经添加上sel对应的 IMP 了,打印出结果。

3)最后 lookUpImpOrForward方法也会把真正的 IMP 或者需要消息转发的 _objc_msgForward_impcache返回,并最终传递到 objc_msgSend中。
而_objc_msgForward_impcache会在转化成 _objc_msgForward或_objc_msgForward_stret。这个后面会讲解原理。

2.3 回顾 objc_msgSend 伪代码

回过头来会发现 objc_msgSend的伪代码描述得很传神啊,因为class_getMethodImplementation的实现如下:

IMP class_getMethodImplementation(Class cls, SEL sel)
{ 
    IMP imp; 
    if (!cls || !sel) return nil; 
    imp = lookUpImpOrNil(cls, sel, nil, YES/*initialize*/, YES/*cache*/, YES/*resolver*/); 
    // Translate forwarding function to C-callable external version 
    if (!imp) { 
        return _objc_msgForward; 
    } 
        return imp;
}

lookUpImpOrNil函数获取不到 IMP 时就返回 _objc_msgForward,后面会讲到它。
lookUpImpOrNil跟lookUpImpOrForward的功能很相似, 只是将 lookUpImpOrForward实现中的 _objc_msgForward_impcache替换成了 nil:

IMP lookUpImpOrNil(Class cls, SEL sel, id inst,  bool initialize, bool cache, bool resolver)
{ 
    IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver); 
    if (imp == _objc_msgForward_impcache) return nil; 

    else return imp;
}

lookUpImpOrNil方法可以查找到 selector 对应的 IMP 或是 nil,
所以如果不考虑返回值类型为结构体的情况,用那几行伪代码来表示复杂的汇编实现还是挺恰当的。

3. forwarding中复杂的消息转发

3.1 objc_msgForward_impcache 的转换

_objc_msgForward_impcache只是个内部的函数指针,只存储于上节提到的类的方法缓存中,需要被转化为_objc_msgForward和 _objc_msgForward_stret才能被外部调用。但在 Mac OS X macOS 10.6 及更早版本的 libobjc.A.dylib 中是不能直接调用的,况且我们根本不会直接用到它。带 stret后缀的函数依旧是返回值为结构体的版本。

上一节最后讲到如果没找到 IMP,就会将 _objc_msgForward_impcache返回到 objc_msgSend函数,而正是因为它是用汇编语言写的,所以将内部使用的 _objc_msgForward_impcache转化成外部可调用的 _objc_msgForward或_objc_msgForward_stret也是由汇编代码来完成。
实现原理很简单,就是增加个静态入口__objc_msgForward_impcache,然后根据此时 CPU 的状态寄存器的内容来决定转换成哪个。如果是 NE(Not Equal) 则转换成 _objc_msgForward_stret,反之是 EQ(Equal) 则转换成 _objc_msgForward:

jne    __objc_msgForward_stret
jmp    __objc_msgForward

为何根据状态寄存器的值来判断转换成哪个函数指针呢?回过头来看看 objc_msgSend中调用完 MethodTableLookup之后干了什么:

MethodTableLookup %a1, %a2  // r11 = IMP
cmp %r11, %r11      // set eq (nonstret) for forwarding
jmp *%r11           // goto *imp

再看看返回值为结构体的 objc_msgSend_stret
这里的逻辑:

MethodTableLookup %a2, %a3 // r11 = IMP
test %r11, %r11 // set ne (stret) for forward; r11!=0
jmp *%r11 // goto *imp

稍微懂汇编的人一眼就看明白了,不懂的看注释也懂。现在总算是把消息转发前的逻辑绕回来构成闭环了。

上一节中提到 class_getMethodImplementation函数的实现,在查找不到 IMP 时返回 _objc_msgForward,而_objc_msgForward_stret正好对应着 class_getMethodImplementation_stret:

IMP class_getMethodImplementation_stret(Class cls, SEL sel)
{ 
    IMP imp = class_getMethodImplementation(cls, sel); 
    // Translate forwarding function to struct-returning version 
    if (imp == (IMP)&_objc_msgForward /* not _internal! */) { 
         return (IMP)&_objc_msgForward_stret; 
    } 
    return imp;
}

也就是说 _objc_msgForward*系列本质都是函数指针,都用汇编语言实现,都可以与 IMP 类型的值作比较。
_objc_msgForward和 _objc_msgForward_stret声明在 message.h 文件中。_objc_msgForward_impcache在早期版本的 Runtime 中叫做 _objc_msgForward_internal。

3.2 objc_msgForward 也只是个入口

从汇编源码可以很容易看出_objc_msgForward和 _objc_msgForward_stret
会分别调用 _objc_forward_handler和 _objc_forward_handler_stret:

ENTRY __objc_msgForward
// Non-stret version

movq __objc_forward_handler(%rip), %r11
jmp *%r11

END_ENTRY     __objc_msgForward

ENTRY         __objc_msgForward_stret
// Struct-return version

movq __objc_forward_stret_handler(%rip), %r11
jmp *%r11
END_ENTRY __objc_msgForward_stret

这两个 handler 函数的区别从字面上就能看出来,不再赘述。
也就是说,消息转发过程是现将 _objc_msgForward_impcache强转成 _objc_msgForward或_objc_msgForward_stret
, 再分别调用 _objc_forward_handler或 _objc_forward_handler_stret。

3.3 objc_setForwardHandler 设置了消息转发的回调

在 Objective-C 2.0 之前,默认的 _objc_forward_handler或 _objc_forward_handler_stret都是 nil,而新版本的默认实现是这样的:

// Default forward handler halts the process.
__attribute__((noreturn)) void         
objc_defaultForwardHandler(id self, SEL sel)
{ 
    _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p " "(no message forward handler is installed)",  
                class_isMetaClass(object_getClass(self)) ? '+' : '-',  
                object_getClassName(self), sel_getName(sel), self);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

#if SUPPORT_STRET
struct stret { int i[100]; };
__attribute__((noreturn)) struct stret 
objc_defaultForwardStretHandler(id self, SEL sel)
{ 
    objc_defaultForwardHandler(self, sel);
}
void *_objc_forward_stret_handler = (void*)objc_defaultForwardStretHandler;
#endif

objc_defaultForwardHandler中的 _objc_fatal作用就是打日志并调用 _builtin_trap()触发 crash,可以看到我们最熟悉的那句 "unrecognized selector sent to instance” 日志。
_builtin_trap()在杀掉进程的同时还能生成日志,比调用 exit()更好。
objc_defaultForwardStretHandler就是装模作样搞个形式主义,把objc_defaultForwardHandler包了一层。attribute((noreturn))属性通知编译器函数从不返回值,当遇到类似函数需要返回值而却不可能运行到返回值处就已经退出来的情况,该属性可以避免出现错误信息。这里正适合此属性,因为要求返回结构体哒。

因为默认的 Handler 干的事儿就是打日志触发 crash,我们想要实现消息转发,就需要替换掉 Handler 并赋值给_objc_forward_handler或 _objc_forward_handler_stret
,赋值的过程就需要用到 objc_setForwardHandler函数,实现也是简单粗暴,就是赋值啊:

void objc_setForwardHandler(void *fwd, void *fwd_stret)
{ 
    _objc_forward_handler = fwd;
#if SUPPORT_STRET 
    _objc_forward_stret_handler = fwd_stret;
#endif
}

大神博客
http://yulingtianxia.com/blog/2016/06/15/Objective-C-Message-Sending-and-Forwarding/

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

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

上图实线是 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

发现第一个断点进入了,说明这个是消息转发中一个步骤。之后我们查阅了官方文档,
- (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

接着又报了-[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

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

image

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

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

image

这个太清晰了,照抄~ 哈哈哈。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

整理:
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的第一篇文章,尽我所能写出的东西不出错误,但是学习总有错的地方,有问题欢迎指出,感谢各位大佬。

来自:有梦想的程序员
链接://www.greatytc.com/p/e552d0897ed1

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