深入理解 iOS Runtime

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

Runtime简介

Runtime 又称运行时,是iOS系统的核心,它是一套底层的C语言API。它会将一些工作放在代码运行时才处理而并非编译时,所以有很多类和成员变量在我们编译时是不知道的,而在运行时,我们所编写的代码会转换成完整的确定的代码运行。

运行时交互

Objective-C中与运行时系统有三个层次的交互:

  • 通过Objective-C源码
  • 通过Foundation中的NSObject定义的方法
  • 通过直接调用运行时函数

Objective-C 源代码

顾名思义,只要编写和编译 Objective-C代码即可使用它。编译包含Objective-C 类和方法的代码时,编译器会创建实现语言动态特性的数据结构和函数。

NSObject 方法

Cocoa 中,大多数对象都是NSObject的子类,因此都继承了他定义的方法。(NSProxy是个特例)
NSObject中有一些方法可以简单地向运行时系统查询信息。支持对象执行自省。例如class方法,有isKindOfClass:isMemberOfClass:, 检查对象在继承层级结构的位置是否正确;
conformsToProtocol:,对象是否要实现特定协议中定义的方法;respondsToSelector:,表示对象是否可以接受特定消息;

// 检查对象在继承层级结构的位置;
- (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 运行时参考

现在,我们知道了运行时交互有哪些,那么接下来,我们再看看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)。(想要深入了解元类可以查看这篇文章 What is a meta-class in Objective-C?)

元类就是类对象的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方法。

消息发送

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

推荐阅读更多精彩内容