KVO探索

KVO归纳

你们都知道kvo是一种设计模式,是一种键值观察,当属性的值改变时候会触发回调,获取该属性的旧值和新值。可是可能有些朋友不清楚何时用它,使用场景是什么。当须要监听一个属性的值改变时候咱们能够用到它。好比:git

  1. 当图片的url改变时候自动加载新的图片。
  2. 当scrollView的offset改变时获得回调获取offset的值,这时候不用delegate较好,特别是封装一个框架时候,若是用delegate那么框架的使用者也可能用它的delegate致使框架的delegate不会执行,可是kvo不会出现这种问题。
  3. 好比监听一段mp3声音进度的属性,根据改变的值来进行UI绘制。
  4. 好比监听一些开关量进行绘制UI等等。可见KVO仍是很经常使用的,也很实用。

KVO的简单使用

  1. 监听Person的对象p1的name属性值的变化
//注册kvo
[p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];

//属性赋值
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [super touchesBegan:touches withEvent:event];
    count++;
    p1.name = [NSString stringWithFormat:@"%d",count];
}

//释放
- (void)dealloc{
    [p1 removeObserver:self forKeyPath:@"name"];
}
复制代码
  1. 手动kvo,能够手动控制是否触发kvo的回调
//重写Person类的automaticallyNotifiesObserversForKey返回NO即关闭了自动kvo
@implementation Person
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
    return NO;
}
@end

//注册kvo
[p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];

//属性值变化(其实只要该对象的成员变量的值改变便可)
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [super touchesBegan:touches withEvent:event];
    count++;
    [p1 willChangeValueForKey:@"name"];
    p1.name = [NSString stringWithFormat:@"%d",count];
    [p1 didChangeValueForKey:@"name"];
}

//kvo回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    
}

//释放
- (void)dealloc{
    [p1 removeObserver:self forKeyPath:@"name"];
}
复制代码

KVO的底层实现探索

系统的kvo是怎么实现的呢?为何只要对象的属性变化了就会触发回调呢?我也很好奇,在查看资料以前,本身也考虑了一下怎么实现。
首先系统的kvo任意一个对象均可以调用addObserver方法,能够肯定应该是NSObject的分类,并新增了这个addObserver的方法。接下来想到的是hook,好比hook Person类对应的setter方法。利用runTime方法交换实如今调用这个setter方法以前获取该属性对应的成员变量的值,获得旧值。以后再调用该setter方法以后再次获取该属性对应的成员变量的值,获得新值。最后再调用observeValueForKeyPath方法把新旧值传递给observe对应的类。github

接下来查看资料,Oh,My God 并非本身想的那样,系统在调用addObserver方法时候动态的建立了一个新的子类继承该被监听的对象所对应的类。并重写了父类的setter方法。并把对象的isa指针从父类指向了该子类。这样当父类的对象调用setter方法时候就会调用子类的setter方法,在该setter方法内部调用了willChangeValueForKey,didChangeValueForKey方法。以后系统会调用observeValueForKeyPath方法,把旧的和新的值传递给oberver所对应的类。在新的子类里除了重写了父类的setter方法之外还重写了class方法,该方法是为了外界调用class时候隐藏新建立的子类。有一点很奇怪当咱们在addObserver方法后打一个断点时并把鼠标光标移动到改对象上会发现它竟然显示的是父类而不是新生成的子类。按理来讲咱们把isa指针指向新的子类后该对象应该就属于子类的实例才对。设计模式

再以后我来验证了一下,打印一下isa指针指向的类bash

NSLog(@"p1:%@",object_getClass(p1));
    [p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
    NSLog(@"p1:%@",object_getClass(p1));
复制代码

打印结果:app

2019-01-29 14:28:40.983065+0800 KVOCustom[20946:172543] p1:Person
2019-01-29 14:28:40.983401+0800 KVOCustom[20946:172543] p1:NSKVONotifying_Person
复制代码

发现确实在addOberver方法调用前是指向了Person类,在调用后指向了新类NSKVONotifying_Person,从而证实了addOberver方法内部确实是建立了新类。
以后固然是想了解下这个系统建立的新的类内部实现了哪些方法啦。本身写了一个打印类内部方法以下:框架

- (void)printMethods:(Class)cls{
    unsigned int count;
    Method *methods = class_copyMethodList(cls, &count);
    NSMutableString *strM = [NSMutableString string];
    [strM appendString:[NSString stringWithFormat:@"%@: ",cls]];
    
    for (int i  = 0; i < count; i++) {
        Method method = methods[i];
        NSString *strMethodName = NSStringFromSelector(method_getName(method));
        [strM appendString:strMethodName];
        [strM appendString:@", "];
    }
    NSLog(@"%@",strM);
}
复制代码

以后咱们调用方法打印函数

[p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
    [p1 printMethods:object_getClass(p1)];
复制代码

打印结果:ui

2019-01-29 14:35:32.435852+0800 KVOCustom[21016:175547] NSKVONotifying_Person: setName:, class, dealloc, _isKVOA,
复制代码

说明系统新建立的类内部实现了setName方法(用来重写父类的setter方法),class方法(为了隐藏子类),dealloc方法(释放内存),_isKVOA方法(是系统的kvo方法)url

当咱们探索到这里基本上也就请楚了系统的kvo的实现。spa

结论

  1. 系统调用addObserver方法时候,内部新建立了一个类NSKVONotifying_Person继承于Person类。而且改变了isa的指向,指向了这个新类。
  2. 新类里边重写了父类的setter方法,这样当person对象的name属性赋值时候,也就是调用了setter方法,这时候会调用子类的setter方法,该方法猜想内部调用了willChangeValueForKey,didChangeValueForKey方法,这样会调用oberver对应类的observeValueForKeyPath方法。将新旧的值都传递过来。
  3. 新类重写了class方法,猜想彻底是为了隐藏子类的实现。当person对象调用class方法时候将返回Person类而不是新的类猜想内部实现应该是这样的:
Class classUse(id self,SEL _cmd){
    return class_getSuperclass(object_getClass(self));
}
复制代码
  1. 新类调用了内部方法_isKVOA,猜想是为了告知系统使用了kvo,内部方法实现猜想是这样的:
int _isKVOAUse(id self,SEL _cmd){
    return YES;
}
复制代码

本身手写一个KVO

如今是否是手痒痒想本身手写个KVO啦,如今咱们清楚了系统KVO的实现,模仿它咱们本身实现一个KVO吧。

  1. 首先肯定这个KVO类确定是NSObject类的分类(由于全部对象均可以调用addObserver方法)在其内部模仿系统的addObserver方法本身写一个相似的
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath valueChangeBlk:(void(^)(id old, id new))valueChangeBlk{
    //建立子类
    NSString *oldClass = NSStringFromClass(self.class);
    NSString *newClass = [NSString stringWithFormat:@"BSKVONotify_%@",oldClass];
    Class classNew = objc_allocateClassPair(self.class, newClass.UTF8String, 16);
    objc_registerClassPair(classNew);
    object_setClass(self, NSClassFromString(newClass));
    
    //新增set方法
    NSMutableString *strM = [NSMutableString string];
    [strM appendString:[[keyPath substringToIndex:1] uppercaseString]];
    [strM appendString:[keyPath substringFromIndex:1]];
    NSString *setMethod = [NSString stringWithFormat:@"set%@:",strM];
    class_addMethod(NSClassFromString(newClass), NSSelectorFromString(setMethod), (IMP)keyPathMethod,"v@:@");
    
    //新增class方法
    class_addMethod(classNew, NSSelectorFromString(@"class"), (IMP)classUse,"#@:");
    
    //新增_isKVOA方法
    class_addMethod(classNew, NSSelectorFromString(@"_isKVOA"),(IMP)_isKVOAUse, "i@:");
    
    //设置关联对象
    objc_setAssociatedObject(self, "keyPath", keyPath, OBJC_ASSOCIATION_COPY);
    objc_setAssociatedObject(self, "blk", valueChangeBlk, OBJC_ASSOCIATION_COPY);
    objc_setAssociatedObject(self, "classNew", classNew, OBJC_ASSOCIATION_RETAIN);
    objc_setAssociatedObject(self, "classOld", self.class, OBJC_ASSOCIATION_RETAIN);
}
复制代码
  1. 在写一个对应的setter方法的函数做为属性值变化的回调
void keyPathMethod(id self,IMP _cmd, id arg){
    //set方法名,原始类和子类
    NSString *keyPath = objc_getAssociatedObject(self, "keyPath");
    NSMutableString *strM = [NSMutableString string];
    [strM appendString:[[keyPath substringToIndex:1] uppercaseString]];
    [strM appendString:[keyPath substringFromIndex:1]];
    NSString *setMethod = [NSString stringWithFormat:@"set%@:",strM];
    Class subClass = objc_getAssociatedObject(self, "classNew");
    Class oldClass = objc_getAssociatedObject(self,"classOld");
    
    //isa指针指向父类,执行set方法
    object_setClass(self, oldClass);
    //获取成员变量的值
    Ivar ivar = class_getInstanceVariable([self class], [NSString stringWithFormat:@"_%@",keyPath].UTF8String);
    id value = object_getIvar(self, ivar);
   // NSLog(@"old:%@",value);
    ((id (*) (id,SEL,id))objc_msgSend)(self,NSSelectorFromString(setMethod),arg);
    id valueNew = arg;
   // NSLog(@"new:%@",valueNew);
    
    //isa指针指向子类
    object_setClass(self, subClass);
    
    void(^blkUse)(id old, id new) = objc_getAssociatedObject(self, "blk");
    if (blkUse) {
        blkUse(value,valueNew);
    }
}
复制代码

以上方法便可实现一个简单的kvo了 3. 重写class方法来隐藏内部子类

Class classUse(id self,SEL _cmd){
    return class_getSuperclass(object_getClass(self));
}

复制代码
  1. 新增_isKVO方法
int _isKVOAUse(id self,SEL _cmd){
    return YES;
}
复制代码

上述方法便可实现一个kvo了,拿去用吧!

关于系统KVO和本身实现的KVO对比的疑惑

  1. 最开始有提到,系统的kvo当咱们打断点时候发现系统的kvo对象竟然是父类就是原始类而不是新的子类。而咱们的kvo对象打断点发现是新的子类而不是原始类。也就是说系统很好的隐藏了子类,而咱们写的kvo却作不到,目前还不清楚这个原理。
  2. void keyPathMethod(id self,IMP _cmd, id arg)你们有没有注意到这里参数写的是id类型,这里的参数对应接收setter方法传递进来的参数。这个参数目前接收的是id类型,这样就有局限了,这样监听的对象的属性类型就必定是对象类型了,若是是基本类型就会崩溃。这里使用时候也要注意了,目前我不清楚该怎么改,怎么能同时接收两种类型。
  3. 注意循环引用的问题
p1 = [[Person alloc] init];
    __weak typeof(self) weakSelf = self;
    [p1 addObserver:self forKeyPath:@"name" valueChangeBlk:^(id  _Nonnull old, id  _Nonnull new) {
        typeof(weakSelf) self = weakSelf;
        NSLog(@"self:%@,old:%@, new:%@",self,old,new);
    }];
复制代码

这里我用__weak typeof(self) weakSelf = self;typeof(weakSelf) self = weakSelf;巧妙的解决了循环引用问题。这里我是参考MJRefresh源码,这样在block内部就能够继续使用self关键字了。 4. 关于以上的疑惑但愿有人能解答,thanks!thanks!thanks!最后附上github上源代码给你们参考:github.com/FreeBaiShun…

相关文章
相关标签/搜索