runtime 总是一副很神秘的样子,就像盖着盖头的新娘,今天就揭下盖头看看新娘(runtime)究竟神秘样子!当然即使揭下盖头也只能看看新娘(runtime)的外貌,究竟内心是什么样的人,还需要长时间的相处去发现。
什么 runtime
runtime简称运行时,是一个c和汇编写的动态库,为 C 添加了面相对象的能力并创造了 Objective-C。也是系统运行时的一些机制,其中最主要的是消息机制。
1 | C语言:函数的调用在编译时会决定调用哪个函数 |
怎样使用 runtime
说了一些没用的废话,那么 runtime 是怎样来使用的呢?下面就简单介绍一下。
常用的头文件
1 | #import <objc/runtime.h> 包含对类、成员变量、属性、方法的操作 |
发送消息
OC 中方法调用的本质就是让对象发送消息。
比如在 OC 中这样调用方法
1 | // 实例方法 |
消息传递的关键是编译器构建每个类和对象时采用的数据结构,每个类必须包含2个必要元素:
1.一个指向父类的指针
2.一个调度表(dispatch table)将类的selector(方法名) 与方法的实际内存地址关联起来。
每个对象都有一个指向其所属类的指针 isa 。
当对象发送消息时,objc_msgSend() 方法根据对象的 isa 指针找到对象的类,然后在类的调度表(dispatch table)中查找 selector ,如果没有找到,objc_msgSend() 方法通过指向父类的指针找到父类,然后在父类的调度表中查找 selector,以此类推直到找到 NSObject 类。一旦找到 selector ,objc_msgSend()方法根据调度表中 selector 的内存地址调用实现。
上面的方法每次都要查找整个调度表,如果类的方法很多,执行起来会消耗较多的时间。为了保证消息的发送与执行速度,系统会将使用过的 selector 会 方法的内存地址缓存起来,每个类都有一个缓存区域,包含当前类使用过的 selector 和父类中使用过的 selector。查找调度表之前,会先查找对象所属类的缓存。
当 selector 没有被实现时,会面临2中情况:
1 | 1. 使用 [p eat] 调用 |
第一种情况编译器会报错,第二种情况需要运行时才能确定对象能否接受到消息,这时会进入消息转发流程:
- 消息转发流程
1、动态方法解析
接收到未知消息时(假设person的eat方法尚未实现),runtime会调用+resolveInstanceMethod:(实例方法)或者+resolveClassMethod:(类方法),比如:
1 | + (BOOL)resolveInstanceMethod:(SEL)sel |
简单介绍一下 class_addMethod 方法的使用
1 | OBJC_EXPORT BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types) |
2、备用接收者
如果以上方法没有做处理,runtime会调用- (id)forwardingTargetForSelector:(SEL)aSelector方法。
如果该方法返回了一个非nil(也不能是self)的对象,而且该对象实现了这个方法,那么这个对象就成了消息的接收者,消息就被分发到该对象。
适用情况:通常在对象内部使用,让内部的另外一个对象处理消息,在外面看起来就像是该对象处理了消息。
比如:person 让女朋友 personGirlFriend 来接收这个消息
1 | - (id)forwardingTargetForSelector:(SEL)aSelector { |
3、完整消息转发
在- (void)forwardInvocation:(NSInvocation *)anInvocation方法中选择转发消息的对象,其中anInvocation对象封装了未知消息的所有细节,并保留调用结果发送到原始调用者。
比如:person将消息完整转发給父母 PersonParent来处理
1 | - (void)forwardInvocation:(NSInvocation *)anInvocation { |
4、如果在以上三个方法都没有处理未知消息,则会引发异常。
遍历对象属性
OC 中成员变量的实质什么?方法的实质又是什么?类的实质又是什么?
1 | /// An opaque type that represents a method in a class definition. |
- 使用方法
1 | // 对成员变量的一些操作 |
- 使用实例1
1 | // 将json 数据转换成model |
- 使用实例2
1 | // 有时想查看model各个属性的值,但是调试时打印出来的却是一个地址,那么怎样解决这个问题呢?重写debugDescription方法。 |
添加属性与方法
该如何给系统的类(比如UIButton)添加一个属性或者方法呢?你可能想到了继承。但是如果不能用继承呢?你也许还会想到分类,但是分类只能添加方法不能添加属性啊,不要着急runtime 来了。
1 | // 如何为系统的类添加属性 |
方法交换
有些时候我们希望有自己的方法取代系统的方法(按照自己的方法实现,或者防止程序崩溃),比如防止数组越界或者传入值为nil 时,系统的崩溃。
改变系统方法的实现的方式有:继承,分类(类目),这里再说一种方法的交换。具体需要那种方式自己去判断。
方法交换的相关函数
1 | // 通过方法名获取方法 |
- 使用实例
1 | // 防止数组越界 |
总结
经过上面的学习,只能说对 runtime 有了一些了解,能够简单的使用一下,如果想要更深入的了解 runtime 还需要持续不断的深入学习。
参考博客: