KVO(Key-Value Observing)提供一种机制,当指定对象的属性被修改后,就会通知观察者。简单的说就是每次指定的被观察的对象的属性被修改后,KVO就会自动通知相应的观察者了。
KVO其实也是“观察者”设计模式的一种应用。这种模式有利于两个类间的解耦合,尤其是对于业务逻辑与视图控制这两个功能的解耦合。
假设已经有一个Person类,包含name属性,我们可以通过KVO监听到name属性被修改的事件:
_p = [Person new];
[_p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
即当_p对象的name属性被修改后,会调用self的以下方法:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionaryid> *)change context:(void *)context{
NSLog(@"kvo监听到%@对象的%@属性被修改为%@",object,keyPath,[change objectForKey:@"new"]);
}
当调用以下函数前:
[_p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
我们可以看到_p的isa指针指向类Person:
但是当单步执行过后,_p的isa指针会变为指向NSKVONotifying_Person类
:
即对于上面的代码,系统会执行如下操作:
1、利用runtime动态创建Person类的子类NSKVONotifying_Person,重写name属性的set方法:
-(void)setName:(NSString*)name{
[self willChangeValueForKey:@"name"];
[super setName:name];
[self didChangeValueForKey:@"name"];
}
2、将p对象的isa指针指向新建的子类
当我们修改_p的name属性时,会执行NSKVONotifying_Person类对象的set方法。
我们可以按照上面的思路,自己用代码实现KVO的基本功能,同时优化为以block的方式进行回调而非使用-observeValueForKeyPath:ofObject:change:context:方法。
1.新建一个NSObjct的Category,声明添加监听和移除监听方法:
NSObject+KVO.h
typedef void(^JXObserveingBlock)(id observer,NSString *key,id oldValue,id newValue);
@interface NSObject (KVO)
-(void)JX_addObserver:(NSObject *)observer forKeyPath:(NSString *)key block:(JXObserveingBlock*)block;
-(void)JX_removeObserver:(NSObject *)observer forKey:(NSString *)key;
@end
2.实现JX_addObserver:forKeyPath:block:方法:
-(void)JX_addObserver:(NSObject *)observer forKeyPath:(NSString *)key block:(JXObserveingBlock)block{
//1.检查对象是否存在对应的set方法,没有则抛出异常。
SEL setterSelector = NSSelectorFromString([self setterForGetter:key]);
Method setterMethod = class_getInstanceMethod([self class], setterSelector);
if (!setterMethod) {
// throw invalid argument exception
}
//2.检查isa指针指向的类是否已经是KVO的类,不是则新建原类的子类,并把isa指针指向新类。
Class class = object_getClass(self);
NSString *className = NSStringFromClass(class);
if (![className hasPrefix:kJXKVOClassPrefix]) {
class = [self makeKvoClassWithOriginalClassName:className];
object_setClass(self, class);
}
//3.检查KVO类是否已经重写了set方法,没有则重写set方法。
if (![self hasSelector:setterSelector]) {
const char *types = method_getTypeEncoding(setterMethod);
class_addMethod(class, setterSelector, (IMP)kvo_setter, types);
}
//添加观察者
JXObservationInfo *info = [[JXObservationInfo alloc] initWithObserver:observer Key:key block:block];
NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void*)kJXKVOAssociatedObservers);
if (!observers) {
observers = [NSMutableArray array];
objc_setAssociatedObject(self, (__bridge const void*)kJXKVOAssociatedObservers, observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[observers addObject:info];
}
其中setterForGetter:方法是获取属性对应的set方法名:
-(NSString*)setterForGetter:(NSString*)key{
if (key.length <= 0) {
return nil;
}
//1.首字母大写
key = [key capitalizedStringWithLocale:[NSLocale currentLocale]];
//2.添加set前缀和:后缀
NSString *setter = [NSString stringWithFormat:@"set%@:",key];
return setter;
}
hasSelector:方法判断是否已经重写了set方法
- (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;
}
makeKvoClassWithOriginalClassName:方法根据类名返回kvo类:
- (Class)makeKvoClassWithOriginalClassName:(NSString *)originalClazzName{
NSString *kvoClassName = [kJXKVOClassPrefix stringByAppendingString:originalClazzName];
Class clazz = NSClassFromString(kvoClassName);
if (clazz) {
return clazz;
}
//如果还没创建对应的kvo类,则创建
Class originalClazz = object_getClass(self);
Class kvoClazz = objc_allocateClassPair(originalClazz, kvoClassName.UTF8String, 0);
//重写class方法
Method clazzMethod = class_getInstanceMethod(originalClazz, @selector(class));
const char *types = method_getTypeEncoding(clazzMethod);
class_addMethod(kvoClazz, @selector(class), (IMP)kvo_class, types);
objc_registerClassPair(kvoClazz);
return kvoClazz;
}
static Class kvo_class(id self, SEL _cmd)
{
return class_getSuperclass(object_getClass(self));
}
最后,重写setter 方法:
static void kvo_setter(id self, SEL _cmd, id newValue)
{
NSString *setterName = NSStringFromSelector(_cmd);
NSString *getterName = getterForSetter(setterName);
if (!getterName) {
NSString *reason = [NSString stringWithFormat:@"Object %@ does not have setter %@", self, setterName];
@throw [NSException exceptionWithName:NSInvalidArgumentException
reason:reason
userInfo:nil];
return;
}
id oldValue = [self valueForKey:getterName];
struct objc_super superclazz = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self))
};
// cast our pointer so the compiler won't complain
void (*objc_msgSendSuperCasted)(void *, SEL, id) = (void *)objc_msgSendSuper;
// call super's setter, which is original class's setter method
objc_msgSendSuperCasted(&superclazz, _cmd, newValue);
// look up observers and call the blocks
NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void *)(kJXKVOAssociatedObservers));
for (JXObservationInfo *each in observers) {
if ([each.key isEqualToString:getterName]) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
each.block(self, getterName, oldValue, newValue);
});
}
}
}
static NSString * getterForSetter(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 *firstLetter = [[key substringToIndex:1] lowercaseString];
key = [key stringByReplacingCharactersInRange:NSMakeRange(0, 1)
withString:firstLetter];
return key;
}
把原来的:
[_p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
修改为:
[_p JX_addObserver:self forKeyPath:@"name" block:^(id observer, NSString *key, id oldValue, id newValue) {
NSLog(@"kvo监听到%@对象的%@属性被修改为%@",observer,key,newValue);
}];
在修改_p的name属性之后,会打印出修改信息。
完整代码下载:http://download.csdn.net/detail/dolacmeng/9849000
参考:http://tech.glowing.com/cn/implement-kvo/