KVO底层原理之手把手教你实现KVO

Demo传送门

一、KVO

KVO是Key Value ObserVing 的简称,也称为键值监听,当指定对象的属性被修改之后,主动通知观察者对象。

即指定一个被观察对象,当对象某个属性发生更改时,对象会发送一个通知给监听者,以便监听者执行回调操作。常见的KVO应用例如监听scrollView的contentOffset属性。

二、如何使用系统KVO

KVO的使用非常简单,只需要为需要被监听的对象添加监听,并在回调方法内进行处理即可。

Person *p = [[Person alloc] init];

// 给Person对象的属性name添加监听

// @param observer : 观察者,处理监听事件的对象
// @param keyPath : 需要监听的属性
// @param options : 要监听新值还是旧值 也可以都观察
// @param context : 上下文,用于传递数据,可以利用上下文区分不同的监听
[p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];

当person对象的属性发生变化时,调用此方法

// @param keyPath : 监听的属性名
// @param object : 属性所属的对象
// @param change : 属性的修改情况
// @param context : 上下文,用于传递数据,可以利用上下文区分不
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    // 执行操作
    if ([keyPath isEqualToString:@"name"]) {
        
        NSLog(@"== %@",change[NSKeyValueChangeNewKey]);
    }
}

三、KVO的底层实现原理

当给对象Person对象添加监听时,系统内部会动态生成一个Person类的子类NSKVONotifying_Person类,并为这个新的子类重写了被观察属性keyPath的setter 方法, 以及替换原对象的isa指针。

<1 NSKVONotifying_Person类剖析:

在这个过程,被观察对象的 isa 指针从指向原来的Person类,被修改为指向系统新创建的子类 NSKVONotifying_Person类,来监听当前类属性值的改变

    // 实例化Person对象
    Person *p = [[Person alloc] init];

    // 断点1 在控制台打印p的类Person
    p.name = @"July";

    // 断点2 在控制台打印p的类为NSKVONotifying_Person
    [p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];

image.png

所以当我们从应用层面上看来,完全没有意识到有新的类出现,这是系统“隐瞒”了对KVO的底层实现过程,让我们误以为还是原来的类。但是此时如果我们新建一个名为NSKVONotifying_Person的类,控制台会输出观察者注册监听失败。


image.png
<2 子类setter方法剖析:

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

// 被观察属性发生改变之前,willChangeValueForKey 被调用
// 通知系统该 keyPath 的属性值即将改变
- (void)willChangeValueForKey:(NSString *)key;
// 当改变发生后, didChangeValueForKey 被调用
// 通知系统该 keyPath 的属性值已经改变
- (void)didChangeValueForKey:(NSString *)key;

举个栗子:

-(void)setName:(NSString *)newName
{
     //KVO在调用存取方法之前调用
    [self willChangeValueForKey:@"name"];   

     //调用父类的存取方法
    [super setValue:newName forKey:@"name"]; 
   
     //KVO在调用存取方法之后调用
    [self didChangeValueForKey:@"name"];    
}

之后会调用observeValueForKey:ofObject:change:context:方法 。

只需要在Person类里重写这两个方法,则可以证明是否会执行这两个方法了。

- (void)willChangeValueForKey:(NSString *)key {

    NSLog(@"%s", __func__);
    [super willChangeValueForKey:key];
}

- (void)didChangeValueForKey:(NSString *)key {
    
    NSLog(@"%s", __func__); 
    [super didChangeValueForKey:key];
}
image.png

四、自己动手实现KVO

<1 为什么要自己实现KVO呢

相信用过KVO的童鞋都能感受到KVO的不便吧,例如

<1 当监听属性过多时,所有判断都写在 -observeValueForKeyPath:ofObject:change:context:里,内部非常混乱
<2 keyPath是以NSString字符串格式定义,容易出错且不会有警告
<3 忘记移除观察者
<2 KVO实现
#import <Foundation/Foundation.h>

typedef void(^kvoBlock)(NSString *value);

@interface NSObject (ZMKVO)

- (void)zm_Observer:(NSObject *)object keyPath:(NSString *)keyPath block:(kvoBlock)block;

@end
#import "NSObject+ZMKVO.h"
#import <objc/runtime.h>

typedef void(^deallocBlock)(void);

@interface ZMKVOController : NSObject

@property(nonatomic, strong)NSObject *observedObject;

@property(nonatomic, strong)NSMutableArray <deallocBlock>*blockArr;

@end

@implementation ZMKVOController

-(NSMutableArray<deallocBlock> *)blockArr {
    
    if (!_blockArr) {
        _blockArr = [NSMutableArray array];
    }
    return _blockArr;
}

//nextviewController -> kvoController
- (void)dealloc {
    
    ///对observedObject  removeObserver
    NSLog(@"kvoController dealloc");
    
    [_blockArr enumerateObjectsUsingBlock:^(deallocBlock _Nonnull block, NSUInteger idx, BOOL * _Nonnull stop) {
        
        block();
        
    }];
}

@end

@interface NSObject()

@property(nonatomic, strong)NSMutableDictionary <NSString *, kvoBlock>*dict;
@property(nonatomic, strong)ZMKVOController *kvoController;

@end

@implementation NSObject (ZMKVO)

- (void)zm_Observer:(NSObject *)object keyPath:(NSString *)keyPath block:(kvoBlock)block
{
    self.dict[keyPath] = block;
    
    self.kvoController.observedObject = object;
    
    __unsafe_unretained typeof(self) weakSelf = self;
    
    [self.kvoController.blockArr addObject:^{
        //
        [object removeObserver:weakSelf forKeyPath:keyPath];
    }];
    
    // 添加监听
    [object addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:NULL];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    kvoBlock block = self.dict[keyPath];
    
    if (block) {
        block(change[NSKeyValueChangeNewKey]);
    }
}

////getter 和 setter方法
- (NSMutableDictionary<NSString *,kvoBlock> *)dict
{
    NSMutableDictionary *tempDict = objc_getAssociatedObject(self, @selector(dict));
    
    if (!tempDict) {
        tempDict = [NSMutableDictionary dictionary];
        objc_setAssociatedObject(self, @selector(dict), tempDict, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return tempDict;
}

- (ZMKVOController *)kvoController
{
    ZMKVOController *tempKvoController = objc_getAssociatedObject(self, @selector(kvoController));
    
    if (!tempKvoController) {
        tempKvoController = [[ZMKVOController alloc] init];
        
        objc_setAssociatedObject(self, @selector(kvoController), tempKvoController, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    return tempKvoController;
}

@end

如果你看完有些小的收获,请为我点个赞哦,蟹蟹

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342