KVO封装

1 KVO基础

KVO是用来做监听的,这个相信大多数人都知道,常规的代码如下:

[self.view addObserver:self
            forKeyPath:@"isSelected"
               options:NSKeyValueObservingOptionNew
               context:nil];

四个参数:
调用该方法的对象就是要监听的对象,即被监听的对象。
addOberver的参数就是监听者对象,即监听回调接收者。
KeyPath的参数就是要监听的健值。
context的参数这个其实就是传值参数,这里传什么值,监听回调方法的context就带过来什么值,可以用来传一些特征参数什么的,但是这个参数传进来并不会被强引用,如果传对象进来要注意让对象的在监听接收的时候没有被释放掉。

KVO到底干了什么:

2 KVO到底监听了什么

很多时候KVO都被用来监听属性,其实KVO并不是只能监听属性,只要KVC能够调用的KVO都能够监听。比如就算被监听没有 touch 属性, 也没有touch成员变量,但是如果对象中有setTouch/touch方法,那么KVO也会被被响应。
例如如下代码,对象并没有touch属性,但是有touch的get/set方法,如果调用了setTouch方法,那么KVO就会监听到,并通过touch方法返回值。
️:没有属性对应的情况下如果没有touch方法,KVO找不到值会崩溃。

事实上KVC也是一样,其取值和赋值也并不是直接取找对象的属性和成员变量,而是通过先找set/get方法 => 没找到再找变量 的顺序调用的。

    [self.rootView jkr_addObserver:self forKeyPath:@"touch" change:^(id newValue) {
        NSLog(@"%@", newValue);
    }];
...
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.isSelected = !self.isSelected;
    if (self.isSelected) {
        self.circleColor = [UIColor redColor];
        [self setNeedsDisplay];
    } else {
        self.circleColor = [UIColor orangeColor];
        [self setNeedsDisplay];
    }
    [self setTouch:YES];
}

#pragma mark - KVO测试
- (void)setTouch:(BOOL)touch {
    
}

- (BOOL)touch {
    return self.isSelected;
}

3 简单的实现KVO封装

如上的代码,我是通过block直接捕捉KVO的调用,就是一个KVO的封装。KVO默认的代码实现起来有一些问题:
1:添加和监听回调代码是分开的,很零散
2:回调的方法都在一个方法中,要自己通过keyPath区别
3:要自己手动移除监听,如果忘记移除还会报错

这个是我自己通过runtime相关方法来实现的一个KVO封装,网上也有很多KVO的封装,但是都是要新建一个自定义对象数组,通过自定义对象来保存监听者、被监听者、回调block来实现的。我想了些办法,通过runtime给监听和被监听对象动态添加了对应的属性来保存这些信息,不需要新建自定义对象。

NSObject+JKR_Observer.h

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

typedef void (^changeBlock)(id newValue);

@interface NSObject (JKR_Observer)

- (void)jkr_addObserver:(NSObject *)object forKeyPath:(NSString *)keyPath change:(changeBlock)change;

@end

NSObject+JKR_Observer.m

#import "NSObject+JKR_Observer.h"

@interface NSObject ()

@property NSMutableDictionary<NSString *, changeBlock> *jkr_observer_blocks;
@property NSMutableArray<NSString *> *jkr_observer_keyPaths;
@property NSObject *jkr_observer_observerdObject;

@end

@implementation NSObject (JKR_Observer)

- (void)jkr_addObserver:(NSObject *)object forKeyPath:(NSString *)keyPath change:(changeBlock)change {
    NSLog(@"%@ 添加监听者 %@ 被监听的值:%@", self, object, keyPath);
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        method_exchangeImplementations(class_getInstanceMethod([object class], NSSelectorFromString(@"dealloc")), class_getInstanceMethod([self class], @selector(jkr_dealloc)));
    });
    [self addObserver:object forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:(__bridge void * _Nullable)([NSString stringWithFormat:@"%zd", self.jkr_observer_blocks.count])];
    [self.jkr_observer_blocks setObject:change forKey:[NSString stringWithFormat:@"%zd", self.jkr_observer_blocks.count]];
    
    NSMutableArray *keyPaths = [object valueForKey:@"jkr_observer_keyPaths"];
    [keyPaths addObject:keyPath];
    object.jkr_observer_observerdObject = self;
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"监听者:%@ 监听到被监听者 %@ 的值 %@ 改变", self, object, keyPath);
    NSString *key = (__bridge NSString *)(context);
    NSMutableDictionary *dict = [object valueForKey:@"jkr_observer_blocks"];
    if (!dict) return;
    changeBlock block = [dict valueForKey:key];
    if (block) {
        id newValue = [change valueForKey:NSKeyValueChangeNewKey];
        block(newValue);
    }
}

- (void)setJkr_observer_blocks:(NSMutableDictionary<NSString *,changeBlock> *)jkr_observer_blocks {
    objc_setAssociatedObject(self, "jkr_observer_blocks", jkr_observer_blocks, OBJC_ASSOCIATION_RETAIN);
}

- (NSMutableDictionary *)jkr_observer_blocks {
    NSMutableDictionary *blocks = objc_getAssociatedObject(self, "jkr_observer_blocks");
    if (!blocks) {
        blocks = [NSMutableDictionary dictionary];
        objc_setAssociatedObject(self, "jkr_observer_blocks", blocks, OBJC_ASSOCIATION_RETAIN);
    }
    return blocks;
}

- (void)setJkr_observer_keyPaths:(NSMutableArray<NSString *> *)jkr_observer_keyPaths {
    objc_setAssociatedObject(self, @"jkr_observer_keyPaths", jkr_observer_keyPaths, OBJC_ASSOCIATION_RETAIN);
}

- (NSMutableArray<NSString *> *)jkr_observer_keyPaths {
    NSMutableArray *keyPaths = objc_getAssociatedObject(self, @"jkr_observer_keyPaths");
    if (!keyPaths) {
        keyPaths = [NSMutableArray array];
        objc_setAssociatedObject(self, @"jkr_observer_keyPaths", keyPaths, OBJC_ASSOCIATION_RETAIN);
    }
    return keyPaths;
}

- (NSObject *)jkr_observer_observerdObject {
    return objc_getAssociatedObject(self, @"jkr_observer_observerdObject");
}

- (void)setJkr_observer_observerdObject:(NSObject *)jkr_observer_observerdObject {
    objc_setAssociatedObject(self, @"jkr_observer_observerdObject", jkr_observer_observerdObject, OBJC_ASSOCIATION_ASSIGN);
}

- (void)jkr_dealloc {
    NSMutableArray *paths = self.jkr_observer_keyPaths;
    if (paths.count) {
        [paths enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            NSLog(@"被监听者 %@ 移除监听者 %@ 被移除的监听值 %@", self.jkr_observer_observerdObject, self, obj);
            [self.jkr_observer_observerdObject removeObserver:self forKeyPath:paths[idx]];
        }];
    }
    NSLog(@"dealloc");
    [self jkr_dealloc];
}

@end

Demo: https://github.com/Joker-388/JKRKVODemo

获取授权

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

推荐阅读更多精彩内容

  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,864评论 6 13
  • *面试心声:其实这些题本人都没怎么背,但是在上海 两周半 面了大约10家 收到差不多3个offer,总结起来就是把...
    Dove_iOS阅读 27,121评论 29 470
  • 上半年有段时间做了一个项目,项目中聊天界面用到了音频播放,涉及到进度条,当时做android时候处理的不太好,由于...
    DaZenD阅读 3,011评论 0 26
  • 1 29岁,对女孩子来说,似乎成了一个很重要的分岔路。过的好的,以后的人生路会越来越好走,过的不好的,往后的路只会...
    安梳颜阅读 23,030评论 175 256
  • 有时候我的生活过着过着就变得不像是我想要的样子了,感觉时间都不是属于我,每天像个陀螺一样转,我想不应该是这样的状态...
    鲁春亚阅读 466评论 0 0