作为 iOSer,想必大家对 KVO 并不生疏,其原理概括起来大抵3个步骤:
- 创立派生子类 NSKVONotifying_Person
- 批改被察看对象 p 的 isa 指针,使其指向新类 NSKVONotifying_Person
- 重写 setter 办法,赋值并且告诉观察者 observer 对象 p 的属性值产生了扭转
然而,零碎的 KVO 着实不好用,察看多个属性时,须要在 observeValueForKeyPath:ofObject:change:context:
中写上大量的判断条件,于是,基于以上的KVO实现原理咱们能够自定义KVO实现。
那么,如何自定义KVO?
基于自定义派生类的KVO
零碎办法的派生类 NSKVONotifying_Person,咱们绕过这个零碎派生类,自定义一个派生类,比方叫做 CustomKVO_Person,应用其替换 NSKVONotifying_Person。
这种形式在自定义的办法中是可行的,然而一旦和零碎的 addObserver:forKeyPath:options:context:
一起应用就会crash;
其解决方案也是有的,在 iOS大解密:玄之又玄的KVO 一文中,给出的解决方案是:
- 给自定义派生类调配 0x68 空间,拷贝零碎派生类的 indexedIvars 到此空间,保障 setter 时
_NSSetIntValueAndNotify
能正确获取KVO信息,防止其crash; - 借助 FishHook 来 hook 零碎的 object_setClass 操作,判断 isa 指针为自定义派生类且继承于零碎派生类时跳过 setClass 操作,这样一来,即便调用零碎办法也能保障 isa 指针指向自定义派生类,防止自定义KVO生效;
以上两步联合就能够解决自定义派生类KVO与零碎办法混用导致的问题了。
然而,如果仅是自定义一个KVO就要引入 FishHook 的话,这感觉可不太好;那么有没有不必引入任何框架,也能实现KVO并且不与零碎抵触的办法呢?
上面,咱们来看看另一种思路
基于零碎派生类自定义的KVO
这种思路不须要创立自定义的派生类,代码实现上与自定义派生类KVO大同小异,先调用零碎办法生成零碎派生类,再批改零碎派生类的setter办法IMP,调用咱们自定义的block回调。
代码实现如下:
#import "NSObject+EasyKVO.h"#import <objc/message.h>#import "MRCEasyKVOTools.h"typedef void(^_EasyKVOChangedBlock)(id newValue, id oldValue);static NSString * __EasyKVOTipsDic = @"__EasyKVOTipsDic";@implementation NSObject (EasyKVO)#pragma mark - public- (void)addObserver:(NSObject*)observer forKeyPath:(NSString *)keyPath changedBlock:(_EasyKVOChangedBlock)block{ if (!observer || keyPath.length < 1) return; /* 应用零碎办法取得派生类 */ if (![NSStringFromClass(object_getClass(self)) containsString:@"NSKVONotifying_"]) { [self addObserver:observer forKeyPath:keyPath options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil]; } NSString *pairClsName = NSStringFromClass(object_getClass(self)); Class pairCls = NSClassFromString(pairClsName); if (!pairCls) { pairCls = objc_allocateClassPair(object_getClass(self), pairClsName.UTF8String, 0x68); [MRCEasyKVOTools object_copyIndexedIvars:object_getClass(self) toTarget:pairCls size:0x68]; objc_registerClassPair(pairCls); object_setClass(self, pairCls); /* 批改 isa 指针 */ } /* 保留 block 信息 */ [_tipsMap(self, _cmd) setObject:[block copy] forKey:[NSString stringWithFormat:@"_%@_%@_block", NSStringFromClass(object_getClass(self)), keyPath]]; /* 扭转setter办法 */ NSString *format = [keyPath stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:[[keyPath substringToIndex:1] uppercaseString]]; SEL setSel = NSSelectorFromString([NSString stringWithFormat:@"set%@:", format]); Method setMethod = class_getInstanceMethod(object_getClass(self), setSel); if (![self _containSelector:setSel]) { /* 避免增加屡次 */ class_addMethod(object_getClass(self), setSel, (IMP)_setterFunction, method_getTypeEncoding(setMethod)); } else { class_replaceMethod(object_getClass(self), setSel, (IMP)_setterFunction, method_getTypeEncoding(setMethod)); }}- (void)removeObserver:(NSObject *)observer blockForKeyPath:(NSString *)keyPath { [self removeObserver:observer forKeyPath:keyPath]; NSString *blockKeyName = [NSString stringWithFormat:@"_%@_%@_block", @"NSKVONotifying_", keyPath]; NSMutableDictionary *tips = _tipsMap(self, _cmd); [tips removeObjectForKey:blockKeyName];}#pragma mark - privatevoid _setterFunction(id self, SEL _cmd, id newValue) { NSString *setterName = NSStringFromSelector(_cmd); if (setterName.length < 4) return; NSString *format = [setterName substringWithRange:NSMakeRange(3, setterName.length -4)]; NSString *keyPath = [format stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:[[format substringToIndex:1] lowercaseString]]; if (keyPath.length < 1) return; id oldValue = [self valueForKeyPath:keyPath]; if (![oldValue isEqual:newValue]) { //调用父类setter struct objc_super supercls = { .receiver = self, .super_class = class_getSuperclass(object_getClass(self)) }; void (* msgSendSuper)(void *, SEL, id) = (void *)objc_msgSendSuper; msgSendSuper(&supercls, _cmd, newValue); } _EasyKVOChangedBlock block = (_EasyKVOChangedBlock)[_tipsMap(self, _cmd) objectForKey:[NSString stringWithFormat:@"_%@_%@_block", NSStringFromClass(object_getClass(self)), keyPath]]; if (block) block(newValue, oldValue);}NSMutableDictionary *_tipsMap(id self, SEL _cmd) { NSMutableDictionary * _tipsDic = objc_getAssociatedObject(self, &__EasyKVOTipsDic); if (!_tipsDic) { _tipsDic = [[NSMutableDictionary alloc] init]; objc_setAssociatedObject(self, &__EasyKVOTipsDic, _tipsDic, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } return _tipsDic;}- (BOOL)_containSelector:(SEL)selector { Class cls = object_getClass(self); unsigned int count = 0; Method *methods = class_copyMethodList(cls, &count); for (int i=0; i<count; i++) { SEL sel = method_getName(methods[i]); if (selector == sel) { free(methods); return YES; } } free(methods); return NO;}@end
其中 MRCEasyKVOTools 用到了 MRC 环境下的 API:
/* 只能在 MRC 环境编译 Build Phases 中 MRCEasyKVOTools.m 增加 -fno-objc-arc */#import "MRCEasyKVOTools.h"#import <objc/objc.h>@implementation MRCEasyKVOTools+ (void)object_copyIndexedIvars:(id)obj toTarget:(id)targetObj size:(size_t)size { uint64_t *s1 = object_getIndexedIvars(obj); uint64_t *s2 = object_getIndexedIvars(targetObj); memcpy(s2, s1, size);}@end
通过测试,零碎KVO与自定义KVO能够同时应用,无抵触。