1.KVO (Key-Value Observing)是什么?
观察者模式,指定一个被观察对象,当被观察对象某个属性发生改变时,观察者会获得通知,并作出相应处理。
2.KVO 实现原理
- 当使用观察者模式观察一个对象时,KVO机制会在运行期动态创建一个对象当前类的子类,如果当前类为
Yinker
,动态创建的子类就是NSKVONotifying_Yinker
。 - 这个新的子类重写了被观察属性 keyPath 的 setter 方法,setter 方法的实现其实就是 Foundation 框架中的
_NSSetXxxxValueAndNotify
(Xxxx为属性类型),方法内部实现:
void _NSSetXxxxValueAndNotify() {
[self willChangeValueForKey:@"key"];
[super setKey:key];
[self didChangevlueForKey:@"key"];
}
1. 被观察属性发生改变之前,'willChangeValueForKey:'被调用,通知系统该 keyPath 的属性值即将变更;
2. 调用父类的'setKey:'方法;
3. 当改变发生后,'didChangeValueForKey:'被调用,通知系统该 keyPath 的属性值已经变更;
4. 最后'didChangeValueForKey:'会调用'observeValueForKey:ofObject:change:context:'。
- 被观察对象的
isa
指针会被修改成指向新创建的子类,被观察对象也就成了新创建的子类的实例。 - Apple 重写了
class
方法,隐藏新创建的子类,通过class
方法获取的还是原来的类。 - Apple 重写了
dealloc
方法。 - Apple 重写了
_isKVOA
方法。
接下来进行一个简单的验证:
在三个断点处获取
[yinker class]
和object_getClass(yinker)
的输出内容:
(lldb) po [yinker class]
Yinker
(lldb) po object_getClass(yinker)
Yinker
(lldb) po [yinker class]
Yinker
(lldb) po object_getClass(yinker)
NSKVONotifying_Yinker
(lldb) po [yinker class]
Yinker
(lldb) po object_getClass(yinker)
Yinker
从上面可以看出来,在添加观察者之后,对象 isa 指向了NSKVONotifying_Yinker
,证明了确实新生成了一个新的子类。但是通过class
获取的还是Yinker
,这就验证了上面说的 Apple 重写了class
方法,隐藏新创建的子类。而在移除观察者之后,又变回了原来的样子。
2.1 如果手动去触发KVO?
- 当前对象手动调用
willChangeValueForKey:
和didChangeValueForKey:
2个方法; - 虽然是
didChangeValueForKey:
内部调用的observeValueForKey:ofObject:change:context:
,willChangeValueForKey:
也必须要调用,因为didChangeValueForKey:
内部有判断willChangeValueForKey:
是否被调用。
3.KVO 不好用的地方
- 如果我想要观察几个不同的属性,就只能在
-observeValueForKeyPath:ofObject:change:context:
对keyPath
做判断,一堆代码摞在一起。。。 - 我只能重写
-observeValueForKeyPath:ofObject:change:context:
方法来获得属性的变化,并不能使用自己想要自定义使用的方式。 - 如果父类同样监听同一个对象的同一个属性,但是我并不想父类也做出相应,这个时候就需要使用
context
来进行区分,在-addObserver:forKeyPath:options:context:
传进去一个父类不知道的context
就成实现,虽然使用context
这个参数可以干这个,但是总感觉这个使用方式有些繁琐。
所以,我们就自定义一个我们自己用起来方便的 KVO。
4.自定义实现 KVO
首先创建NSObject
的category
,添加两个自定义方法,分别是添加观察者和移除观察者,详情如下:
#import <Foundation/Foundation.h>
/**
属性变化后执行的block
@param observedObject 需要被观察的对象
@param observedKey 观察的属性
@param oldValue 属性旧值
@param newValue 属性新值
*/
typedef void(^CJObservingBlock)(id observedObject, NSString * observedKey, id oldValue, id newValue);
@interface NSObject (CJKVO)
/**
添加观察者
@param observer 需要添加的观察者
@param key 观察的属性
@param block 属性变化后执行的block
*/
- (void)CJ_addObserver:(NSObject *)observer
forKey:(NSString *)key
withBlock:(CJObservingBlock)block;
/**
移除观察者
@param observer 需要移除的观察者
@param key 观察的属性
*/
- (void)CJ_removeObserver:(NSObject *)observer forKey:(NSString *)key;
@end
然后我们的主要思路就是在CJ_addObserver:forKey:withBlock:
方法的实现当中:
- 根据
key
得到setter
方法,判断对象的类有没有相应的setter
方法,如果没有则返回。 - 获取当前类的
name
,如果当前类不是kvo子类
,那么就去生成 kvo子类,然后让 isa 指向kvo子类
。 - 如果
kvo子类
没有对应的setter
方法,则添加自定义的setter
方法。(同一个key
可能会被添加多次)。 - 添加观察者集合,并关联观察者集合的数组,存储所有的观察者集合。
接下来,就开始上代码和详细注释:
/*
CJ_addObserver:forKey:withBlock:方法的实现
添加观察者
*/
- (void)CJ_addObserver:(NSObject *)observer forKey:(NSString *)key withBlock:(CJObservingBlock)block {
//1.检查对象的类有没有相应的 setter 方法
SEL setterSelector = NSSelectorFromString([self setter:key]);
// 因为重写了 class,所以[self class]获取的一直是父类
Method setterMethod = class_getInstanceMethod([self class], setterSelector);
if (!setterMethod) {
NSLog(@"key 没有相应的 setter 方法");
return;
}
// 获取当前类的 name
Class clazz = object_getClass(self);
NSString * clazzName = NSStringFromClass(clazz);
// 如果当前类不是 kvo子类。(如果添加了多次观察者,kvo子类在第一次添加观察者的时候就创建了)
if (![clazzName hasPrefix:kCJKVOClassPrefix]) {
// 生成 kvo子类
clazz = [self setKVOClassWithOriginalClassName:clazzName];
// 让 isa 指向 kvo子类
object_setClass(self, clazz);
}
// 如果 kvo子类 没有对应的 setter 方法,则添加。(同一个 key 可能会被添加多次)
if (![self hasSelector:setterSelector]) {
const char * types = method_getTypeEncoding(setterMethod);
class_addMethod(clazz, setterSelector, (IMP)kvo_setter, types);
}
// 创建观察者组合
CJObservation * observation = [[CJObservation alloc] initWithObserver:observer key:key block:block];
// 获取所有观察者组合
NSMutableArray * observations = objc_getAssociatedObject(self, (__bridge const void *)(kCJKVOObservations));
if (!observations) {
observations = [NSMutableArray array];
// 添加关联所有观察者组合
objc_setAssociatedObject(self, (__bridge const void *)(kCJKVOObservations), observations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[observations addObject:observation];
}
详细解析:
1.首先我们根据传进来的key
的首字符大写,然后在前面拼上set
,也就变成了setKey:
的样子,然后再用class_getInstanceMethod
去获得setKey:
的实现(Method)
,如果没有就返回,具体代码如下:
//获取 setter 方法
- (NSString *)setter:(NSString *)key {
if (key.length <= 0) {
return nil;
}
// key 第一个大写
NSString * firstStr = [[key substringToIndex:1] uppercaseString];
// 截取 key 第二到最后
NSString * remainingStr = [key substringFromIndex:1];
// 拼接成 setter
NSString * setter = [NSString stringWithFormat:@"set%@%@:", firstStr, remainingStr];
return setter;
}
2.获取当前类的name
,如果当前类不是kvo子类
,那么就去生成kvo子类
,然后通过object_setClass()
让isa
指向kvo子类
。动态创建子类具体代码以及详细注释如下:
// 生成 kvo子类
- (Class)setKVOClassWithOriginalClassName:(NSString *)originalClazzName {
//1.拼接 kvo 子类并生成
NSString * kvoClazzName = [NSString stringWithFormat:@"%@%@",kCJKVOClassPrefix,originalClazzName];
Class kvoClazz =NSClassFromString(kvoClazzName);
//2.如果已经存在则返回
if (kvoClazz) {
return kvoClazz;
}
//3.如果不存在,则传一个父类,类名,然后额外的空间(通常为 0),它返回给你一个子类。
Class originalClazz = object_getClass(self);
kvoClazz = objc_allocateClassPair(originalClazz, kvoClazzName.UTF8String, 0);
//4.重写了 class 方法,隐藏这个新的子类
Method clazzMethod = class_getInstanceMethod(originalClazz, @selector(class));
const char * types = method_getTypeEncoding(clazzMethod);
class_addMethod(kvoClazz, @selector(class), (IMP)kvo_class, types);
//5.注册到 runtime 告诉 runtime 这个类的存在
objc_registerClassPair(kvoClazz);
return kvoClazz;
}
// 获取当前类的父类
static Class kvo_class(id self, SEL _cmd) {
return class_getSuperclass(object_getClass(self));
}
3.通过hasSelector
判断kvo子类
有没有对应的setter
方法,如果没有,则添加自定义的setter
方法。加这一步判断的原因是因为如果同一个key
可能会被添加多次,那么再添加完第一次之后它的setter
方法就会存在了,不需要重新添加。使用class_addMethod()
动态添加setter
方法 ,并自定义完成这个方法的实现kvo_setter
,具体代码和详细注释如下:
// 是否包含 selector 方法
- (BOOL)hasSelector:(SEL)selector {
Class clazz = object_getClass(self);
unsigned int methodCount = 0;
// 获取方法列表
Method* methodList = class_copyMethodList(clazz, &methodCount);
for (unsigned int i = 0; i < methodCount; i++) {
SEL thisSelector = method_getName(methodList[i]);
if (thisSelector == selector) {
free(methodList);
return YES;
}
}
free(methodList);
return NO;
}
// 实现 setter 方法
static void kvo_setter(id self, SEL _cmd, id newValue) {
// 根据 setter 获取 getter,_cmd 代表本方法的名称
NSString * setterName = NSStringFromSelector(_cmd);
NSString * getterName = [self getter:setterName];
if (!getterName) {
NSLog(@"key 没有相应的 getter 方法");
return;
}
// 根据 key 获取对应的旧值
id oldValue = [self valueForKey:getterName];
// 构造 objc_super 的结构体
struct objc_super superclazz = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self)),
};
// 对 objc_msgSendSuper 进行类型转换,解决编译器报错的问题
void (* objc_msgSendSuperCasted)(void *, SEL, id) = (void *)objc_msgSendSuper;
// id objc_msgSendSuper(struct objc_super *super, SEL op, ...) ,传入结构体、方法名称,和参数等
objc_msgSendSuperCasted(&superclazz, _cmd, newValue);
// 调用之前传入的 block
NSMutableArray * observations = objc_getAssociatedObject(self, (__bridge const void *)(kCJKVOObservations));
for (CJObservation * observation in observations) {
if ([observation.key isEqualToString:getterName]) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 此处是处于子线程,如果在 block 内需要处理 UI 的话,记得回到主线程
observation.block(self, getterName, oldValue, newValue);
});
}
}
}
//获取 getter 方法字符串
- (NSString *)getter:(NSString *)setter {
if (setter.length <=0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) {
return nil;
}
// 先截掉 set,获取后面属性字符
NSRange range = NSMakeRange(3, setter.length - 4);
NSString * key = [setter substringWithRange:range];
// 把第一个字符换成小写
NSString * firstStr = [[key substringToIndex:1] lowercaseString];
key = [key stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstStr];
return key;
}
4.最后,我们创建观察者集合,并关联观察者集合的数组,存储所有的观察者集合。这个所谓的观察者集合就是指存储了CJ_addObserver:forKey:withBlock:
方法的三个参数的对象,方便我们管理。详细内容如下:
@interface CJObservation : NSObject
// 观察者
@property (nonatomic, weak) NSObject *observer;
// 属性key
@property (nonatomic, copy) NSString *key;
// 回调block
@property (nonatomic, copy) CJObservingBlock block;
@end
@implementation CJObservation
- (instancetype)initWithObserver:(NSObject *)observer key:(NSString *)key block:(CJObservingBlock)block {
self = [super init];
if (self) {
_observer = observer;
_key = key;
_block = block;
}
return self;
}
@end
当然,添加观察者就需要移除它,不然会造成内存泄漏的,而且在所有的观察者全部移除之后,再把对象的isa
指针重新指向它原本的类。移除方法CJ_removeObserver:forKey:
具体实现如下:
// 移除观察者
- (void)CJ_removeObserver:(NSObject *)observer forKey:(NSString *)key {
// 获取所有观察者组合
NSMutableArray * observations = objc_getAssociatedObject(self, (__bridge const void *)(kCJKVOObservations));
// 根据 key 移除观察者组合
CJObservation * observationShouldRemove;
for (CJObservation * observation in observations) {
if (observation.observer == observer && [observation.key isEqual:key]) {
observationShouldRemove = observation;
break;
}
}
[observations removeObject:observationShouldRemove];
//在移除所有观察者之后,让对象的 isa 指针重新指向它原本的类
if (observations && observations.count == 0) {
// 获取当前类的 name
Class clazz = object_getClass(self);
NSString * clazzName = NSStringFromClass(clazz);
// 如果当前类是 kvo子类
if ([clazzName hasPrefix:kCJKVOClassPrefix]) {
// 获取对象原本的类
clazz = NSClassFromString([clazzName substringFromIndex:kCJKVOClassPrefix.length]);
// 让 isa 指向原本的类
object_setClass(self, clazz);
}
}
}
接下来,我们就来看一下这个自定义的 KVO 好不好用吧。
@interface Yinker : NSObject
@property (nonatomic, copy) NSString * name;
@property (nonatomic, copy) NSString * job;
@end
#import "Yinker.h"
@implementation Yinker
@end
@interface ViewController ()
@property (nonatomic, strong) Yinker * yinker;
@property (weak, nonatomic) IBOutlet UILabel *label;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Yinker * yinker = [[Yinker alloc] init];
yinker.name = @"HH";
// 添加观察者
[yinker CJ_addObserver:self forKey:@"name" withBlock:^(id observedObject, NSString *observedKey, id oldValue, id newValue) {
NSLog(@"%@,%@,%@,%@,%@",[NSThread currentThread],observedObject,observedKey,oldValue,newValue);
dispatch_async(dispatch_get_main_queue(), ^{
self.label.text = newValue;
});
}];
_yinker = yinker;
}
- (IBAction)modifyName:(id)sender {
// 修改属性值
_yinker.name = @"CJ";
// 移除观察者
[_yinker CJ_removeObserver:self forKey:@"name"];
}
下图点击按钮之后控制台输出:
2017-11-14 CJKVO[63387:4285714] <NSThread: 0x60400027a6c0>{number = 3, name = (null)},<Yinker: 0x60000000e490>,name,HH,CJ
效果已经实现了,如果观察多个属性值的时候,我们就可以在每一个
block
内对不同的属性做不同的处理,哈哈,好用吧。
接着我们再像上面一样加断点查看一下对象的类:
在每一个断点的输出内容为如下:
(lldb) po [_yinker class]
Yinker
(lldb) po object_getClass(_yinker)
Yinker
(lldb) po [_yinker class]
Yinker
(lldb) po object_getClass(_yinker)
CJKVONotifying_Yinker
(lldb) po [_yinker class]
Yinker
(lldb) po object_getClass(_yinker)
CJKVONotifying_Yinker
(lldb) po [_yinker class]
Yinker
(lldb) po object_getClass(_yinker)
CJKVONotifying_Yinker
(lldb) po [_yinker class]
Yinker
(lldb) po object_getClass(_yinker)
Yinker
和系统的观察者模式类似,也是在添加了观察者模式之后类变成了CJKVONotifying_Yinker
,而在移除一个观察的属性之后,对象的类也是和系统的观察者模式一样并没有变化,而在移除所有的观察属性之后,对象的类又变回了原来的类Yinker
。
完整的demo在这里,希望能对大家有所帮助,水平有限,有错误请指出。
参考:
iOS--KVO的实现原理与具体应用
如何自己动手实现 KVO
Key-Value Observing Done Right