作为 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 - private
void _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 能够同时应用,无抵触。