一、KVO原理
1. KVO 简介
KVO 是 Objective-C 对观察者设计模式的一种实现。KVO 提供一种机制,指定一个被观察对象,当对象某个属性发生改变时,对象会获得通知,并做出相应处理;
在 MVC 设计架构的项目,KVO机制很适合实现 model 模型和 View 视图之间的通讯。
2. 实现原理
KVO 的实现依赖于 Objective-C 强大的 Runtime。
当观察某对象α时,KVO 机制动态创建一个对象α当前类的子类,并为这个新的子类重写被观察属性 keyPath 的 setter 方法。setter 方法随后负责通知观察对象属性的改变状况。
3. 深入剖析:
Apple 使用了 isa-swizzling 来实现 KVO。当观察对象α时,KVO 机制动态创建一个新的名为:NSKVONotifying_α的新类,该类继承自对象α的本类,且 KVO 为 NSKVONotifying_α重写观察属性的 setter 方法,setter 方法会负责在调用原setter 方法之前和之后,通知所有观察对象属性值的更改情况。
NSKVONotifying_α类剖析:在这个过程,被观察对象的 isa 指针从原来的α类,被 KVO 机制修改为指向系统新创建的子类 NSKVONotifying_α类,来实现当前类属性值改变的监听。
所以当我们从应用层面上看来,完全没有意识到有新的类出现,这是系统对 KVO 的底层实现过程,此时如果我们创建一个新的名为“NSKVONotifying_α”的类,就会发现系统运行到注册 KVO 的那段代码时程序就会崩溃。因为系统在注册监听的时候创建了名为 NSKVONotifying_α的中间类,并指向这个中间类。
(isa 指针的作用: 每个对象都有 isa 指针,指向该对象的类,它告诉 Runtime 系统这个对象的类是什么。所以对象注册为观察者时,isa 指针指向新子类,那么这个被观察的对象就神奇地变成新子类的对象(或实例)了。)因而在该对象上对 setter 的调用就会调用已重写的 setter,从而激活键值通知机制。
子类 setter 方法剖析:KVO 的键值观察通知依赖于 NSOjbect 的两个方法:willChangeValueForKey:和 didChangeValueForKey:,在存取数值的前后分别调用2个方法:
被观察属性发生改变之前,willChangeValueForKey:被调用,通知系统该 keyPath 的属性值即将变更;当改变发生后,didChangeValueForKey:被调用,通知系统该 keyPath 的属性值已经变更;之后,observeValueForKey:ofObject:change:context:也会被调用。且重写观察属性的 setter 方法这种继承方式的注入是在运行时而不是编译时实现的。
KVO 为子类的观察者属性重写调用存取方法的工作原理在代码中相当于:
- (void)setName:(NSString *)newName{
[self willChangeValueForKey:@"name"];
[super setValue:newName forKey:@"name"];
[self didiChangeValueForKey:@"name"];
}
4.特点:
观察者观察的是属性,只有遵循 KVO 变更属性值的方式才会执行 KVO 的回调方法,例如是否执行了 setter 方法、或者是否使用了 KVC 赋值。如果赋值没有通过 setter 方法或者 KVC,而是直接修改属性对应的成员变量,例如:仅调用_name = @"newName",这事是不会触发 KVO 机制,更加不会调用回调方法的。所以使用 KVO 机制的前提是遵循 KVO 的属性设置方式来变更属性值。
5.步骤
- 1.注册观察者,实施监听;
- 2.在回调方法中处理属性发生的变化;
- 3.移除观察者
6. 实现方法
A. 注册观察者:
//第一个参数 observer:观察者(这里观察 self.myKVO 对象的属性变化)
//第二个参数 keyPath: 被观察的属性名称(这里 self.myKVO 中 num属性值的改变)
//第三个参数 options:观察属性的新值、旧值等的一些配置(枚举值,可以根据需要设置,例如这里可以使用两项)
//第四个参数 context: 上下文,可以为 kvo 的回调方法传值(例如设定为一个放置数据的字典)
[self.myKVO addObserver:self forKeyPath:@"num" options: NSKeyValueObservingOptionOld|NSKeyValueObservingOpitonNew context:nil];
B.属性(keyPath)的值发生变化时,收到通知,调用以下方法:
//keyPath:属性名称
//object: 被观察的对象
//change:变化前后的值都存储在 change 字典中
//context: 注册观察者时,context 传过来的值
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *) change context:(void *)context{
}
7.拓展
1.KVC 和 KVO 的不同
KVC(键值编码),即 Key-Value Coding,一个非正式的 Protocol,使用字符串(键)访问一个对象实例变量的机制。而不是通过调用 Setter、Getter 方法等显式的存取方法去访问。KVO(键值监听),即 Key-Value Observing,它提供一种机制,当指定的对象的属性被修改后,对象就会接受到通知,前提是执行了 setter 方法、或者使用了 KVC 赋值。
2.和 notification(通知)的区别?
notification 比 KVO 多了发送通知的一步。
两者都是一对多,但是对象之间直接的交互,notification 明显很多,需要 notificationCenter 来作为中间交互。而 KVO 如我们介绍的,设置观察者->处理属性变化,
notification 的优点是监听不局限与属性的变化,还可以对多种多样的状态变化进行监听,监听范围广,例如键盘、前后台等系统通知的使用也更更显灵活方便。
- 与 delegate 的不同
和 delegate 一样,KVO 和 NSNotification 的作用都是类与类之间的通信。但是与 delegate 不同的是:
这两个都是负责发送接收通知,剩下的事情由系统处理,所以不同返回值;而 delegate 则需要通信的对象通过变量(代理)联系;
delegate 一般是一对一,而这两个可以一对多。
二、Category实现原理
1. 简介
category 是 Objective-C 2.0 之后添加的语言特性。主要作用是为已经存在的类添加方法。还有以下两种用法:
可以把类的实现分开在几个不同的文件里面。1)可以减少单个文件的体积, 2)可以把不同的功能组织到不同的 category 里 3)可以由多个开发者共同完成一个类 4)可以按需加载想要的 category 等等
声明私有方法。
2. 比较 category 和 extension
extension 在编译期决定,它是类的一部分,在编译期和头文件里的@interface 以及实现文件里的@implement 一起形成一个完整的类,它伴随类的产生而产生,消亡而消亡。extension 一般用来隐藏类的私有信息,你必须有一个类的源码才能为一个类添加 extension,所以无法为系统的类添加 extension。
但是 category 实在运行期决定的,就 category 和 extension 的区别来看,extension 可以添加实例变量,而 category 无法添加实例变量(因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的)。
3. category 原理
所有 OC 类和对象,在 Runtime 层都是用 struct 表示的,category 也不例外,在 runtime 层,category 用结构体 category_t (在 objc-runtime-new.h 中可以找到此定义),它包括了:
1)类的名字(name)
2)类(cls)
3)category 中所有给类添加的实例方法的列表(instanceMethods)
- category 中所有添加的类方法的列表(classMethods)
5) category 实现的所有协议的列表(Protocols)
6) category 中添加的所有属性(instanceProperties)
typedef struct category_t {
const char * name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t * classMethods;
struct protocol_list_t * protocols;
struct property_list_t * instanceProperties;
} category_t;
从 category 的定义也可以看出 category 的可为(可以添加实例方法,类方法,甚至可以实现协议,添加属性)和不可为(无法添加实例变量)。
MyClass.h:
#import <Foundation/Foundation.h>
@interface MyClass:NSObject
- (void)printName;
@end
@interface MyClass(MyAddition)
@property(nonatomic,copy) NSString *name;
- (void)printName;
@end
MyClass.m
#import"MyClass.h"
@implementation MyClass
- (void)printName{
NSLog(@"%@",@"MyClass");
}
@end
@implementation MyClass(MyAddition)
- (void)printName{
NSLog(@"%@",@"MyAddition");
}
4. category 和关联对象
在 category 里是无法为 category 添加实例变量的。但是我们很多时候需要在 category 中添加和对象关联的值,这个时候可以求助关联对象来实现。
MyClass + Category.h:
#import “MyClass.h”
@interface MyClass(Category1)
@property(nonatomic,copy) NSString *name;
@end
MyClass+Category.m:
#import "MyClass+Category.h"
#import <objc/runtime.h>
@implementation MyClass(Category)
- (void)setName:(NSString *)name{
objc_setAssociatedObject(self, "name", name, OBJC_ASSOCIATION_COPY);
}
-(NSString *)name{
NSString *nameObject = objc_getAssociatedObject(self, "name");
return nameObject;
}
@end