探究Objective-C-之runtime
主要参考自:南峰子 和 王中周的个人博客, 提取了其中感兴趣的点。
##前言
作为一门动态编程语言,Objective-C 会尽可能的将编译和链接时要做的事情推迟到运行时。只要有可能,Objective-C 总是使用动态 的方式来解决问题。这意味着 Objective-C 语言不仅需要一个编译环境,同时也需要一个运行时系统来执行编译好的代码。运行时系统(runtime)扮演的角色类似于 Objective-C 语言的操作系统,Objective-C 基于该系统来工作。因此,runtime好比Objective-C的灵魂,很多东西都是在这个基础上出现的。Objc Runtime其实是一个Runtime库,它基本上是用C和汇编写的,这个库使得C语言有了面向对象的能力。
##要点
- Class
- objc_object 与id
- objc_cache
- Meta Class(元类)
当我们向一个对象发送消息时,runtime会在这个对象所属的类的方法列表中查找方法;而当我们向一个类发送消息时,会在这个类的meta-class的方法列表中查找。meta-class是一个类对象所属的类,每个类都会有一个单独的meta-class,因为每个类的类方法基本不可能完全相同。
##类与对象操作函数
runtime提供了大量的函数来操作类和对象。类的操作方法大部分以class为前缀,而对象的操作方法大部分是以objc或object_为前缀。
Parameter | 说明 |
---|---|
const char* class_getName(Class cls); | 获取类的类名 |
Class class_getSuperclass(Class cls); | 获取父类 |
BOOL class_isMetaClass(Class cls); | 是否是元类 |
size_t class_getInstanceSize(Class cls); | 获取实例大小 |
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char* types); | 添加方法 |
Method class_getClassMethod(Class cls, SEL name); | 获取类方法 |
Method* class_getInstanceMethod(Class cls, SEL name); | 获取实例方法 |
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char* types); | 替换方法的实现 |
…. | …. |
objc_alllocateClassPair(Class superclass, const char*name, size_t textraBytes); | 创建一个新类和元类 |
objc_registerClassPair(Class cls); | 注册创建的类 |
Ivar object_getInstanceVariable(id obj, const char name, void* outvalue); | 获取对象实例变量的值 |
Ivar object_setInstanceVariable(id obj, const char name, void value); | 修改对象实例变量的值 |
id object_getIvar(id obj, Ivar ivar); | 返回对象中实例变量的值 |
const char* object_getClassName(id obj) | 获取对象的类名 |
Class object_getClass(id obj); | 获取对象的类 |
Class object_setClass(id obj, Class cls); | 设置对象的类 |
…. | …. |
方法与消息
###SEL
Objective-C在编译的时候,会根据方法的名字,生成一个用来区分这个方法的唯一的一个ID,这个ID就是SEL类型的,本质上就是一个char型指针。如下所示:
我们需要注意的是,只要方法的名字相同,那么它们的ID都是相同的。就是说,不管是超类还是子类,不管是有没有超类和子类的关系,只要名字相同那么ID就是一样的。当然,不同的类可以拥有相同的selector,这个没有问题。不同类的实例对象执行相同的selector时,会在各自的方法列表中去根据selector去寻找自己对应的IMP。
工程中的所有的SEL组成一个Set集合,Set的特点就是唯一,因此SEL是唯一的。因此,如果我们想到这个方法集合中查找某个方法时,只需要去找到这个方法对应的SEL就行了,SEL实际上就是根据方法名hash化了的一个字符串,而对于字符串的比较仅仅需要比较他们的地址就可以了,可以说速度上无语伦比!!但是,有一个问题,就是数量增多会增大hash冲突而导致的性能下降(或是没有冲突,因为也可能用的是perfect hash)。但是不管使用什么样的方法加速,如果能够将总量减少(多个方法可能对应同一个SEL),那将是最犀利的方法。那么,我们就不难理解,为什么SEL仅仅是函数名了。
本质上,SEL只是一个指向方法的指针(准确的说,只是一个根据方法名hash化了的KEY值,能唯一代表一个方法),它的存在只是为了加快方法的查询速度。
我们可以在运行时添加新的selector,也可以在运行时获取已存在的selector,我们可以通过下面三种方法来获取SEL:
- sel_registerName函数
- Objective-C编译器提供的@selector()
- NSSelectorFromString()方法
###IMP
IMP实际上是一个函数指针,指向方法实现的首地址,其定义如下:
第一个参数是只想self的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针),第二个参数是方法选择器(selector),之后是方法的实际参数列表。根据SEL取得IMP后,我们就获得了执行这个方法代码的入口点,此时,我们就可以像调用普通的c语言函数一样来使用这个函数指针了。
通过取得IMP,我们可以跳过Runtime的消息传递机制,直接执行IMP只想的函数实现,这样省去了Runtime消息传递过程中所做的一系列查找操作,会比直接向对象发送消息高效一些。
###Method
Method用于表示类定义中的方法,可以看上其包含了一个SEL和IMP,相当于在SEL和IMP之间做了一个映射。
###方法相关操作函数
包括以下:
消息调用流程
一切还是从消息表达式[receiver message]开始,在被转换成objc_msgSend(receiver, SEL)后,在运行时,runtime system会做以下事情:
1、检查忽略的Selector,比如当我们运行在有垃圾回收机制的环境中,将会忽略retain和release消息。
2、检查receiver是否为nil。不像其他语言,nil在objective-C中是完全合法的,并且这里有很多原因你也愿意这样,比如,至少我们省去了给一个对象发送消息前检查对象是否为空的操作。如果receiver为空,则会将 selector也设置为空,并且直接返回到消息调用的地方。如果对象非空,就继续下一步。
3、接下来会根据SEL到当前类中查找对应的IMP,首先会在cache中检索它,如果找到了就根据函数指针跳转到这个函数执行,否则进行下一步。
4、检索当前类对象中的方法表(method list),如果找到了,加入cache中,并且就跳转到这个函数之行,否则进行下一步。
5、从父类中寻找,直到根类:NSObject类。找到了就将方法加入对应类的cache表中,如果仍未找到,则要进入:动态方法决议。
6、如果动态方法决议仍不能解决问题,只能进行最后一次尝试,进入消息转发流程。
7、如果还不行,去死吧。
####动态方法解析
由NSObject根类提供的类方法,调用时机为 当被调用的方法实现部分没有找到,而消息转发机制启动之前的这个中间时刻:
####消息转发
对象是谦恭的,它会接收所有发送过来的消息,哪怕这些消息自己无法响应。问题来了:当对象无法响应这些消息时怎么办?runtime提供了消息转发机制来处理该问题。
当外部调用的某个方法对象没有实现,而且resolveInstanceMethod方法中也没有做重定向处理时,就会触发- (void)forwardInvocation:(NSInvocation *)anInvocation方法。在该方法中,可以实现对不能处理的消息做的一些默认处理,也可以以其它的某种方式来避免错误被抛出。像forwardInvocation:的名字一样,这个方法通常用来将不能处理的消息转发给其它的对象。通常我们重写该方法的方式如下所示
##参考