Runtime的常见用法 之 KVO的底层原理
1、Runtime是什么?
不严谨的说法:
1、运行时(Runtime)是指将数据类型的确定由编译时推迟到了运行时。
2、Runtime是一套比较底层的C语言API, 属于1个C语言库, 包含了很多底层的C语言API。
3、平时编写的OC代码,在程序运行过程中,其实最终会转换成Runtime的C语言代码,Runtime是Object-C的幕后工作者。
4、Object-C需要Runtime来创建类和对象,进行消息发送和转发。
较严谨说法:
1、将尽可能多的决策从编译时和链接时推迟到运行时。
2、运行时系统充当着Object-C语言的操作系统,它使语言能够工作。
3、Runtime编写的代码具有运行时、动态特性。
2、Runtime与OC的交互方式
1、通过Object-C源代码进行交互。
2、通过NSObject类中定义的方法交互。
3、通过直接调用运行时函数。
3、Runtime的作用以及常见用法
Runtime的作用:
1、在程序运行过程中,动态的创建类,动态添加、修改这个类的属性和方法。
2、遍历一个类中所有的成员变量、属性、以及所有方法。
3、消息传递、转发。
Runtime常见用法:
1、给系统分类添加属性、方法。
2、方法交换。
3、获取对象的属性、私有属性。
4、字典转换模型。
5、KVC、KVO。
6、归档(编码、解码)。
7、获取类名。
8、block。
...
4、Runtime定义
NSObject的定义:
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
NSObject是一个Class类型来表示的指向objc_class结构体的指针。
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
id和Class指向的都是objc_class结构体
objc_class结构体中的定义:
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
5、Runtime消息机制
在OC中所有调用的方法,都是Runtime通过objc_msgSend向这个类发送消息。
Objc-msgSend消息传递的过程:
1、消息发送给对象时,消息传递函数遵循对象的isa指针指向类结构的指针,在该结构中它查询结构体变量methodLists中的方法SEL
(方法选择器)。
2、如在isa指向的类结构中找不到SEL
(方法选择器),Objc_msgSend会跟随指向Supercalss(父类)指针并再次尝试查找该SEL
。
3、如连续失败直到NSObject类,它的superclass也就是它自己本身。
4、一旦找到SEL
,该函数就会调用methodLists的方法并将接收对象的指针传给它。
消息的加速发送:
有的时候在一个类会有继承关系,Objective-C中大部分对象都是继承于NSObject、自己自定义类,在这种继承体系当中有很多的方法,这些方法有可能不会用到,在向类发送消息的时候,去methodLists中查找无疑会拖慢程序的运行速度,所以Apple在开发的时候加入了cache的概念,也就是缓存。
在每个类中都会有一个单独的缓存,它可以包含继承过来的方法SEL
以及自己定义的SEL
,在搜索methodLists之前,消息传递程序会检查接受者对象的告诉缓存cache,如果找到,就不会再去搜索methodLists列表。
cache缓存的是一些已经调用过的SEL
,当写的程序预热足够时间,那么所有发送过的SEL
都会在cache中找到。
cache会动态增长,直到程序中所有调用的SEL
运行一遍为止。
6、Runtime消息转发
消息的传递过程中,当调用的对象的方法不存在时,会触发消息的转发机制。
消息转发过程:
1.动态方法解析
+(BOOL)resolveInstanceMethod:(SEL)sel 实例方法解析
+(BOOL)resolveClassMethod:(SEL)sel 类方法解析
当运用消息转发运行时,根据调用的方法类型调用这两个方法其中一个,返回值BOOL类型,告诉系统该消息是否被处理,YES处理 NO 未处理。
当这个对象在实现了resolveInstanceMethod,resolveClassMethod两个方法,并没有对该对象消息进行处理,那么该方法会被调用两次:
一次是没有找到该方法需要对象解析处理;第二次是告诉系统我处理完成需要再次调用该方法但实际上并没有处理完成,所以会调用第二次该方法崩溃。
2.后备接收者对象
-(id)forwardingTargetForSelector:(SEL)aSelector
在消息转发第一次方法解析中没有处理方法,并告诉系统本对象无法处理,需另寻办法,那么系统会给予另外一个办法,就是让别的对象B来处理该问题,如果对象B能够处理该消息,那么该消息转发结束。
3.以其他形式实现该消息方法
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
-(void)forwardInvocation:(NSInvocation *)anInvocation
当前面两个步骤都没有处理该SEL
时,就会到第三个步骤,这是最后寻找IML
的机会
将未知SEL作为参数传入methodSignatureForSelector
,在该方法中处理该消息,一旦能够处理,返回方法签名,让后续forwardInvocation
来进行处理。
forwardInvocation
可以直接切换调用目标,也就是该方法的Target
。
4.直到最后未处理,抛出异常
-(void)doesNotRecognizeSelector:(SEL)aSelector
作为找不到函数实现的最后一步,NSObject实现这个函数只有一个功能,就是抛出异常。
7、KVO的底层原理
KVO(Key-Value Observing),是一种观察者设计模式(另一种观察者模式是Notification),当指定的对象的属性被修改后,则其观察者就会接受到通知。简单的说就是每次指定的被观察对象的属性被修改后,KVO就会自动通知相应的观察者了。
KVO使用简单,底层实现复杂,故称之为黑魔法(isa-swizzling)。具体的实现过程如下:


当观察对象Person时,KVO机制动态创建一个继承自Person的名为 NSKVONotifying_Person的新类,且为NSKVONotifying_Person重写观察属性的setter 方法,setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象属性值的更改情况。
在这个过程,被观察对象的 isa 指针从指向原来的Person类,被KVO机制修改为指向系统新创建的子类 NSKVONotifying_Person,来实现当前类属性值改变的监听; 从应用层看,我们完全没有意识到有新的类出现,这是因为系统“隐瞒”了对KVO的底层实现过程。但是此时如果我们创建一个新的名为“NSKVONotifying_Person”的类,就会发现系统运行到注册KVO的那段代码时程序就崩溃,因为系统在注册监听的时候动态创建了名为NSKVONotifying_Person的中间类,并让isa指针指向这个中间类了。(isa 指针的作用:每个对象都有isa 指针,指向该对象的类,它告诉 Runtime 系统这个对象的类是什么。所以对象注册为观察者时,isa指针指向新子类,那么这个被观察的对象就变成新子类的对象了。) 因而在该对象上对 setter 的调用就会调用已重写的 setter,从而激活键值通知机制。
KVO为子类的观察者属性重写调用存取方法的工作原理在代码中相当于:上述例子中,当 person.name 的值改变时,Person对象的 isa 指针会指向 NSKVONotifying_Person,意味着,在程序运行时,会动态生成一个 NSKVONotifying_Person 类,该类继承于 Person,而且该类中也有个 -setName: 方法,方法中在设置 name 的同时实现了:

KVO的键值观察通知依赖于 NSObject 的两个方法:willChangeValueForKey:和 didChangevlueForKey:,在存取数值的前后分别调用2个方法:被观察属性发生改变之前,willChangeValueForKey:被调用,通知系统该 keyPath 的属性值即将变更;当改变发生后, didChangeValueForKey: 被调用,通知系统该 keyPath 的属性值已经变更;之后observeValueForKey:ofObject:change:context: 也会被调用。且重写观察属性的setter 方法,这种继承方式是在运行时而不是编译时实现的。此时已修改后的属性值,已准备就绪,等待被使用。