KVO全称Key-Value Observing,键值监听。
基本使用和原理:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
NSLog(@"%@", change);
}
- (void)KVO {
Person *p0 = [[Person alloc] init];
Person *p1 = [[Person alloc] init];
NSLog(@"before addObserver");
NSLog(@"%@-%p", object_getClass(p0), object_getClass(p0));//Person
NSLog(@"%@-%p", object_getClass(p1), object_getClass(p1));//Person
NSLog(@"%d", object_getClass(p0) == object_getClass(p1));//1
[p0 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
NSLog(@"after addObserver");
NSLog(@"%@-%p", object_getClass(p0), object_getClass(p0));//NSKVONotifying_Person
NSLog(@"%@-%p", object_getClass(p1), object_getClass(p1));//Person
NSLog(@"%d", object_getClass(p0) == object_getClass(p1));//0
p0.name = @"Jack";
}
KVO的本质:
当我们给对象注册一个观察者添加了KVO监听时,系统会修改这个对象的isa指针指向。在运行时,动态创建一个新的子类,NSKVONotifying_A类,将A的isa指针指向这个子类,来重写原来类的set方法;set方法实现内部会顺序调用willChangeValueForKey方法、原来的setter方法实现、didChangeValueForKey方法,而didChangeValueForKey方法内部又会调用监听器的observeValueForKeyPath:ofObject:change:context:监听方法。
手动触发KVO:
实现调用willChangeValueForKey和didChangeValueForKey方法。
//手动触发
[p0 willChangeValueForKey:@"name"];
[p0 didChangeValueForKey:@"name"];
自定义KVO
Person.h
#import
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *gender;
- (void)custom_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
@end
NS_ASSUME_NONNULL_END
Person.m
#import "Person.h"
#import
#import
static const char *custom_observer = "custom_observer";
static const char *custom_getter = "custom_getter";
static const char *custom_setter = "custom_setter";
@implementation Person
void setMethod(id self, SEL _cmd, id newValue) {
//getter
NSString *getterName = objc_getAssociatedObject(self, custom_getter);
//setter
NSString *setterName = objc_getAssociatedObject(self, custom_setter);
Class class = [self class];
//isa指向原类
object_setClass(self, class_getSuperclass(class));
//获取旧值
id oldValue = objc_msgSend(self, NSSelectorFromString(getterName));
//调用原类set方法
objc_msgSend(self, NSSelectorFromString([setterName stringByAppendingString:@":"]), newValue);
id observer = objc_getAssociatedObject(self, custom_observer);
NSMutableDictionary *change = [NSMutableDictionary dictionary];
if (newValue)
change[NSKeyValueChangeNewKey] = newValue;
if (oldValue)
change[NSKeyValueChangeOldKey] = oldValue;
objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:), getterName, self, change, nil);
object_setClass(self, class);
}
- (void)custom_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
//创建指定前缀CustomKVONotifying_%@的子类并注册
NSString *curClassName = NSStringFromClass([self class]);
NSString *customClassName = [NSString stringWithFormat:@"CustomKVONotifying_%@", curClassName];
Class customClass = objc_getClass(customClassName.UTF8String);
if (!customClass) {
customClass = objc_allocateClassPair([self class], customClassName.UTF8String, 0);
objc_registerClassPair(customClass);
}
//set方法首字母大写
NSString *keyPathChange = [[[keyPath substringToIndex:1] uppercaseString] stringByAppendingString:[keyPath substringFromIndex:1]];
NSString *setNameStr = [NSString stringWithFormat:@"set%@", keyPathChange];
SEL setSEL = NSSelectorFromString([setNameStr stringByAppendingString:@":"]);
//添加set方法
Method getMethod = class_getInstanceMethod([self class], @selector(keyPath));
const char *types = method_getTypeEncoding(getMethod);
class_addMethod(customClass, setSEL, (IMP)setMethod, types);
//修改isa
object_setClass(self, customClass);
//保存observer
objc_setAssociatedObject(self, custom_observer, observer, OBJC_ASSOCIATION_ASSIGN);
//保存set、get方法名
objc_setAssociatedObject(self, custom_setter, setNameStr, OBJC_ASSOCIATION_COPY_NONATOMIC);
objc_setAssociatedObject(self, custom_getter, keyPath, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end
Demo地址:KVO