iOS KVO实现原理及使用

2023-03-19 11:46:35 浏览数 (2)

技术不缺乏缔造者,网络不缺乏键盘侠,但缺乏分享技术的源动力!

关于KVO的实现,文章已经很多了,这里阐述我个人的观点,写一些自己的感受

1、简介

KVO(key-value observe)是在KVC的基础上实现的一种用于监听属性变化的设计模式;如果对某个类的某个属性设置了KVO,那么当这个属性发生变化时,就会触发监听方法,从而知道属性变化了。如果本类一个属性的改变会影响到其他多个属性的变化,我们也会经常自己重写这个属性的set方法,用来监听他的变化,但是如果不是本类的属性,我们就没办法重写其set方法了,这个时候KVO就可以上场了,其实KVO本质上也是重写set方法,而整个过程依赖于runtime才能实现。

2、使用

1)设置监听

代码语言:javascript复制
// 初始化一个测试类,类里面声明一个属性:nameStr_kvoTest = [[KVOTestModel alloc] init];// 给属性nameStr设置监听,类型不同,回调值不同,这里监听的是新值(NSKeyValueObservingOptionNew),还可以监听其他变化,具体看枚举[_kvoTest addObserver:self forKeyPath:@"nameStr" options:NSKeyValueObservingOptionNew context:nil];

2)实现监听回调方法

代码语言:javascript复制
// 监听的属性通过kvc发生变化时,执行下面方法- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {    
   // 这里我们只判断了keyPath,如果存在多个kvo,为防止重名,可判断object是否为我们监听的实例:_kvoTest
   if ([keyPath isEqualToString:@"nameStr"]) {        // 我们直接打印新值
       NSLog(@"change: %@",[change objectForKey:NSKeyValueChangeNewKey]);
   }
}

3)使用KVO时应该特别注意移除观察者,否在当类要被释放时会发生崩溃

代码语言:javascript复制
- (void)dealloc {
   [_kvoTest removeObserver:self forKeyPath:@"nameStr"];
}

这里有一点,提一下,iOS11如果不调用上面的方法,也不会崩溃,亲测,但是iOS10及以下的设备会崩溃。在官方文档中没有查到相关的的说明(在iOS9之后,NSNotification已经不用移除了,可能也是这个趋势吧)。

3、实现原理

KVO是根据iOS runtime实现的,当监听某个对象(_kvoTest)的某个属性时,KVO会创建这个对象的子类,并重写我们监听的属性(keyPath)的set方法,具体实现可能是下面这个样子。

代码语言:javascript复制
- (void)setNameStr:(NSString *)nameStr {
   [self willChangeValueForKey:@"nameStr"];
   [super setValue:nameStr forKey:@"nameStr"];
   [self didChangeValueForKey:@"nameStr"];
}

didChangeValueForKey:方法执行完之后,KVO会根据注册时option返回不同的字典

上面我们提到了KVO是建立在KVC基础上的,可以看到,如果不通过KVC改变属性,那么set方法就不会执行,那么KVO也就没办法监听属性的变化了。

4、再具体一点(原理)

我们都说他是通过runtime实现的,是因为这个操作是运行的时候动态添加的,可以多阅读一些关于runTime的文章,知道原理,用起来更顺手,也会有更多的想法。

当观察对象时,KVO机制动态创建一个新的名为:NSKVONotifying_对象名 的新类,该类继承自目标对象的本类,且 KVO 为 NSKVONotifying_对象名 重写观察属性的 set 方法。在这个过程,被观察对象的 isa 指针从指向原来的对象,被 KVO 机制修改为指向系统新创建的子类 NSKVONotifying_对象名 类,来实现当前类属性值改变的监听,这也就是前面所说的“黑魔法”;我还试了一下,创建一个新的名为“NSKVONotifying_对象名”的类,发现系统运行到注册 KVO 的代码时,iOS10及以下会崩溃,iOS11下控制台打印警告:

代码语言:javascript复制
[general] KVO failed to allocate class pair for name NSKVONotifying_KVOTestModel,
automatic key-value observing will not work for this class

存在同名类,无法进行KVO监听了~。

5 、扩展

很多人都用过YYKit,很强大的开源类库。里面有个扩展类NSObject YYAddForKVO,里面对kvo做了一下封装,用起来方便好多。他通过运行时,动态的给类添加了属性,存储了所有的keyPath和回调。

三个方法:

代码语言:javascript复制
/**
Registers a block to receive KVO notifications for the specified key-path
relative to the receiver.
@discussion The block and block captured objects are retained. Call
`removeObserverBlocksForKeyPath:` or `removeObserverBlocks` to release.
@param keyPath The key path, relative to the receiver, of the property to
observe. This value must not be nil.
@param block   The block to register for KVO notifications.
*/- (void)addObserverBlockForKeyPath:(NSString*)keyPath block:(void (^)(id _Nonnull obj, _Nullable id oldVal, _Nullable id newVal))block;
/**
Stops all blocks (associated by `addObserverBlockForKeyPath:block:`) from
receiving change notifications for the property specified by a given key-path
relative to the receiver, and release these blocks.
@param keyPath A key-path, relative to the receiver, for which blocks is
registered to receive KVO change notifications.
*/- (void)removeObserverBlocksForKeyPath:(NSString*)keyPath;
/**
Stops all blocks (associated by `addObserverBlockForKeyPath:block:`) from
receiving change notifications, and release these blocks.
*/- (void)removeObserverBlocks;

0 人点赞