深入理解 iOS Runtime

Posted 程序员大咖

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入理解 iOS Runtime相关的知识,希望对你有一定的参考价值。

👇👇关注后回复 “进群” ,拉你进程序员交流群👇👇

了解Runtime有助于我们理解Objective-C运行时系统的工作原理以及如何利用它。本章将介绍NSObject类以及Objective-C程序如何与运行时系统进行交互,如何在运行时查找对象的信息,如何将消息转发给其他对象。

// 检查对象在继承层级结构的位置;
- (BOOL)isKindOfClass:(Class)aClass;
- (BOOL)isMemberOfClass:(Class)aClass;

// 指示对象是否声称要实现特定协议中定义的方法
- (BOOL)conformsToProtocol:(Protocol *)aProtocol;

// 表示对象是否可以接受特定消息 (检查是否实现某个方法)
- (BOOL)respondsToSelector:(SEL)aSelector;

// 方法实现的地址
- (IMP)methodForSelector:(SEL)aSelector;

运行时函数

运行时系统是一个共享的动态库,公共的接口在目录下/usr/include/objc。其中包含了一些函数和数据结构,许多函数可以使用纯C来复制编译器在编写 Objective-C 代码时所做的事情。这其实也就是我们常常看到的,使用某些运行时函数可以达到可以NSObject方法一样的效果,其实也正是这些底层的函数构成了NSObject的基础功能。这里是官方文档Objective-C 运行时参考[1]

现在,我们知道了运行时交互有哪些,那么接下来,我们再看看Objective-C中的一些基本概念:类、对象、Method、SEL、IMP。熟悉这些概念之后,会更加理解运行时做了哪些事。

类、对象、Method、SEL、IMP

类对象(Class)是由程序设置后在运行时由编译器创建的,当一个对象的实例方法被调用时,会通过isa找到这个类,然后在该类中方法列表中查找。

// Class 定义
typedef struct objc_class *Class;
// 类结构体
struct objc_class 
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    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;

结构体里有指向父类的指针、类名、版本、实例大小、实例变量列表、方法列表、缓存、遵守的协议列表等。

那疑问来了,请问类方法是存在哪里的?我们在调用类方法的时候,我们如何去找呢?这里就引入一个概念:元类(meta-class)。

元类就是类对象的isa指向的类,也可以说是类对象的类

对象

实例对象(Object)是我们对类对象alloc或者new操作时所创建的,在这个过程中会拷贝实例所属的类的成员变量,但并不拷贝类定义的方法。调用实例方法时,系统会根据实例的isa指针去类的方法列表及父类的方法列表中寻找与消息对应的selector指向的方法

// 对象结构体
struct objc_object 
    Class _Nonnull isa;     OBJC_ISA_AVAILABILITY;
;

由此,我们得出了一个结论,类对象和实例对象的查找机制是一样的:

  • 对象的实例方法调用时,通过对象的 isa 在类中获取方法的实现。

  • 类对象的类方法调用时,通过类的 isa 在元类中获取方法的实现。

对应关系如下图,描述了对象,类,元类之间的关系:

图中实线是 super_class指针,虚线是isa指针。

  • Root class (class)就是NSObject,NSObject没有超类,所以Root class(class)的superclass指向nil。

  • 每个Class都有一个isa指向唯一的Meta class

  • Root class(meta)的superclass指向Root class(class),也就是NSObject,形成一个回路。

  • 每个Meta class的isa都指向Root class (meta)。

Method

Method就是我们平时所说的函数,它表示的是能够独立完成一个功能的一段代码;Method通过selectorIMP两个属性,实现了快速查询方法及实现,相对提高了性能,又保持了灵活性。

typedef struct objc_method *Method;
struct objc_method 
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp

SEL

SEL是方法选择器, 常见的写法有:@selector()

/// 代表一个方法的不透明类型
typedef struct objc_selector *SEL;
复制代码
声明方式:
SEL s1 = @selector(test1);
SEL s2 = NSSelectorFromString(@"test2");

IMP

IMP是指向最终实现程序的内存地址的指针,下面是它的定义:

/// 指向一个方法实现的指针
typedef id (*IMP)(id, SEL, ...); 
#endif

理解了前面的这些概念之后,接下来我们进入正题。

在 Objective-C 中,消息直到运行时才绑定到方法实现。编译器转换消息表达式,调用objc_msgSend[2]方法。

消息发送

Objective-C 中所有方法的调用/类的生成都在运行时进行,我们可以通过类名/方法名反射得到相应的类和方法,也可以替换某个类的方法为新的实现,理论上你可以在运行时通过类名/方法名调用到任何 Objective-C 方法,替换任何类的实现以及新增任意类。

比方说我们写一个调用方法[receiver message],那这个方法会被编译器转化成:

// 第一个参数类型是发送者, 第二个参数类型是SEL。SEL在OC中是selector方法选择器
id objc_msgSend ( id _Nullable self, SEL op, ... );

不同类中相同名字的方法所对应的方法选择器是相同的,即使方法名字相同而变量类型不同也会导致它们具有相同的方法选择器。由于这点特性,也导致了Objective-C不支持函数重载。

实际上,我们在调用的方法的过程,其实在Runtime中就是消息发送。

objc_msgSend的实现是由汇编语言实现,根据CPU架构实现的过程各不相同,如果想阅读相关的代码要有一定的汇编基础;

objc_msgSend会做以下几件事情:

  • 1.检测这个 selector是不是要忽略

  • 2.检查target是不是为nil

    • 如果这里有相应的nil的处理函数,就跳转到相应的函数中

    • 如果没有处理nil的函数,就自动清理并返回。这一点就是为何在Objective-C中给nil发送消息不会崩溃的原因

  • 3.确定不是给nil发消息之后,在该class的缓存中查找方法对应的IMP实现

    • 如果找到,就跳转进去执行

    • 如果没有找到,就在方法列表里面继续查找,一直找到NSObject为止

  • 4.如果还没有找到,那就需要开始消息转发阶段了。至此,发送消息Messaging阶段完成。这一阶段主要完成的是通过select()快速查找IMP的过程

消息传递框架:

为了加快消息传递过程,运行时系统会在使用方法时缓存方法的选择器和地址。每个类都有一个单独的缓存,它可以包含继承方法以及类中定义的方法的选择器。在消息传递过程中,会首先检查接收对象类的缓存。

消息转发

转发阶段,会调用_objc_msgForward(id self, SEL _cmd,...)方法

_objc_msgForward(id _Nonnull receiver, SEL _Nonnull sel, ...)

_objc_msgForward 会做以下几件事情:

  • 1.先调用 forwardingTargetForSelector 方法获取新的 target 作为 receiver 重新执行 selector,

    • 如果返回的内容合法, 跳转去执行

    • 如果返回的内容不合法(为 nil 或者跟旧 receiver 一样),继续执行后续方法。

  • 2.调用 methodSignatureForSelector获取方法签名后,判断返回类型信息是否正确,再调用 forwardInvocation 执行 NSInvocation 对象,并将结果返回。如果对象没实现 methodSignatureForSelector 方法,继续执行后面方法。

  • 3.调用 doesNotRecognizeSelector 方法,抛出异常。

参考资料

[1]

Objective-C 运行时参考: https://developer.apple.com/documentation/objectivec/objective_c_runtime

[2]

objc_msgSend: https://developer.apple.com/documentation/objectivec/1456712-objc_msgsend

来源:新生代农民工No1

https://juejin.cn/post/7063376892178464799

-End-

最近有一些小伙伴,让我帮忙找一些 面试题 资料,于是我翻遍了收藏的 5T 资料后,汇总整理出来,可以说是程序员面试必备!所有资料都整理到网盘了,欢迎下载!

点击👆卡片,关注后回复【面试题】即可获取

在看点这里好文分享给更多人↓↓

以上是关于深入理解 iOS Runtime的主要内容,如果未能解决你的问题,请参考以下文章

iOS runtime探究: 从runtime开始深入weak实现机理

iOS runtime探究: 从runtime开始理解OC的属性property

iOS runtime探究: 从runtime开始理解面向对象的类到面向过程的结构体

iOS runtime探究: 从runtiem开始实践Category添加属性与黑魔法method swizzling

Runtime - 01

iOS开发之Runtime机制深入解析