一、Objective-C之Runtime的概念
前一篇关于NSProxy代理涉及到的关于消息转发,把以前写的runtime文章从github上转移过来。一共三篇,似乎自己也忘记了一些runtime的细节,需要温故一下。
一、什么是Objc的Runtime?
Runtime是Objc语言的磐石,Objc语言得以运行,也是依靠runtime库的支持。
Objc语言是一门动态语言,它将很多静态语言在编译和链接时期做的事放到了运行时来处理。这种动态语言的优势在于:我们写代码时能够更具灵活性,如我们可以把消息转发给我们想要的对象,或者随意交换一个方法的实现等。这种特性意味着Objc不仅需要一个编译器,还需要一个运行时系统来执行编译的代码。对于Objc来说,这个运行时系统就像一个操作系统一样:它让所有的工作可以正常的运行,这个运行时系统即Objc Runtime。
Runtime基本上是用C和汇编写的,这个库使得C语言有了面向对象的能力。在这个库中,对象可以用C语言中的结构体表示,而方法可以用C函数来实现,再加上了一些额外的特性。这些结构体和函数被runtime函数封装后,我们就可以在程序运行时创建,检查,修改类、对象和它们的方法了。runtime可以有效的帮助我们为程序增加很多动态的行为。
二、Runtime中的重要概念
先看一句最常见的方法调用代码:[receiver message];
刚开始学习Objc时,觉得上面的方法就是调用receiver对象的message方法。这样的理解也没错。更底层的原理应该是这样的:
向receiver对象发送消息,上面的代码会被编译器转成:
objc_msgSend(receiver, selector)
如果含有多个参数则是:
objc_msgSend(receiver, selector, arg1, arg2, ...)
如果消息的接收者receiver能够找到对应的selector,那就执行接收者receiver的这个message方法。否则消息将被转发,或临时动态的向接收者receiver添加这个selector的实现,或者直接崩溃。
至此,知道了编译阶段只是知道了向receiver对象发送了这样一个消息,至于消息能否被响应,需要等到运行时的具体情况决定。由此看出Objc的Runtime铸就了它动态语言的特性。
大部分情况下你就只管写你的Objc代码就行,runtime系统自动在幕后辛勤劳作着。消息的执行会使用到一些编译器为实现动态语言特性而创建的数据结构和函数,Objc中的类、方法和协议等在runtime中都由一些数据结构来定义。
三、梳理objc_msgSend
方法原型是:id objc_msgSend(id self, SEL op, ...)
。
id
id是指向类实例的结构体指针。定义如下:
typedef struct objc_object *id;
id这个结构体的定义本身就带了一个*
号, 所以我们在使用其他NSObject类型的实例时需要在前面加上*
, 而使用id时却不用。
接着,我们发现一个objc_object的定义,继续深入进去看下..
objc_object
objc_object其实是一个结构体,结构体里包含一个指向Class类的isa指针。根据isa指针就可以找到id实例所属的Class类了。
struct objc_object { Class isa; };
发现一个Class的定义,继续深入进去看下..
Class
Class是一个指针类型,指向了objc_class类型。
typedef struct objc_class *Class;
发现一个objc_class的定义,继续深入进去看下..
objc_class
这个objc_class可是一个重要大明星。也是runtime的重点。到这里,我们可以得出一个结论:每个id对象都有一个指向所属类的指针isa。通过该指针,对象可以找到它所属的类,也可以找到了其全部父类(这一句需要在学完objc_class结构体后得到,不过本篇不打算再说objc_class了,因为篇幅有限,重点需要另开一篇来学习总结)。
花开两朵各表一枝,这条线先挖到objc_class这里。然后回头继续看objc_msgSend的第二个参数SEL类型的op。
SEL
SEL其实是selector方法选择器的标示,换言之:SEL是用来标示selector的。
typedef struct objc_selector *SEL;
Objc在编译时,会依据每一个方法的名字、参数序列,生成一个唯一的整型标识(Int类型的地址),这个标识就是SEL。如下代码所示:
SEL sel = @selector(method);
NSLog(@"sel: %p", sel);
//output:sel: 0x108dfbba3
两个类之间,不管它们是父类与子类的关系,还是之间没有这种关系,只要方法名相同,那么它的SEL就是一样的。每一个方法都对应着一个SEL。编译器会根据每个方法的方法名为那个方法生成唯一的SEL。这些SEL组成了一个Set集合,当我们在这个集合中查找某个方法时,只需要去找这个方法对应的SEL即可。而SEL本质是一个字符串,所以直接比较它们的地址即可。
当然,不同的类可以拥有相同的selector。不同类的实例对象执行相同的selector时,会在各自的方法列表中去根据selector去寻找自己对应的IMP。
objc_msgSend的调用过程(先理解其中的概念):
当向一个对象发送消息时,objc_msgSend方法根据对象的isa指针找到对象的类,然后在类的调度表(dispatch table)中查找selector。如果无法找到selector,objc_msgSend通过指向父类的指针找到父类,并在父类的调度表(dispatch table)中查找selector,以此类推直到NSObject类。一旦查找到selector,objc_msgSend方法根据调度表的内存地址调用该实现。 通过这种方式,message与方法的真正实现在执行阶段才绑定。
为了保证消息发送与执行的效率,系统会将全部selector和使用过的方法的内存地址缓存起来。每个类都有一个独立的缓存,缓存包含有当前类自己的 selector以及继承自父类的selector。查找调度表(dispatch table)前,消息发送系统首先检查receiver对象的缓存。
上面的描述如下图所示:
至此初步认识了runtime,也初步了解了Objc的重要概念内容,比如id、SEL等。当然更深的东西,还要继续学习挖掘~~~