美文网首页
RunTime之二:消息传递机制

RunTime之二:消息传递机制

作者: 双鱼子曰1987 | 来源:发表于2020-11-06 17:27 被阅读0次

提醒,阅读本章,需要首先了解上一章节的基础知识。

一、「消息传递」的疑问

面向对象拥有强大的「数据封装」能力,而「消息传递」更是其强大利器之一,那么RunTime是如何实现的呢?

所谓的「消息传递」指的就是「方法的调用」,其中「方法的调用」又可以分为「实例方法」调用 和 「类方法」调用。

其中,「方法的调用」可以分为两个步骤来看,一个根据「方法名」找到「实现」;一个是执行方法的「实现」代码。

例如,定义一个Person类,继承NSObject。
@interface Person : NSObject
- (void)sayHelloWorld;
@end
@implementation ZTwinkleLoadView
- (void)sayHelloWorld {
     NSLog(@"HelloWorld!");
}
@end

// 调用实例方法
Person *obj = [[Person alloc] init];
[obj sayHelloWorld]

// 调用类方法
[Person hash] 
  • 疑问一:调用[obj sayHelloWorld]实例方法的时候,Runtime是如何找到sayHelloWorld方法的呢?
  • 疑问二:调用[Person hash]类方法的时候,Runtime是如何找到hash方法的呢?

二、「方法」的「查找」过程

「方法调用」的本质:「消息接收者」 找到对应的「方法」SEL 的 「实现」IMP

这其实就是一个寻找搜索的过程,而在链表寻找,往往都是不那么快,这也是结构体struct objc_class 需要cache的原因。

2.1、「实例方法」的「查找」过程

  • 1、「对象实例」 通过struct objc_object 里面的 isa 指针找到对应的类Class
  • 2、首先,会在「类」的结构体struct objc_class 中的 cache(方法缓存) 的散列表中寻找对应的 IMP(方法实现);
  • 3、如果缓存中没有找到,会继续在 struct objc_class 中的 methodLists 中查找。如果找到,保存到缓存中,并返回;
  • 4、如果上一步在 「类」 中没有找到,会通过struct objc_class 中的 superClass指针找到其父类,然后在父类中method_list 中查找;一直往下,直到基类。
  • 5、上面步骤中,一旦找到「方法,SEL」的「实现,IMP」,就会立即缓存,并且执行objc_msgSend函数,执行「方法」的「实现」。
  • 6、假设上面步骤中,都没有找到「方法」的「实现」,消息将会被转发,进入「消息异常捕获逻辑」。
方法的查找过程
2.2、「类方法」的「查找」过程

其查找的过程 和 「实例方法」的「查找」过程 的过程类似,不一样的是寻找的是沿着「元类」的路径。

其大致的过程如下:

  • 1、通过「类对象」struct objc_classisa 指针 找到所属的 「Meta Class(元类)」;
  • 2、剩下的「类方法」查找过程 和 「类方法」的执行,同实例方法一样。如上所示。
  • 3、如果没有查找到,也会进入到「消息异常捕获逻辑」。

2.3、解答疑问:

  • 疑问一:调用[obj sayHelloWorld]实例方法的时候,Runtime是如何找到sayHelloWorld方法的呢?
    调用[obj sayHelloWorld]实例方法的时候,obj首先查找缓存cache,找到执行;否则在methodLists链表找到sayHelloWorld方法的实现,然后保存到cache中,然后返回。
    如果是调用[obj copy] 会在父类NSObject中找到该方法。
    如果是调用[obj xxx]未被定义的方法,会进入到异常捕获流程。

  • 疑问二:调用[Person hash]类方法的时候,Runtime是如何找到hash方法的呢?
    调用[Person hash]类方法的时候,会先找到Person的「元类」,然后在元类的缓存中找,没找到,在方法列表中找;如果还是没有找到,会通过superClass指针找到父类NSObject,然后重复查找过程,知道找到返回,并且执行。

2.4、若干问题

  • 关于「MetaClass,元类」
    每一个类都有对应的元类,元类的结构体定义同objc_class一样。「元类」对于开发中来说一般是不可见,也不建议去操作它。

  • 一个问题,「元类」的定义同struct objc_class一样,那么「元类」也有isa指针,它又指向哪里呢?
    为了不让这种结构无限延伸下去, Objective-C 的设计者让所有的「元类」的isa指向「基类」(比如 NSObject )的「元类」。而「基类」的「元类」的isa` 指向自己。这样就形成了一个的闭环。

  • 一个问题,「基类」也有super_class指针,它又指向哪里呢?
    我们知道,子类通过super_class指向父类,依次往下,直到「基类」的 super_class指向 nil 。这样就形成了一个的闭环。

OC中类的继承链,也是通过super_class实现的。由于objc_class只有一个superClass指针,因此它也只能支持单继承。

可以推断,如果多继承,那么这个链路就会显得太过于复杂;另一方面,多继承在实际开发中并不常见,而且推荐少用或者不用,或者可以通过其他方式(例如接口)实现设计意图,因此设计者舍弃。

2.5、一张图说明「实例方法」和「类方法」的查找过程

对象和类的isa指针的指向关系
注意图中,实线表示superClass的查找过程,虚线表示isa指针的查找过程。

三、「方法」的「执行」过程,即objc_msgSend

所有 Objective-C 方法调用在编译时都会转化为对 C 函数 objc_msgSend 的调用。

objc_msgSend函数的定义:id objc_msgSend(id receiver, SEL op, ...),前面两个参数固定,第一个参数是接收者id,第二个参数方法名SEL,后面的...可变参数列表,传递函数的参数。

objc_msgSend函数的定义和 IMP = id + SEL + ...(即参数列表) 的定义是一模一样的。因此,查找到方法的具体「实现」,找到是IMP执行,具体执行代码的是objc_msgSend函数。

至于objc_msgSend函数的内部代码实现,有兴趣的同学可以深入学习下。深入解构objc_msgSend函数的实现

四、方法找不到的异常处理

现实开发中,经常会遇到一种Crash,就是“unrecognized selector sent to instance ,意思是某个实例或者类找不到某个方法名”,这背后的处理机制如何呢?有没有避免Crash的统一处理机制呢?带着问题,继续往下看。

从上面关于「实例方法」和「类方法」的查找过程中,它们最后都有个异常处理逻辑,就是当方法找不到的时候进入「消息异常捕获逻辑」或者「消息的转发机制」。

4.1、「消息的转发机制」的原理图

这个「消息异常捕获逻辑」内部机制又是怎么样的呢?
OC设计一套异常的处理机制,即「消息的转发机制」,当某个方法名找不到对应的「实现」的时候,给你个机会处理这种异常。这种能力就为我们提供 “方法找不到Crash” 的统一拦截处理方案。

4.2、「消息的转发机制」可以分为三个步骤:

  • 1、「消息动态解析」
    对应有两个方法捕获,分别是,「类方法」+ (BOOL)resolveClassMethod:(SEL)sel、「实例方法」+ (BOOL)resolveInstanceMethod:(SEL)sel
    通常的做法是「动态添加一个方法」,并返回 YES 告诉程序已经成功处理消息。如果这两个方法返回 NO ,这个流程会继续往下走。「动态添加方法」一般调用BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);函数来实现。
// 重写 resolveInstanceMethod: 添加对象方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(xxx_unrecognized_func)) { 
        class_addMethod([self class], sel, (IMP) xxx_unrecognized_func, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

void xxx_unrecognized_func(id obj, SEL _cmd) {
    ...
}

上面的例子中,class_addMethod 的特殊参数 v@: 称为 Type Encodings ,其具体说请跳转 传送门

  • 2、「消息接收者的重定向」
    在第一步的时候,resolveInstanceMethod:没有添加其他函数实现,运行时就会进行下一步,进行「消息接收者的重定向」。对应的方法是:- (id)forwardingTargetForSelector:(SEL)aSelector
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return NO; 

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(xxx_unrecognized_func)) {
        return [[xxx_Class alloc] init];
    }
    
    return [super forwardingTargetForSelector:aSelector];
}
  • 3、「消息重定向」
    如果错过了「消息动态解析」、「消息接收者重定向」两次机会后,NSObject提供最后一次机会「消息重定向」。

    其大致的过程如下:
    a、提供「函数签名」即NSMethodSignature对象,「函数签名」 = 函数的参数 + 返回值类型。Runtime会通过以下方法获取「函数签名」
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
    + (NSMethodSignature *)instanceMethodSignatureForSelector:(SEL)aSelector

    b、如果 methodSignatureForSelector: 返回合法的 NSMethodSignature对象;Runtime会创建 「调用者」即NSInvocation对象,然后并通过 forwardInvocation: 消息通知当前对象,方法里面指定某个对象来处理某个方法。
    对应方法:- (void)forwardInvocation:(NSInvocation *)anInvocation

    c、如果 methodSignatureForSelector: 返回nil,则Runtime 系统会发出 doesNotRecognizeSelector: 消息,程序也就崩溃了。
    对应方法:- (void)doesNotRecognizeSelector:(SEL)aSelector;


+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return NO;
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    return nil;
}


- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if ([NSStringFromSelector(aSelector) isEqualToString:@"xxx_unrecognized_func"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = anInvocation.selector;
    
    xxx_Class *obj = [[xxx_Class alloc] init];
    if([obj respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:obj];
    } else {
        [self doesNotRecognizeSelector:sel];
    }
}

4.3、注意

异常解析的方法都被定义在NSObject根类中,由于OC所有的类都是继承来自NSObject,因此所有的类或者类的实例都可以处理异常消息。

4.4、利用NSInvocation实现动态方法调用

即随意指定某个对象,执行某个方法。如下

NSMethodSignature *sign = [UIView.class instanceMethodSignatureForSelector:@selector(setBackgroundColor:)];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sign];

[invocation invokeWithTarget:viewObj_1];
[invocation invokeWithTarget:viewObj_2];
[invocation invokeWithTarget:viewObj_3];

其他

RunTime原理之一:如何让Objective-C支持面向对象的能力呢?
Runtime:消息发送机制、动态方法解析、消息转发机制
iOS 开发:『Runtime』详解(一)基础知识

相关文章

  • Runtime 的应用

    前面我们说到:Runtime 消息传递机制Runtime 消息转发机制Runtime 交换方法今天我们来谈谈Run...

  • RunTime之二:消息传递机制

    提醒,阅读本章,需要首先了解上一章节的基础知识。 一、「消息传递」的疑问 面向对象拥有强大的「数据封装」能力,而「...

  • iOS - Runtime - 概念和方法交换

    runtime的概述runtime的相关概念runtime消息机制消息传递动态方法解析消息转发runtime的作用...

  • iOS消息转发机制

    消息转发机制: 消息转发机制是相对于消息传递机制而言的。 1、消息(传递)机制 RunTime简称运行时。就是系统...

  • Runtime 消息传递机制

    求婚时,想给女友买颗好钻戒,那颗他曾参与撰写文案的优雅钻戒,承载着他的所有想象和创造力,很多人被他的文案打动,而他...

  • Runtime消息传递机制

    先看下整个Class的底层结构 objc_msgSend的源码是汇编实现的 objc_msgSend的执行流程01...

  • iOS之Runtime(二)消息机制(传递和转发)

    Runtime之二 消息机制(传递和转发) 先看一个栗子 上述代码在编译期没有任何问题,因为id类型可以指向任何类...

  • Objective-C Runtime浅析

    前言 Runtime是什么 Runtime的实现原理消息传递机制Runtime基础数据结构NSObject & i...

  • iOS 底层 - 名词解析

    目录 前言 名词解析 OC消息传递和转发机制 Runtime runtime动态创建类 Runloop Metho...

  • Runtime(一)消息传递机制

    Introduction The Objective-C language defers as many deci...

网友评论

      本文标题:RunTime之二:消息传递机制

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