iOS - KVO

2020-07-05  本文已影响0人  felix6

[toc]

参考

KVO

KVC

iOS--KVO的实现原理与具体应用

IOS-详解KVO底层实现

KVO底层探索和遇到的常见错误(一)

【 [深入浅出Cocoa]详解键值观察(KVO)及其实现机理

自动移除observer的KVO和Notification

观察者模式:

观察者模式定义了一种一对多的依赖关系, 让多个观察者对象同时监听某一个目标对象。这个目标对象在状态上发生变化时, 会通知所有观察者对象。观察者模式较完美地将目标对象与观察者对象解耦。
iOS中观察者模式的实现有两种:Notification、KVO。

KVO简介

KVO ( Key-Value Observing) 是OC对"观察者"设计模式的一种实现。

和 KVC 类似, 在 OC 中要实现 KVO 则必须实现 <NSKeyValueObserving> 协议, 但由于 NSObject 已经实现了该协议, 因此几乎所有的OC对象都可以使用KVO。

The NSKeyValueObserving (KVO) informal protocol (非正式协议) defines a mechanism that allows objects to be notified of changes to the specified properties of other objects.

KVO 作用

KVO提供一种机制, 指定一个被观察对象, 当对象某个属性发生更改时, 观察者会获得通知, 并作出相应处理, 在MVC设计架构下的项目, KVO机制很适合实现model模型和view视图之间的通讯。

这种模式有利于两个类间的解耦合, 尤其是对于业务逻辑与视图控制这两个功能的解耦合。

利用它可以很容易实现视图组件和数据模型的分离, 当数据模型的属性值改变之后作为监听器的视图组件就会被激发, 激发时就会回调监听器自身。


KVO API

NSKeyValueObserving.h

KVO 关键方法定义在 NSKeyValueObserving.h

给NSObject添加的分类
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
    NSKeyValueObservingOptionNew = 0x01,
    NSKeyValueObservingOptionOld = 0x02,
    NSKeyValueObservingOptionInitial API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x04,
    NSKeyValueObservingOptionPrior API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x08
};
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    NSKeyValueChangeSetting = 1,
    NSKeyValueChangeInsertion = 2,
    NSKeyValueChangeRemoval = 3,
    NSKeyValueChangeReplacement = 4,
};
typedef NS_ENUM(NSUInteger, NSKeyValueSetMutationKind) {
    NSKeyValueUnionSetMutation = 1,
    NSKeyValueMinusSetMutation = 2,
    NSKeyValueIntersectSetMutation = 3,
    NSKeyValueSetSetMutation = 4
};

@interface NSObject(NSKeyValueObserving)
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
@end

  
@interface NSObject(NSKeyValueObserverRegistration)
  
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

@end

@interface NSObject(NSKeyValueObserverNotification)

- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;

- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;

- (void)willChangeValueForKey:(NSString *)key withSetMutation:(NSKeyValueSetMutationKind)mutationKind usingObjects:(NSSet *)objects;
- (void)didChangeValueForKey:(NSString *)key withSetMutation:(NSKeyValueSetMutationKind)mutationKind usingObjects:(NSSet *)objects;

@end

@interface NSObject(NSKeyValueObservingCustomization)

+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key;

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key;

@property (nullable) void *observationInfo NS_RETURNS_INNER_POINTER;

@end
给NSArray添加的分类
@interface NSArray<ObjectType>(NSKeyValueObserverRegistration)
  
- (void)addObserver:(NSObject *)observer toObjectsAtIndexes:(NSIndexSet *)indexes forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer fromObjectsAtIndexes:(NSIndexSet *)indexes 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 fromObjectsAtIndexes:(NSIndexSet *)indexes forKeyPath:(NSString *)keyPath;
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)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;

@end
给NSOrderedSet、NSSet添加的分类
@interface NSOrderedSet<ObjectType>(NSKeyValueObserverRegistration)
  
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)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;

@end

  
@interface NSSet<ObjectType>(NSKeyValueObserverRegistration)

- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)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;

@end

KVO使用方法

注册观察者
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
观察者实现回调方法

在回调方法中处理属性发生变化时要做的事

// 当监听对象的属性值发生改变时, 该方法会被回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;
移除观察者

在观察者的dealloc中移除观察者 移除指定Key路径的监听器

- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;

KVO案例

@interface ViewController ()
@property (nonatomic, strong) Person *psn;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.psn = [[Person alloc] init];
    // 运行到这里_psn    isa = (Class) Person
    [self.psn addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
    // 运行到这里_psn    isa = (Class) NSKVONotifying_Person
    
    [_psn addObserver:self forKeyPath:@"hobby" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
    [_psn addObserver:self forKeyPath:@"profession" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    /// 如果有多个监听, 必须对触发回调函数的来源进行判断
    if (object == _psn && [keyPath isEqualToString:@"name"]) {
        NSString *nameOld = [change objectForKey:NSKeyValueChangeOldKey];
        NSString *nameNew = change[NSKeyValueChangeNewKey];
        NSLog(@"旧%@新%@", nameOld, nameNew);
    }else if (object == _psn && [keyPath isEqualToString:@"hobby"]) {
        NSString *hobbyOld = [change objectForKey:NSKeyValueChangeOldKey];
        NSString *hobbyNew = change[NSKeyValueChangeNewKey];
        NSLog(@"旧%@新%@", hobbyOld, hobbyNew);
    }else if (object == _psn && [keyPath isEqualToString:@"profession"]) {
        NSString *professionOld = [change objectForKey:NSKeyValueChangeOldKey];
        NSString *professionNew = change[NSKeyValueChangeNewKey];
        NSLog(@"旧%@新%@", professionOld, professionNew);
    }else {
        /// 如果被观察者的类还有父类, 并且其父类对象也绑定了其他KVO, 则当前类可能无法捕捉到这个KVO, 此时应该传给super处理。
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

- (void)dealloc {
    [_psn removeObserver:self forKeyPath:@"name" context:nil];
    [_psn removeObserver:self forKeyPath:@"hobby"];
    [_psn removeObserver:self forKeyPath:@"profession"];
}

/**
 潜在的问题有可能出现在dealloc中对KVO的注销上。
 KVO的一种缺陷(其实不能称为缺陷,应该称为特性)是,当对同一个keypath进行两次removeObserver时会导致程序crash,
 这种情况常常出现在: 父类有一个KVO,父类在dealloc中remove了一次,子类又remove了一次。
 不要以为这种情况很少出现!当你封装framework开源给别人用或者多人协作开发时是有可能出现的,而且这种crash很难发现。
一般代码中context字段都是nil,可否利用该字段来标识出到底KVO是superClass注册的,还是self注册的
我们可以分别在父类以及本类中定义各自的context字符串,比如在本类中定义context为@"ThisIsMyKVOContextNotSuper";然后在dealloc中remove observer时指定移除的自身添加的observer。这样iOS就能知道移除的是自己的kvo,而不是父类中的kvo,避免二次remove造成crash。
 
 */

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    _psn.name = @"TimCook"; // 点语法调用setter可以激发KVO
    _psn.hobby = @"football"; // 调用setter可以激发KVO
    _psn->_hobby = @"tennis"; // 未调用setter, 不能激发KVO
    [_psn setValue:@"engineer" forKey:@"profession"]; // 使用KVC, 可以激发KVO
    /// KVO的关键就是监听对象的属性进行赋值时需调用setter方法或者KVC
}

@end

KVO实现原理

所用技术:

Apple使用了 isa混写(isa-swizzling) 来实现 KVO。

基本原理★★:
  1. 当类A的实例对象a第一次被添加观察(addObserver)时, KVO会利用 Runtime 动态创建一个继承自A的子类 NSKVONotifying_A, 并让a的isa指向这个全新的子类。
  2. 为新子类 NSKVONotifying_A 重写被观察的属性的 setter 方法 (实际是替换了setterIMP, 即 _NSSetXxxValueAndNotify())
  3. 重写setter 方法中, 给属性赋值前后, 分别调用 -willChangeValueForKey:-didChangeValueForKey:
  4. didChangeValueForKey: 中, 触发 observer 的监听方法 observeValueForKeyPath:ofObject:change:context:, 通知observer, 属性值发生了改变。
  5. 当某个 property 被移除观察者时, 删除其对应重写的 setter 方法。
  6. 当没有 observer 观察任何一个 property 时, 删除动态创建的子类 NSKVONotifying_A

当修改 instance 对象的属性时, 调用setter方法, 实际是调用了 Foundation 的 _NSSetXxxValueAndNotify 函数

// 看不到实现, 写的伪代码
void _NSSetXxxValueAndNotify() {
    [self willChangeValueForKey:@"xxx"]; 
    // 调用父类的setter
    [super setXxx: xxx];
      [self didChangeValueForKey:@"xxx"];  
}

didChangeValueForKey: 内部会触发 Observer 的监听方法 observeValueForKeyPath:ofObject:change:context:

// 看不到实现, 写的伪代码
- (void)didChangeValueForKey:(NSString *)key {
      // 通知 observer, xxx 属性值发生了改变
      [observer observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
原理剖析:

① 打断点可以看到:

addObserver 之前, 对象a的 isa指针 指向原始类 A;

addObserver 走完, 对象a的 isa指针 指向中间类 NSKVONotifying_A;

如此, 被观察的对象a就变成了中间类 NSKVONotifying_A 的对象了, 因而对对象a的被观察属性调用setter方法, 实际上就是调用中间类NSKVONotifying_A 中重写的setter, 从而激活KVO。

或者

// 在 `addObserver` 前后分别打印以下信息
object_getClass(self.a); // a的类, addObserver 前后类名发生了改变
[self.a methodForSelector:@selector(setNum:)]; // %p 打印方法地址, addObserver 前后地址值不一样

// 然后在a的setNum方法调用处, 打断点, lldb分别查看上面得到的IMP地址
// 可见, addObserver后, 属性num的setter方法的IMP变成了 _NSSetIntValueAndNotify
(lldb)p (IMP)0x10123456 // 查看addObserver前的IMP地址, 控制台输出 (ModuleName`-[A setNum:])
(lldb)p (IMP)0x10123567 // 查看addObserver后的IMP地址, 控制台输出 (Foundation`_NSSetIntValueAndNotify)

② 子类setter方法剖析

KVO 的键值观察通知依赖于 NSObject 的两个方法:

willChangeValueForKey:didChangeValueForKey: 在set属性的前后分别调用2个方法;

被观察属性发生改变之前, willChangeValueForKey: 被调用, 通知系统该 keyPath 的属性值即将变更;

改变发生后, didChangeValueForKey: 被调用, 通知系统该 keyPath 的属性值已经变更;

之后, observeValueForKey:ofObject:change:context: 也会被调用。(在didChangeValueForKey:中被触发)

重写观察属性的 setter 方法, 这种继承方式的注入是在运行时而不是编译时实现的。

KVO 为子类的观察者属性重写调用存取方法的工作原理在代码中相当于:

- (void)setName:(NSString *)newName {
    [self willChangeValueForKey:@"name"];    // KVO在调用存取方法之前总调用
    [super setValue:newName forKey:@"name"]; // 调用父类的存取方法
    [self didChangeValueForKey:@"name"];     // KVO在调用存取方法之后总调用
}
图示
未使用KVO监听的对象
image
使用了KVO监听的对象
image

KVO 注意点

KVO 触发条件

需调用对象属性的 setter 或使用 KVC 赋值, 才能触发 KVO。

使用KVC赋值

如果使用了KVC:

若有访问器方法, 运行时会在 访问器方法 中调用 will/didChangeValueForKey: 方法, 从而触发KVO

若无访问器方法, 运行时会在 setValue:forKey 方法中调用 will/didChangeValueForKey: 方法, 从而触发KVO

成员变量访问 ->

如果使用->直接对成员变量进行赋值, 不能触发 KVO

_psn->_hobby = @"tennis"; // 未调用setter, 不能触发KVO
多个监听, 来源判断

如果有多个监听, 必须对触发回调函数的来源进行判断

if (object == _psn && [keyPath isEqualToString:@"name"]) {}
未处理的KVO提交给父类处理

如果被观察者的类还有父类, 并且其父类对象也绑定了其他KVO, 则当前类可能无法捕捉到这个KVO, 此时应该传给super处理。

[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
移除观察者

不需要监听时, 记得在观察者的 - (void)dealloc; 方法中移除KVO, 以防止内存泄漏

避免多次移除观察者导致crash

同一个 keypath 只能进行一次 removeObserver, 多次移除会导致程序crash, 这种情况常出现在父类和子类同时注册了同一个KVO, 销毁时会移除两次。

这种情况context字段不应传nil, 在父类和子类中定义各自唯一的context, 利用context来标识出KVO是父类or子类注册的

KVO 和 多线程

KVO 的响应 和 KVO 观察的值变化 是在一个线程上的, 所以, 大多数时候, 不要把KVO与多线程混合起来。
除非能够保证所有的观察者都能线程安全的处理KVO


面试题★★:

KVO 与 KVC 的不同?

KVO(Key-Value Observing 键值监听), 它提供一种机制, 当指定的对象的属性被修改后, 对象就会接受到通知, 前提是执行了setter方法、或者使用KVC赋值。

KVC(Key-Value Coding 键值编码), 使用字符串(键)访问一个对象实例变量的机制。而不是通过调用Setter、Getter方法等显式的存取方式去访问。

KVO 和 NSNotification的区别?

两者都是一对多
NSNotification 比 KVO 多了发送通知的一步, 需要 notificationCenter 来做为中间交互。

NSNotification 的优点是监听范围广, 监听不局限于属性的变化, 还可以对如键盘弹出收起、程序进入前后台等各种状态变化进行监听。

KVO 只需要设置观察者->处理属性变化, 至于中间通知这一环, 使用runtime机制隐藏实现

KVO、NSNotification 与 delegate 的异同及优缺点, 采用哪种方式更好?
如何手动触发KVO?

手动调用 willChangeValueForKey:didChangeValueForKey:

实际上是 didChangeValueForKey: 中调用了 observer 的 observeValueForKey:ofObject:change:context:

理论上不需要调用 willChangeValueForKey: , 但是 didChangeValueForKey: 内部会判断之前是否调用了 willChangeValueForKey: , 如果没有调用就会失效。

直接修改成员变量会触发KVO么?

不会触发KVO

因为没有调用setter方法, KVO是依赖于setter的, 是把setter的实现替换掉

只有调用setter才会触发KVO

上一篇下一篇

猜你喜欢

热点阅读