0
点赞
收藏
分享

微信扫一扫

KVO详解&使用及底层实现

MaxWen 2021-09-25 阅读 22

概念

KVO (Key-Value Observing),看名字为键值观察,它是观察者设计模式的一种实现。简单来说 就是提供一种机制,对目标对象的某个属性添加观察,当该属性发生变化的时候,会通过KVO提供的接口自动通知观察者。【不需要对被观察对象添加任何额外代码,就可使用KVO机制】

使用

注册观察者
- (void)addObserver:(NSObject *)observer 
                 forKeyPath:(NSString *)keyPath 
                       options:(NSKeyValueObservingOptions)options
                       context:(nullable void *)context;
  • observer : 观察者
  • keyPath : 键路径参数,描述将要观察的属性
  • options :标识KVO希望变化如何传递给观察者,可以使用|进行多选(有四个选项)
  • context:上下文内存区,通常指为NULL,设置了上下文可以用上下文在 通知回调中做区分 相对来说安全
被观察者改变,收到通知,调用以下方法:
-(void)observeValueForKeyPath:(NSString *)keyPath
                     ofObject:(id)object
                       change:(NSDictionary<NSString *,id> *)change
                      context:(void *)context {}
  • keyPath: 被观察者的属性
  • object : 被观察者对象
  • change:变化前后的值都存储在 change 字典中
  • context:注册观察者时,context 传过来的值

使用了上下文的判断区分

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
 
    if (context == PersonAccountBalanceContext) {
        // Do something with the balance…
 
    } else if (context == PersonAccountInterestRateContext) {
        // Do something with the interest rate…
 
    } else {
     //任何无法识别的上下文都必须属于super
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                               context:context];
    }
}
移除观察者
///使用了上下文用这个移除
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
///没有使用用这个移除
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
触发自动kvo方法
/// set方法
[account setName:@"Savings"];
///kvo 赋值
[account setValue:@"Savings" forKey:@"name"];
[document setValue:@"Savings" forKeyPath:@"account.name"];
/// 集合类型的需要  mutableArrayValueForKey 来获取集合 并增删改查 
Transaction *newTransaction = <#Create a new transaction for the account#>;
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
 [transactions addObject:newTransaction]; 

手动KVO

KVO中,当被观察的属性改变时,观察者相应被触发。在某些情况下,您可能希望控制通知过程,例如,最大程度地减少出于应用程序特定原因而不必要的触发通知,或者将多个更改分组为一个通知。手动更改通知提供了执行此操作的方法

  • automaticallyNotifiesObserversForKey:(NSString *)theKey
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
 
    BOOL automatic = NO;
    if ([theKey isEqualToString:@"balance"]) {
        automatic = NO;
    }
    else {
        automatic = [super automaticallyNotifiesObserversForKey:theKey];
    }
    return automatic;
}
  • 要实现手动通知需要 将 willChangeValueForKey:放在更改值之didChangeValueForKey:
  1. 普通操作单个属性:
- (void)setBalance:(double)theBalance {
    [self willChangeValueForKey:@"balance"];
    _balance = theBalance;
    [self didChangeValueForKey:@"balance"];
}
  1. 如果单个操作更改多个属性,则必须嵌套更改通知,如下:
- (void)setBalance:(double)theBalance {
    [self willChangeValueForKey:@"balance"];
    [self willChangeValueForKey:@"itemChanged"];
    _balance = theBalance;
    _itemChanged = _itemChanged+1;
    [self didChangeValueForKey:@"itemChanged"];
    [self didChangeValueForKey:@"balance"];
}
  1. 对于有序的一对多关系,不仅必须指定已更改的键,而且还必须指定更改的类型和所涉及对象的索引。的类型变化的是NSKeyValueChange ,指定NSKeyValueChangeInsertionNSKeyValueChangeRemovalNSKeyValueChangeReplacement。受影响对象的索引作为NSIndexSet对象传递 ;如下:
- (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes {
    [self willChange:NSKeyValueChangeRemoval
        valuesAtIndexes:indexes forKey:@"transactions"];
 
    // Remove the transaction objects at the specified indexes.
 
    [self didChange:NSKeyValueChangeRemoval
        valuesAtIndexes:indexes forKey:@"transactions"];
}

底层原理探究

官方文档
  • 自动键值观察是使用一种称为isa-swizzling的技术实现的。
  • isa指针,顾名思义,指向维护分派表的对象的类。这个分派表本质上包含了指向类实现的方法和其他数据的指针。
  • 当观察者为对象的属性注册时,被观察对象的isa指针将被修改,指向中间类而不是真正的类。因此,isa指针的值不一定反映实例的实际类。
  • 您不应该依赖isa指针来确定类成员关系。相反,您应该使用类方法来确定对象实例的类。
实践出真理
  • 当观察者为对象的属性注册时,被观察对象的isa指针将被修改


1、 通过 runtime api object_getClassName()方法 根据isa获取类
2、 分别在注册前和注册后断点打印 发现的确 是isa被修改 被修改的isa的指向为 NSKVONotifying_A 类

  • 证明这个中间类到底和本类是什么关系

通过如下封装方法 遍历类及子类

- (void)printClasses:(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(@"classes = %@", mArray);
}


由此可以证明 中间类 为 本类的 子类 也就是继承关系

  • 中间类中有什么?

可通过下面封装方法

#pragma mark - 遍历方法-ivar-property
- (void)printClassAllMethod:(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(@"%@-%p",NSStringFromSelector(sel),imp);
    }
    free(methodList);
}


由此可以看出 KVO派生出的中间类 除了重写了要观察的属性的setter方法 还重写了class dealloc 因为 NSObject 都有这些方法。至于_isKVOA方法个人猜测是 派生类的一个 标识。

  • 那他是继承下来的方法 还是 新建立的 标识?

  1. 在注册之前 向self.a 发送 _isKVOA的消息 发现返回 nil
  2. 在注册之前 向self.a 发送 不存在 的消息发现 直接报错 (由此可以看出 父类是有实现的 虽然没有见过)
  3. 注册之后 发现_isKVOA 不在返回nil了 (由此可以看出_isKVOA也是继承下来的方法)
  • dealloc中移除观察者后,isa指向是谁,以及中间类是否会销毁?


由此可以看出 在移除观察者之前 self.a 对象 的isa依旧指向的 中间类 在移除之后变回 A本类

  • 那么中间类从创建后,到dealloc方法中移除观察者之后,是否还存在?

我们在上一级 来打印 A的子类情况

通过子类的打印结果可以看出,中间类一旦生成,没有移除,没有销毁,还在内存中 -- 主要是考虑重用的想法,即中间类注册到内存中,为了考虑后续的重用问题,所以中间类一直存在

总结

基本原理
  • 当观察某对象 A 时,KVO 机制动态创建一个对象A当前类的子类,并为这个新的子类重写了被观察属性 keyPath 的 setter 方法(类似手动KVO)。setter 方法随后负责通知观察对象属性的改变状况。
底层实现
  • Apple 使用了 isa-swizzling技术 来实现 KVO 。当观察对象A时,KVO机制动态创建一个新的名为:NSKVONotifying_A 的新类, 该类继承自对象A的本类,此时A的实例对象isa的指向会指向 NSKVONotifying_A 新类。 且 KVO 为 NSKVONotifying_A 重写观察属性的 setter 方法,setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象属性值的更改情况。当移除 KVO观察者之后 才会将 A实例对象的isa指向 指回来 指为 A。
  • 中间类创建后就会存在内存中,不会被销毁
  • 除了重写 被观察的属性setter ,还有 class ,delloc ,_isKVOA.
举报

相关推荐

0 条评论