iOS-KVO浅谈

767

KVO简介

KVO概述

  • KVO是键值观察者(key-value-observing)
  • KVO提供一种观察者机制,通过对某个属性添加添加观察者,当值改变时,就会调用"observeValueForKeyPath:"方法,为我们提供一个“对象值改变了!”的时机进行一些操作
  • KVO 是一个观察者模式。观察一个对象的属性,注册一个指定的路径,若这个对象的的属性被修改,则 KVO 会自动通知观察者
  • Objective-C 中有两种使用键值观察的方式:手动或自动,此外还支持注册依赖键(即一个键依赖于其他键,其他键的变化也会作用到该键

KVO的基本使用

  • 注册观察者,实施监听
    HTPerson *p = [HTPerson new];
    self.p = p;
    [self.p addObserver:**self** forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"name改变了"];
  • 回调方法
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
  • 移除观察者
[self removeObserver:**self** forKeyPath:@"name"];

KVO的使用场景

  • 下拉刷新、下拉加载监听UIScrollView的contentoffsize;
  • webview混排监听contentsize;
  • 监听模型属性实时更新UI;
  • 监听控制器frame改变,实现抽屉效果。

键值观察

设置属性

将观察者与被观察者注册好之后,就可以对观察者对象的属性进行操作,这些变更操作就会被通知给观察者对象。注意,只有遵循 KVO 方式来设置属性,观察者对象才会获取通知,也就是说遵循使用属性的 setter 方法,或通过key-path来设置:

[target setAge:30];
[target setValue:[NSNumber numberWithInt:30] forKey:@"age"];
[[self.p mutableArrayValueForKeyPath:@"moneys"]addObject:@"哈哈"];

处理变更通知

观察者需要实现名为 NSKeyValueObserving 的 category 方法来处理收到的变更通知:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;

在这里,change 这个字典保存了变更信息,具体是哪些信息取决于注册时的 NSKeyValueObservingOptions。

手动关闭和自动关闭监听

系统默认automaticallyNotifiesObserversForKey返回YES,如果设置成NO则监听回调不会执行,可以对单个属性进行关闭,也可以在automaticallyNotifiesObserversOfName返回NO关闭name的监听

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
    return NO;
}
+ (BOOL)automaticallyNotifiesObserversOfName{
    return NO;
}

KVO原理

  • 当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类
  • 在这个派生类中重写基类中任何被观察属性的 setter 方法。
  • 派生类在被重写的 setter 方法实现真正的通知机制,就如前面手动实现键值观察那样。
  • 基于设置属性会调用 setter 方法,而通过重写就获得了 KVO 需要的通知机制。
  • 前提是要通过遵循 KVO 的属性设置方式来变更属性值,如果仅是直接修改属性对应的成员变量,是无法实现 KVO 的。
  • 同时派生类还重写class 方法以“欺骗”外部调用者它就是起初的那个类。
  • 系统将这个对象的 isa 指针指向这个新诞生的派生类,因此这个对象就成为该派生类的对象了,因而在该对象上对 setter 的调用就会调用重写的 setter,从而激活键值通知机制。
  • 派生类还重写dealloc 方法来释放资源。
  • 当一个观察者注册对象的一个属性 isa 观察对象的指针被修改,指着一个中间类而不是在真正的类。
  • isa 指针的作用:每个对象都有 isa 指针,指向该对象的类,它告诉 runtime 系统这个对象的类是什么

KVO实现过程

实现代码如下所示

- (void)viewDidLoad {

    [super viewDidLoad];

    HTPerson *p = [HTPerson new];
    _p = p;
    
    //输出类和子类
    [self printClass:[p class]];
    //输出方法列表
    [self printClassMethod:objc_getClass("HTPerson")];
    //监听
    [p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"name改变了"];

    //输出类和子类
    [self printClass:[p class]];
    //输出方法列表
    [self printClassMethod:objc_getClass("NSKVONotifying_HTPerson")];
    //监听
    [p addObserver:self forKeyPath:@"moneys" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"array改变了"];

     //输出类和子类
    [self printClass:[p class]];
    //输出方法列表
    [self printClassMethod:objc_getClass("NSKVONotifying_HTPerson")];

    NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector: @selector(timerAction) userInfo:nil repeats:YES];

    [[NSRunLoop mainRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
    [timer fire];
    _timer = timer;
}

//便利类和子类
- (void)printClass:(Class)cls{
    //注册类的总数
    int count = objc_getClassList(NULL, 0);
    //创建一个数组,其中包含特定对象
    NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
    //获取所有已经注册的类
    Class *classes = (Class *)malloc(**sizeof**(Class)*count);
    //
    objc_getClassList(classes, count);
   
    for (int i = 0; i<count; i++) {
        if (cls == class_getSuperclass(classes[i])) {
            [mArray addObject:classes[i]];
        }
    }

    free(classes);
    NSLog(@"%@",mArray);
}

//输入类的方法
- (void)printClassMethod:(Class)cls{

    unsigned int count = 0;
    Method *methodList = class_copyMethodList(cls, &count);
    for (int i = 0; i<count; i++) {
        Method method = methodList[i];
        SEL sel = method_getName(method);
//        IMP imp = class_getMethodImplementation(cls, sel);
        NSLog(@"%@",NSStringFromSelector(sel));
    }
    free(methodList);
}

- (void)timerAction{

    self.p.name = [NSString stringWithFormat:@"%@$",self.p.name];
    [[self.p mutableArrayValueForKeyPath:@"moneys"]addObject:@"哈哈"];

    if (self.p.name.length>10) {
        [self.timer invalidate];
        self.timer = nil;

        [self.p removeObserver:self forKeyPath:@"name"];
        [self.p removeObserver:self forKeyPath:@"moneys"];
        //输出类和子类
        [self printClass:objc_getClass("NSKVONotifying_HTPerson")];
    }

}

\


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

    /*
       * keyPath : 监听的属性
       * object : 监听的哪个对象
       * change : 根据监听的结构体,展示监听的内容
       * context : 监听传入的context
       */

      NSLog(@"keyPath:%@ object:%@ context:%@ change:%@",keyPath,object,context,change);

}
  • 添加监听,会派生一个新的子类 NSKVONotifying_HTPerson,self.p的isa指向该类。

截屏2021-08-17 下午1.09.41.png

  • 同时会重写其部分方法:setNameclassdealloc_isKVOA
  • 在移除监听后,self.p的isa指向不再是派生类,是类

参考

似水流年 xinxin