前言
有关Runtime的知识总结,我本来想集中写成一篇文章的,但是最后发现实在是太长,而且不利于阅读,最后分成了如下几篇:
- RunTime 之使用前须知
- RunTime 之常规操作
- RunTime 之消息处理与消息转发
- RunTime 之Method Swizzling
- RunTime 之其他实践运用
另外这个库可以使你很轻松的使用Runtime:Aspects
在使用Runtime之前,了解Runtime的一些相关知识有助于我们更好的理解其中的实现过程和原理。以及API的参数意义和使用场景。
需要理解如下概念:
- Class
一种结构体
- ISA
一个指向类的指针,每个对象中都存在。
- SEL
方法名称的描述。
- IMP
具体的方法的地址。
- 消息机制
- 动态特性
在Objective-C中,任何类的定义都是对象。类和类的实例(对象)没有任何本质上的区别。
任何对象都有isa指针。
对象在内存中的排布可以看成一个结构体。
类的本质与isa指针及SEL指针的关系
Class
Class 被定义为一个指向 objc_class的结构体指针,这个结构体表示每一个类的类结构。
代码语言:javascript复制而 objc_class 在objc/objc_class.h中定义如下:
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class
const char *name; /*类名字*/
long version; /*版本信息*/
long info; /*类信息*/
long instance_size; /*实例大小*/
struct objc_ivar_list *ivars; /*实例参数链表*/
struct objc_method_list **methodLists; /*方法链表*/
struct objc_cache *cache; /*方法缓存*/
struct objc_protocol_list *protocols; /*协议链表*/
};
因为类也是一个对象,那它也必须是另一个类的实列,这个类就是元类 (metaclass)。元类保存了类方法的列表。当一个类方法被调用时,元类会首先查找它本身是否有该类方法的实现,如果没有,则该元类会向它的父类查找该方法,直到一直找到继承链的头。
元类 (metaclass) 也是一个对象,那么元类的 isa 指针又指向哪里呢?为了设计上的完整,所有的元类的 isa 指针都会指向一个根元类 (root metaclass)。根元类 (root metaclass) 本身的 isa 指针指向自己,这样就行成了一个闭环。上面提到,一个对象能够接收的消息列表是保存在它所对应的类中的。在实际编程中,我们几乎不会遇到向元类发消息的情况,那它的 isa 指针在实际上很少用到。不过这么设计保证了面向对象的干净,即所有事物都是对象,都有 isa 指针。
我们再来看看继承关系,由于类方法的定义是保存在元类 (metaclass) 中,而方法调用的规则是,如果该类没有一个方法的实现,则向它的父类继续查找。所以,为了保证父类的类方法可以在子类中可以被调用,所以子类的元类会继承父类的元类,换而言之,类对象和元类对象有着同样的继承关系。
下面这张图或许能够让大家对 isa 和继承的关系:
该图中,最让人困惑的莫过于 Root Class 了。在实现中,Root Class 是指 NSObject,我们可以从图中看出:
1. NSObject 类包括它的对象实例方法。 2. NSObject 的元类包括它的类方法,例如 alloc 方法。 3. NSObject 的元类继承自 NSObject 类。 4. 一个 NSObject 的类中的方法同时也会被 NSObject 的子类在查找方法时找到。
类的成员变量
如果把类的实例看成一个 C 语言的结构体(struct),上面说的 isa 指针就是这个结构体的第一个成员变量,而类的其它成员变量依次排列在结构体中。排列顺序如下图所示
ISA指针
Every object is connected to the run-time system through its isa instance variable, inherited from the NSObject class. isa identifies the object's class; it points to a structure that's compiled from the class definition. Through isa, an object can find whatever information it needs at runtime such as its place in the inheritance hierarchy, the size and structure of its instance variables, and the location of the method implementations it can perform in response to messages.
- 每一个OC实例对象都保存有isa指针和实例变量,
- 其中isa指针所属类,类维护一个运行时可接收的方法列表(MethodLists) ;
- 方法列表(MethodLists)中保存选择器的方法名(SEL)和方法实现(IMP,指向方法实现的指针)的映射关系。
- 在运行时,通过selecter找到匹配的IMP,从而找到的具体的实现函数。
“一些”对象会使用其 isa 指针的一部分空间来存储它的引用计数
isa是一个Class 类型的指针,通过一个实例对象(Object)的isa,我们可以找到一个对象的所以信息,如类属性的结构,类方法(消息)的入口地址等。
SEL
实际是SEL,只是具体实现方法的索引值(通过它找到IMP(具体实现的函数指针)),它的存在是为了快速找到具体实现的方法。
代码语言:javascript复制typedef struct objc_selector *SEL;
如下所示,打印出 selector。
-(NSInteger)maxIn:(NSInteger)a theOther:(NSInteger)b
{
return (a > b) ? a : b;
}
NSLog(@"SEL=%s", @selector(maxIn:theOther:));
输出:SEL=maxIn:theOther:
不同的类可以拥有相同的 selector,这个没有问题,因为不同类的实例对象performSelector相同的 selector 时,会在各自的消息选标(selector)/实现地址(address) 方法链表中根据 selector 去查找具体的方法实现IMP, 然后用这个方法实现去执行具体的实现代码。
这是一个动态绑定的过程,在编译的时候,我们不知道最终会执行哪一些代码,只有在执行的时候,通过selector去查询,我们才能确定具体的执行代码。
IMP
IMP 的定义为:
代码语言:javascript复制 typedef id (*IMP)(id, SEL, ...);
我们知道 id是一个指向 objc_object 结构体的指针,该结构体只有一个成员isa,所以任何继承自 NSObject 的类对象都可以用id 来指代,因为 NSObject 的第一个成员实例就是isa。
至此,我们就很清楚地知道 IMP 的含义:IMP 是一个函数指针,这个被指向的函数包含一个接收消息的对象id(self 指针), 调用方法的选标 SEL (方法名),以及不定个数的方法参数,并返回一个id。也就是说 IMP 是消息最终调用的执行代码,是方法真正的实现代码 。我们可以像在C语言里面一样使用这个函数指针。
NSObject 类中的methodForSelector:方法就是这样一个获取指向方法实现IMP 的指针,methodForSelector:返回的指针和赋值的变量类型必须完全一致,包括方法的参数类型和返回值类型。
消息机制
先看下OC的编译时和运行时:
- 编译时: 即编译器对语言的编译阶段,编译时只是对语言进行最基本的检查报错,包括词法分析、语法分析等等,将程序代码翻译成计算机能够识别的机器语言,编译通过并不意味着程序就可以成功运行。详情可看:iOS编译过程的原理和应用
编译器前端(Objective C采用Clang作为前端,而Swift则采用swift()作为前端)
编译器前端的任务是进行:语法分析,语义分析,生成中间代码(intermediate representation )。在这个过程中,会进行类型检查,如果发现错误或者警告会标注出来在哪一行。
编译器后端 LLVM(Low level vritual machine)
编译器后端会进行机器无关的代码优化,生成机器语言,并且进行机器相关的代码优化。iOS的编译过程,后端的处理如下
(1)LVVM优化器会进行BitCode的生成,链接期优化等等。
(2)LLVM机器码生成器会针对不同的架构,比如arm64等生成不同的机器码。
- 运行时: 即程序通过了编译这一关之后编译好的代码被装载到内存中跑起来的阶段,而运行时类型检查就与前面讲的编译时类型检查(或者静态类型检查)不一样.不是简单的扫描代码.而是在内存中做些操作,做些判断,此时若出错程序会崩溃。
可以说编译时是一个静态的阶段,类型错误很明显可以直接检查出来,可读性也好;而运行时则是动态的阶段,开始具体与运行环境结合起来。
如何理解消息机制:
(1)在Objective-C中,message与方法是在执行阶段绑定的,而不是编译阶段。 (2)简单的说 a someFunc 这样一个调用,在编译阶段,编译器并不知道someFunc要执行哪段代码。这个时候a someFunc会被转换为 objc_msgSend(a, "someFunc"),字面的意思也很容易理解,就是给a这个instance,发“someFunc”这个消息,以selector的形式。 (3)在运行阶段,执行到上述的objc_msgSend这个函数时。函数内部会到a对应的内存地址,寻找someFunc这个方法的地址,并执行。如果找不到,就会抛一个“unknown selector sent to instance”的异常。(比如.h中声明了方法,但.m中没有实现,就可以重现这个错误)
在 Objective-C 语言中,每一个类实际上也是一个对象。每一个类也有一个名为 isa 的指针。每一个类也可以接受消息,例如NSObject alloc,就是向 NSObject 这个类发送名为alloc消息。
SEL会依据方法名生成唯一的表示作为key ,便于查找
IMP指针是指向实现函数的指针,通过SEL取得IMP,objc_msgSend来执行实现方法
objc_msgSend函数在执行方法时不会直接在 isa 指针指向的类的方法列表中遍历查找能够响应的方法,因为每次都要查找效率太低了,而是优先在缓存(方法列表)中查找,若是找不到再沿着继承体向上查找。每次匹配到的结果会缓存在"快速映射表"里,来提高效率。
另外objc_msgSend方法也能hold住message的参数,如:
objc_msgSend(receiver, selector, arg1, arg2, …);
OC的动态特性
OC在底层也提供了相当丰富的运行时的特性,基本的也是经常被提到和用到的有动态类型,动态绑定和动态加载。
- 动态类型 即运行时再决定对象的类型。这类动态特性在日常应用中非常常见,简单说就是id类型。id类型即通用的对象类,任何对象都可以被id指针所指。动态类型也是另一个动态特性 “动态绑定” 的基础和结果。 -isMemberOfClass: 是 NSObject 的方法,用以确定某个 NSObject 对象是否是某个类的成员。 -isKindOfClass:,可以用以确定某个对象是否是某个类或其子类的成员。这两个方法为典型的introspection方法。 #在确定对象为某类成员后,可以安全地进行强制转换,继续之后的工作。
- 动态绑定 基于动态类型,在某个实例对象被确定后,其类型便被确定了。该对象对应的属性和响应的消息也被完全确定,这就是动态绑定。
在继续之前,需要明确Objective-C中 消息 的概念。由于OC的动态特性,在OC中其实很少提及“函数”的概念,传统的函数一般在编译时就已经把参数信息和函数实现打包到编译后的源码中了,而在OC中最常使用的是消息机制。调用一个实例的方法,所做的是向该实例的指针发送消息,实例在收到消息后,从自身的实现中寻找响应这条消息的方法。
动态绑定所做的,即是在实例所属类确定后,将某些属性和相应的方法绑定到实例上。这里所指的属性和方法当然包括了原来没有在类中实现的,而是在运行时才需要的新加入的实现。
这是使用Runtime方法替换和注入的实现基础。可以在任意需要的地方调用class_addMethod或者method_setImplementation(前者添加实现,后者替换实现),来完成动态绑定的需求。
在Cocoa层,我们一般向一个NSObject对象发送-respondsToSelector:或者-instancesRespondToSelector:等来确定对象是否可以对某个SEL做出响应,而在OC消息转发机制被触发之前,对应的类的 resolveClassMethod:和 resolveInstanceMethod:将会被调用,在此时有机会动态地向类或者实例添加新的方法,也即类的实现是可以动态绑定的。
- 动态加载 根据需求加载所需要的资源,最经典的例子就是在Retina设备上加载@2x的图片,而在老一些的普通屏设备上加载原图。
几个重要的辅助函数
可以在使用过程中起到很好的辅助作用,尤其是在动态编译等起到了比较大的作用。
我们可以通过NSObject的一些方法获取运行时信息或动态执行一些消息:
代码语言:javascript复制 class 返回对象的类;
isKindOfClass 和 isMemberOfClass检查对象是否在指定的类继承体系中;
respondsToSelector 检查对象能否相应指定的消息;
conformsToProtocol 检查对象是否实现了指定协议类的方法;
methodForSelector 返回指定方法实现的地址。
performSelector:withObject 执行SEL 所指代的方法。
参考文章:
深入Objective-C的动态特性
关于CLASS , SEL, IMP的说明
唐巧-Objective-C对象模型及应用