作为 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
#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
其中 MRCEasyKVOTools 用到了 MRC 环境下的 API:
/*
只能在 MRC 环境编译
Build Phases 中 MRCEasyKVOTools.m 添加 -fno-objc-arc
*/
#import "MRCEasyKVOTools.h"
#import
@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可以同时使用,无冲突。