做了很久的iOS开发了,但依然还是没有将一些基础的知识弄清楚,想要真正的掌握一门技术或则语言,真的不能一知半解,就像你说你熟练掌握了iOS的开发,但是如果别人问你什么是Runtime,它的原理是什么,如果这你都不知道真的算不上对iOS已经熟练掌握了。以前一直有一个误区,拿到一门语言或则技术直接就开始写东西了,但是对很多的原理都是一知半解,以致于忽略了很多基本知识,这篇笔记我要将我丢掉runtime的一些知识都捡起来。
Runtime简介
Objective-C是一门动态语言,它将很多静态语言在编译和链接时期做的事情放到了运行时来处理。对于Objective-C来说,这个Runtime就像是一个操作系统一样,它让所有的工作可以正常运行。Runtime简称运行时。Objective-C就是运行时机制,也就是在运行时的一些机制,最主要的就是消息机制。
- 对于C语言,函数的调用在编译的时候会决定调用那个函数。
- 对于Objective-C的函数,属于动态调用过程,在编译的时候并不能真正的决定调用哪个函数,只有真正运行的时候才会根据函数的名称找到对应的函数来调用。
Runtime消息传递
一个对象的方法像这样[obj foo]
,编译器转成消息发送objc_msgSend(obj, foo)
,Runtime时的执行流程是这样的:
- 首先,通过它的
obj
的isa
指针找到它的class
; - 在
class
的method_list
中找到foo
方法; - 如果
class
中没有找到foo
,就继续往它的super_class
中找; - 一旦找到
foo
这个函数,就去执行它的实现IMP
(如果还是找不到就会报unrecognized selector
的错)。
类对象(objc_class)
Objective-C类是由Class类型来表示,它实际上是指向objc_class结构体的一个指针。
1 | typedef struct objc_class *Class |
查看objc/runtime.h
文件中objc_class结构体的定义如下:
1 | // 类 |
实例(objc_object)
objc_object是表示一个类的实例的结构体,在objc/objc.h
文件中定义如下:
1 | typedef struct objc_class *Class; |
可以看到这个结构体只有一个字段,及指向其类的isa指针。这样当我们向一个Objective-C对象发送消息时,Runtime库会根据实例对象的isa指针找到这个实例对象所属的类。Runtime库会在类方法列表以及父类的方法列表中去寻找消息对应的selector指向的方法,找到后即运行这个方法。
元类(Meta Class)
类对象中的元数据存储的是如何创建一个实例的相关信息,类对象和类方法都应该从哪里创建呢?就是从isa指针指向的结构体创建,类对象的isa指针指向的我们称之为元类(Meta Class),元类中保存了创建类对象以及类方法所需的所有信息。因此整个结构应该如下图所示:

通过上图我们可以看出整个体系构成了一个自闭环,struct objc_object
结构体实例它的isa指针指向类对象,类对象的isa指向了元类,super_class
指向了父类的类对象,而元类的super_class
指向了父类的元类,那元类的isa又指向了自己。
元类(Meta Class)是一个类对象的类。所有的类自身也是一个对象,我们可以向这个对象发送消息(即调用方法)。为了调用方法,这个类的isa指针必须指向一个包含类方法的一个objc_class
结构体。这就引入了Meta Class概念,元类中保存了创建类对象以及类方法需要的所有信息。任何NSObject
集成体系下的meta-class
都使用NSObject
的meta-class
作为自己的所属类,而基类的meta-class
的isa指向它自己。
Method(objc_method)
在objc/runtime.h
中的定义如下:
1 | // 方法 |
Method
和我们平时理解的函数是一致的,就是表示能够独立完成一个功能的一段代码,比如:
1 | - (void)logName |
上面这段代码就是一个函数。
在objc_method
的结构体中,看到了SEL
和IMP
,说明SEL
和IMP
其实都是Method
的属性。
SEL(objc_selector)
在objc/objc.h
中的定义为:
1 | /// An opaque type that represents a method selector. |
objc_msgSend
函数第二个参数类型为SEL
,它是selector
在Objective-C
中的表示类型。selector
是方法选择器,可以理解为区分方法的ID
,而这个ID
的数据结构是SEL
:
1 | @property SEL selector; |
可以看到selector
是SEL
的一个实例。
其实selector
就是映射到方法的C字符串,你可以用Objective-C
编译器命令@selector()
或则Runtime系统的sel_registerName
函数来获得一个SEL
类型的方法选择器。
selector
既然是一个string,我觉得应该是类似于className+MethodName
的组合,命名规则有两条:
- 同一个类,selector不能重复
- 不同的类,selector可以重复
所以在Objective-C中如下的代码会报错:
1 | - (void)caculate(NSInteger)num; |
只能通过方法名来进行区别:
1 | - (void)caculateWithInt(NSInteger)num; |
IMP
在objc/objc.h
中IMP的定义如下:
1 | /// A pointer to the function of a method implementation. |
就是指向最终实现程序函数的内存地址的指针。
在iOS
的Runtime
中,Method
通过SEL
和IMP
两个属性,实现了快速方法的查询以及实现,相对提高了性能又保持了灵活性。
类缓存(objc_cache)
当Objective-C运行时通过跟踪它的isa指针检查对象时,它可以找到一个实现多个方法的对象。然而你只调用其中的以一小部分,并且每次检查时,搜索所有选择器的分派表没有意义。所以实现一个缓存,每当你搜索一个类分派表,并找到相应的选择器,它把它放入缓存。所以当objc_msgSend
查找一个类的选择器,它首先搜索缓存。
为了加速消息分发,系统会对方法和对应的地址进行缓存,就放在上面所述的objc_cache
,所以在实际运行中,大部分常用的方法都会被缓存起来,Runtime
系统实际上非常快,接近于直接执行内存地址程序的速度。
Category(objc_category)
在obj/runtime.h
中objc_category
的定义如下:
1 | struct objc_category { |
从上面的objc_category
的结构体可以看出,分类中可以添加实例方法、类方法,甚至可以实现协议,不能添加实例变量和属性。
Runtime消息转发
上面Runtime消息传递中已经介绍了一次发送消息会在相关的类对象中搜索方法列表,如果找不到则会沿着继承树向上一直搜索直到继承树根部(通常为NSObject),如果还是找不到并且消息转发都失败了就会执行doesNotRecognizeSelector:
方法报unrecognized selector
错。
因此对于对象尝试调用未实现的方法会报错,遇到这种情况会不会有什么“补救措施”,当然有,这就需要了解消息的转发机制。
当没有找到实现方法时,会调用一下函数:
- 动态方法解析
1
2+(BOOL)resolveInstanceMethod:(SEL)sel
+(BOOL)resolveClassMethod:(SEL)sel - 备用接受者
1
-(id)forwardingTargetForSelector:(SEL)aSelector
- 完整地消息转发
1
2-(NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector
-(void)forwardInvocation:(NSInvocation *)anInvocation
消息转发流程简图:
动态解析方法
首先会调用动态方法的解析方法,我们可以尝试在+(BOOL)resolveInstanceMethod:(SEL)selector
(针对实例方法)和+(BOOL)resolveClassMethod:(SEL)sel
(针对类方法)中添加实现方法。
实现一个动态方法解析的例子如下:
1 | - (void)viewDidLoad { |
2018-08-08 15:54:30.652862+0800 Runtime[32473:3482683] Person eat
从上面的例子可以看到虽然没有实现eat:
这个函数,但是通过class_addMethod
动态添加eatMethod
函数,并执行eatMethod
这个函数的IMP
。
如果+ (BOOL)resolveInstanceMethod:(SEL)sel
或+(BOOL)resolveClassMethod:(SEL)sel
方法没有处理eat:
方法,运行时就会移到下一步:- (id)forwardingTargetForSelector:(SEL)aSelector
。
备用接受者
如果目标对象实现了- (id)forwardingTargetForSelector:(SEL)aSelector
,那么运行时就会调用这个方法,把这个消息转发给其他对象。
1 |
|
打印结果:
2018-08-08 16:14:54.714890+0800 Runtime[35945:3529505] forwardingTargetForSelector Person eat
从上面的例子我们可以看到通过forwardingTargetForSelector
把当前ViewController
的方法传给了Person
去执行了。
完整消息转发
如果上面两部步都无法处理未知消息,那么唯一能做的就是启用完整消息转发机制了。首先它会发送- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
消息获得函数的参数和返回值类型。如果- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
返回nil
,Runtime
则会发-doesNotRecognizeSelector:
消息,程序也会挂掉。如果返回的了一个函数签名,Runtime
就会创建一个NSInvocation
对象并发送- (void)forwardInvocation:(NSInvocation *)anInvocation
消息给目标对象。
实现的例子如下:
1 |
|
打印结果:
2018-08-08 16:38:29.076233+0800 Runtime[39848:3579675] 完整消息转发 Person eat
从打印结果来看,我们实现了完整的消息转发。通过签名,Runtime
生成了一个对象(NSInvocation *)anInvocation
发送给forwardInvocation
方法,我们在forwardInvocation
方法中让Person
对象去执行eat
函数。
关于签名参数
v@:
的解释,在苹果官方文档Type Encoding中有详细的解释。