iOS 从runtime理解消息发送

什么是runtime

runtime就是运行时,在实际开发中使用runtime的场景并不多,但是了解runtime有助于我们更好的理解OC的原理,从而提高开发水平。
我们都知道高级编程语言想要成为可执行文件需要先编译为汇编语言再汇编为机器语言,机器语言也是计算机能够识别的唯一语言,但是OC并不能直接编译为汇编语言,而是要先转写为纯C语言再进行编译和汇编的操作,从OC到C语言的过渡就是由runtime来实现的。然而我们使用OC进行面向对象开发,而C语言更多的是面向过程开发,这就需要将面向对象的类转变为面向过程的结构体,本文正是通过runtime源码分析来讲解runtime是如何将面向对象的类转变为面向过程的结构体。

消息发送

众所周知OC是一门动态语言,在编译期应用根本不知道什么对象该做什么事情,那我们的程序到底是怎么执行相关操作的呢?那我们就必须了解Runtime这个库了,Runtime把对象需要做的事情放到运行时期来处理。
消息发送主要就是objc_msgSend这个方法,任何方法的调用最终都会转换为objc_msgSend方法进行处理。

    id objc_msgSend(id self, SEL op, ...);

在上面的代码中我们可以看到objc_msgSend方法有3个参数,self :是消息的接受者 op : 是消息的方法名字 最后一个是参数列表。

[cell setListData:model];

cell是消息的执行者,setListData是选择子,model是参数。
选择子和参数组合起来就是消息。

typedef struct objc_class *Class;     //类,结构体指针,实际也是对象

/// Represents an instance of a class.
struct objc_object {    //实例对象的isa指针指向类对象,而类对象的isa指向元类
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;    //对象,结构体指针

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;     //isa指针

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;  // 父类
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;  // 类名
    long version                                             OBJC2_UNAVAILABLE; // 类的版本信息,默认为0
    long info                                                OBJC2_UNAVAILABLE; // 类信息,供运行期使用的一些位标识
    long instance_size                                       OBJC2_UNAVAILABLE;  // 该类的实例变量大小
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;  // 该类的成员变量链表
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;   // 方法定义的链表
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;   // 缓存方法列表
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;  // 协议列表
#endif
} OBJC2_UNAVAILABLE;

的源码中我们可以发现,对象的本质就是一个结构体指针,它的isa指针指向类对象,类对象中包括方法列表,属性列表等等,每个对象都会有一个isa指针,类对象的isa指向元类,元类里存放类方法列表。

当发送消息给实例对象时,消息是在寻找这个对象的类的方法列表(实例方法)。
当发送消息给类对象时,消息是在寻找这个类的元类的方法列表(类方法)。


屏幕快照 2019-06-20 15.57.12.png

OC方法会被Runtime转换为objc_msgSend函数,然后根据接受者和消息来调用适当的方法。每个方法里都有2个属性,SEL是方法的编号,IMP是方法地址。runtime的黑魔法就是改变了IMP方法编号的指向。

消息传递流程

  • 对象会根据isa指针找到这个对象自己所属的类,用消息里的选择子名称在所属类的方法列表中依次遍历,如果找到相符合的方法名称,就会根据IMP指针跳转到方法的实现代码进而执行代码逻辑,如果找不到相符的名称,就会沿着继承体系向上查找,如果在根类还没有查找到该方法,此时就会执行消息转发的操作。
  • 每次的方法执行都会在底层运行很多的步骤,方法的查询是非常耗时间和性能的,因此苹果提供了一个缓存机制。当一个类的方法别调用后,系统就会将这个方法放入该类的方法类表中,方便下次调用大大缩短了查找时间。

方法缓存机制是用散列表来实现的,这样可以提高方法的查找速度。

散列表的原理

  • 初始时, 为对象的cach_t分配一个空间, 值为NULL。
  • 调用方法时, 为对象发送一个 SEL 消息, 如 @selector(est), 将这个方法缓存。
  • 系统用 SEL 与 _mask 作按位与计算: @selector(est) & _mask
  • 根据按位与得到的结果去检查对应索引的空间是否为NULL , 如果为NULL 就将这个bucket_t 缓存在索引2对应空间
  • 如果不为空, 索引减1, 再检查是否为NULL, 依次类推. 如果索引<0, 则使索引 =_mask - 1, 直至找到索引对应空间为NULL, 再缓存

_mask 是缓存结构体中的属性,是散列表的长度减1。

系统为什么要把方法编号与_mask进行与运算呢?

与运算符 &
只有对应的二个二进位都为1时,结果位才是1
10010 & 00010 = 00010

由此可以知道得到的数不会大于_mask,也就不会超出分配的空间,如果分配的空间不够,散列表会自动扩容,空间扩大2倍,并且散列表清空。

iOS方法查询是怎样实现的呢?

  • 向对象发送一个消息时,例如:@selector(eat)。
  • 系统会用eat方法里的SEL(方法编号)与_mask作与计算。
  • 把按位与得到的结果当做散列表的索引, 判断其中的 SEL 是否与传过来的 SEL 相同, 如果相同, 这个_imp就是寻找的方法。
  • 如果不相同, 索引减1, 再比较SEL, 依次类推. 如果索引<0, 则使索引 = _mask - 1, 直至找到_imp。

你可能感兴趣的:(iOS 从runtime理解消息发送)