上一篇我们分析了kvo的原理,那么我们是否可以自定义一个自己的kvo
呢?主要大致思路
:
添加
观察者,创建中间类,isa
替换。- 中间类
setter实现
包括回调,父类setter
方法调用。 移除
操作,isa还原
。
1.中间类创建
我们基于NSObject创建自定义KVO分类
@interface NSObject (KBKVO)
//添加
- (void)kb_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
//观察回调
-(void)kb_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context;
//移除
- (void)kb_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
@end
复制代码
1.1 setter方法的判断
我们的kvo
是基于对setter方法的监听
,所以我们添加观察者,先要确保keypath
是否实现setter方法
。
[self judgeMethodIsExist:keyPath];//setter方法是否实现
-(void)judgeMethodIsExist:(NSString*)keyPath
{
Class class = object_getClass(self);
NSString *setKey = [NSString stringWithFormat:@"set%@:",keyPath.capitalizedString];
SEL sel = NSSelectorFromString(setKey);
Method method = class_getInstanceMethod(class, sel);
if (!method) {
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"没实现%@的setter方法,无法使用kvo",keyPath] userInfo:nil];
}
}
复制代码
运行一下,我们先观察KBPerson
的成员变量nickName
,没有实现setter
方法,进入报错逻辑
1.2 中间类实现
-(Class)createSubClassWithKeyPath:(NSString*)keyPath
{
NSString *superClassName = NSStringFromClass(self.class);
NSString *associateClassName = [NSString stringWithFormat:@"%@%@",kKBKVOProfix,superClassName];
Class associateClass = NSClassFromString(associateClassName);
if (associateClass) {
return associateClass;//1.存在就直接返回不用在创建了
}
associateClass = objc_allocateClassPair(self.class, associateClassName.UTF8String, 0);//2.1申请类
objc_registerClassPair(associateClass);//2.2注册
// 2.3.1 : 添加class : class的指向是KBPerson
SEL classSEL = NSSelectorFromString(@"class");
Method classMethod = class_getInstanceMethod([self class], classSEL);
const char *classTypes = method_getTypeEncoding(classMethod);
class_addMethod(associateClass, classSEL, (IMP)kb_class, classTypes);
// 2.3.2 setter方法添加
NSString *setKey = [NSString stringWithFormat:@"set%@:",keyPath.capitalizedString];
SEL sel = NSSelectorFromString(setKey);//方法名
Method method = class_getInstanceMethod(self.class, sel);
const char *type = method_getTypeEncoding(method);//方法类型
class_addMethod(associateClass,sel, (IMP)kb_setter, type);//添加方法
return associateClass;
}
Class kb_class(id self,SEL _cmd){
return class_getSuperclass(object_getClass(self));
}
static void kb_setter(id self,SEL _cmd,id newValue){
NSLog(@"自定义来了:%@",newValue);
}
复制代码
- 我们先判断
是否已经存在
,不存在再去创建。 申请
关联类,之后进行注册
关联类。- 给关联类添加
setter
方法,并实现它。
1.3 中间类指向实例
- (void)kb_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{
//1.判读setter方法实现
[self judgeMethodIsExist:keyPath];
//2. 实现中间类
Class newClass = [self createSubClassWithKeyPath:keyPath];
//3. isa指向关联类
object_setClass(self, newClass);
}
复制代码
我们观察属性name
,进入了中间类的setter
方法。
2. setter方法实现
2.1 正常逻辑
按照理解我们会发通知告诉系统改变了
属性值,同时调用父类的setter
方法,打印如下:
题外:调用系统进行消息发送的时候会报错 Too many arguments to function call, expected 0, have 3
我们按图修改下。
也可以 objc_msgSendSuper
函数是无返回参数的函数,需要把它强制转化
类型
void (*kb_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
// objc_msgSendSuper(&superStruct,_cmd,newValue);
kb_msgSendSuper(&superStruct,_cmd,newValue);
复制代码
之后进行通知观察者回调
,我们就需要对观察者发送消息
。首先保存观察者
NSObject *oldObserver = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kKBKVOAssociateKey));
if (!oldObserver) {
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kKBKVOAssociateKey), observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
复制代码
之后进行调用,但是又要记录值的变化
,,记录keypath
传过去很麻烦。
SEL observerSEL = @selector(kb_observeValueForKeyPath:ofObject:change:context:);
objc_msgSend(info.observer,observerSEL,keyPath,self,change,NULL);
复制代码
2.2 函数式编程处理
我们日常iOS开发中是面向对象
开发,基于mvc
的架构模式,有的会进行mvvm
和mvp
模式降低耦合
性。但是有的时候模块之间还是有关联性
,难以解耦合,使用函数式
编程就可以解决部分耦合
问题。
比如:函数f=x()
,我们可以把整个函数作为一个参数
进行传递。f = x(x())
。在iOS中我们通常把block
作为参数也是一种体现。关于函数式思想,以及链式语法有机会写一篇探讨下。
- 我们直接把通知回调写在添加方法中,这样可以是我们的
逻辑
更加紧密,RAC框架
对这样block
回调封装了很多系统的功能,比如监测textField
的变化直接block回调
,button
点击block回调
等。
typedef void(^KBKVOBlock)(NSObject* observer,NSString*keyPath,id oldValue,id newValue);
-(void)kb_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(KBKVOBlock)block;
复制代码
首先我们需要保存observer
,keypath
,option
,我们创建一个类专门保存这些信息
//仿照系统
@interface KBKVOInfo : NSObject
@property (nonatomic, weak) NSObject *observer;//防止循环引用
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, copy) KBKVOBlock handleBlock;
- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handBlock:(KBKVOBlock)block;
@end
@implementation KBKVOInfo
- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handBlock:(KBKVOBlock)block{
if (self=[super init]) {
_observer = observer;
_keyPath = keyPath;
_handleBlock = block;
}
return self;
}
@end
复制代码
我们在添加观察者的时候把observer
,keyPath
和block
放在info
中并保存info
-(void)kb_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(KBKVOBlock)block
{
****************************************关联*******************
KBKVOInfo *info = [[KBKVOInfo alloc]initWitObserver:observer forKeyPath:keyPath handBlock:block];
//4.2保存,考虑复用性,我们把info放到一个数组进行关联
NSMutableArray *arr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kKBKVOAssociateKey));
if (!arr) {
arr = [NSMutableArray arrayWithCapacity:1];//第一次
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kKBKVOAssociateKey), arr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[arr addObject:info];
}
复制代码
具体实现kb_setter
:
static void kb_setter(id self,SEL _cmd,id newValue){
//1.计算旧值
NSString *keyPath = getterForSetter( NSStringFromSelector(_cmd));
id oldValue = [self valueForKey:keyPath];
//1.调用父类的方法
Class superClass = class_getSuperclass(object_getClass(self));
struct objc_super superStruct = {
.receiver = self,
.super_class = superClass,
};
void (*kb_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
// objc_msgSendSuper(&superStruct,_cmd,newValue);
kb_msgSendSuper(&superStruct,_cmd,newValue);
NSLog(@"自定义来了:%@",newValue);
//2.告诉系统将要改变
NSMutableArray *arr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kKBKVOAssociateKey));
for (KBKVOInfo *info in arr) {
if (info.handleBlock && [info.keyPath isEqualToString:keyPath]) {
info.handleBlock(info.observer, keyPath, oldValue, newValue);
}
}
}
复制代码
取出关联数组,根据keyPath
找到info
对象,调用回调
。
3. 移除观察者,还原isa
三部曲最后一步,移除
。我们知道removeObserver
主要做了将isa还原
。由于我们自定义关联对象的数组中存在保存的KBKVOInfo
信息,因此要进行移除操作。
- (void)kb_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath
{
NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kKBKVOAssociateKey));
if (observerArr.count<=0) {
return;
}
for (KBKVOInfo *info in observerArr) {
if ([info.keyPath isEqualToString:keyPath]) {
[observerArr removeObject:info];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kKBKVOAssociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
break;
}
}
if (observerArr.count<=0) {
// 指回给父类
Class superClass = [self class];
object_setClass(self, superClass);
}
}
//调用
-(void)dealloc
{
[self.person kb_removeObserver:self forKeyPath:@"name"];
}
复制代码
4. 自动移除
由于我们要每次dealloc
中手动去移除操作,那么是否能有一种自动
的方式呢?
我们监听子类的dealloc
方法,当被观察者调用dealloc
的时候,我们去调用移除操作
。
之前我们了解过methd-swizzing
方法交换,我们可以把子类的dealloc
进行交换。
+(void)load
{
//交换
[self kb_hookOrigInstanceMenthod:NSSelectorFromString(@"dealloc") newInstanceMenthod:@selector(customeDealloc)];
}
-(void)customeDealloc
{
//还原isa
Class superClass = [self class];
object_setClass(self, superClass);
}
//方法交换,具体可了解methd-swizzing
+ (BOOL)kb_hookOrigInstanceMenthod:(SEL)oriSEL newInstanceMenthod:(SEL)swizzledSEL {
Class cls = self;
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
if (!swiMethod) {
return NO;
}
if (!oriMethod) {
class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ }));
}
BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
if (didAddMethod) {
class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else{
method_exchangeImplementations(oriMethod, swiMethod);
}
return YES;
}
复制代码
这样写有2个问题
- 我们分类重写会导致
所有的类
都交换了。 +load
方法里面的代码不一定只走一次
。
+(void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//只执行一次,防止交换2的倍数,交换回来了。
[self kb_hookOrigInstanceMenthod:NSSelectorFromString(@"dealloc") newInstanceMenthod:@selector(customeDealloc)];
});
}
-(void)customeDealloc
{
Class superClass = [self class];
object_setClass(self, superClass);
[self customeDealloc];//交换后imp,再次调用实现原有的dealloc方法。
}
复制代码
修改后:1. 交换后imp
,再次调用实现原有的dealloc
方法,不影响原有方法调用。
2. 防止主动调用
,使用单例的形式只执行一次
,防止交换2的倍数,交换回来了。
5. 总结
kvo
的自定义,大致分为3个步骤
- 中间类的
创建
(setter
方法是否实现, 中间类的实现,isa
指向中间类) setter
方法实现(通过block
的回调,进行处理)移除
(方法交换,实现自动移除,还原isa
)
过程中我们学习函数式编程
的思想,当然有些不是完善
的地方,比如自动移除的时候没有移除关联数组
中的保存的信息。Facebook
实现了git
上有关于kvo
的自定义实现,感兴趣的可以看下 KVOController。