iOS KVO原理探究

KVO原理探究.jpeg

导语:

KVO全称Key Value Observing,直译为键值观察。KVO 作为 iOS 中一种强大并且有效的机制,为 iOS 开发者们提供了很多的便利;可以使用 KVO 来检测对象属性的变化、快速做出响应,这能够为开发者在开发强交互、响应式应用以及实现视图和模型的双向绑定时提供大量的帮助。

Demo源码见KVODemo,主要从以下5个方面来探究KVO:

  1. KVO基本使用
  2. KVO触发模式
  3. KVO属性依赖
  4. KVO基本原理
  5. KVO容器观察

1. KVO基本使用

1.1 使用KVO分为三个步骤:

  1. 通过addObserver:forKeyPath:options:context:方法注册观察者,观察者可以接收keyPath属性的变化事件。
  2. 在观察者中实现observeValueForKeyPath:ofObject:change:context:方法,当keyPath属性发生改变后,KVO会回调这个方法来通知观察者。
  3. 当观察者不需要监听时,可以调用removeObserver:forKeyPath:方法将KVO移除。 需注意调用removeObserver需要在观察者消失之前,否则会导致Crash。

1.2 addObserver方法

在注册观察者时,可以传入下列参数:

  • Observer参数,观察者对象。

  • keyPath参数,需要观察的属性。由于是字符串形式,传错容易导致Crash。一般利用系统的反射机制NSStringFromSelector(@selector(keyPath))。

  • options参数,参数是一个枚举类型。

    NSKeyValueObservingOptionNew 接收新值,默认为只接收新值
    NSKeyValueObservingOptionOld 接收旧值
    NSKeyValueObservingOptionInitial 在注册时立即接收一次回调,在改变时也会发送通知
    NSKeyValueObservingOptionPrior 改变之 前发一一次,改变之后发-一次

  • Context参数,传入任意类型的对象,在接收消息回调的代码中可以接收到这个对象,是KVO中的一种传值方式。
    注意:调用addObserver方法后,KVO并不会对观察者进行强引用,所以需要注意观察者的生命周期,否则会导致观察者被释放带来的Crash。

1.3 监听回调

观察者需要实现observeValueForKeyPath:ofObject:change:context:方法,如果没有实现会导致Crash。
里面参数:

  • keyPath : 监听属性名称
  • Object : 被观察对象
  • Change : 字典,字典中存放KVO属性相关的值,根据options时 传入的枚举来返回。
  • Context : 传入进来的上下文,一般在添加观察者时,留下一个入口,用于传值。

Demo如下:

NS_ASSUME_NONNULL_BEGIN
@interface KVOModel : NSObject
@property (nonatomic, strong) NSString* name;
@end
NS_ASSUME_NONNULL_END

有个class为KVOModel,需要对类中的name属性进行监听

@interface ViewController ()
@property (nonatomic, strong) KVOModel* model;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    _model = [KVOModel new];
    // 注册
    [_model addObserver:self forKeyPath:NSStringFromSelector(@selector(name)) options:(NSKeyValueObservingOptionNew) context:nil];
}
/** 监听方法 */
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    NSLog(@"%@", change);
}
/** 屏幕touch */
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    static int magicNum;
    _model.name = [NSString stringWithFormat:@"name=%d", magicNum++];
}

运行结果:

2018-11-23 00:47:57.035430+0800 KVODemo[40822:260341] {
    kind = 1;
    new = "name=0";
}
2018-11-23 00:47:58.197815+0800 KVODemo[40822:260341] {
    kind = 1;
    new = "name=1";
}

2. KVO触发模式

KVO在属性发生改变时的调用是自动的,如果想要手动控制这个调用时机,或想自已实现KVO属性的调用,则可以通过KVO提供的方法进行调用。

@implementation KVOModel
/** 模式调整 */
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
    return NO;  // 改为手动模式
}
@end

这样在ViewController中改name的值不会进到监听方法中,需要手动调用触发,在更改name地方需要做如下处理:

/** 屏幕touch */
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    static int magicNum;
    [_model willChangeValueForKey:NSStringFromSelector(@selector(name))];
    _model.name = [NSString stringWithFormat:@"name=%d", magicNum++];
    [_model didChangeValueForKey:NSStringFromSelector(@selector(name))];
}

手动模式的好处,可能有一种需求在某些情况下在更改value的时候,不需要通知,有的时候需要通知,这个时候就需要手动模式来处理。
如果把上面代码中对model中的name赋值给注视掉,再次去点击屏幕,会发现还是会进到监听方法中,这种情况下,监听方法调不调用与设置name无关,只是和有没有调用方法willChangeValueForKey:和didChangeValueForKey:有关。

3. KVO属性依赖

在开发过程中,model一般不会那么简单,比如KVOModel中有个Person类的属性,要观察Person属性中的属性变化,就不能上面那样方法进行处理,Person类:

@interface Person : NSObject
@property (nonatomic, assign) int age;
@end

KVOModel改为如下:

@interface KVOModel : NSObject
@property (nonatomic, strong) NSString* name;
@property (nonatomic, strong) Person* person;
@end

在ViewController注册的时候要做如下处理:

// 注册
[_model addObserver:self forKeyPath:@"person.age" options:(NSKeyValueObservingOptionNew) context:nil];

在Person中只有一个age属性,如果还有其他属性,可以在上面注册代码下加入一样的代码,只是更改person.age值。但是如果是Person中的属性很多很多,每个属性更改都要通知观察者,这样写就比较麻烦,这个时候就要通过属性依赖进行处理。需要重写KVOModel中类方法:

+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
    NSSet* keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"person"])
    {
        keyPaths = [[NSSet alloc] initWithObjects:@"_person.age", @"_person.name", nil];
    }
    return keyPaths;
}

在ViewController注册,只要监听person属性就可以

[_model addObserver:self forKeyPath:@"person" options:(NSKeyValueObservingOptionNew) context:nil];

这样更改person对象的属性值,都会通知到观察者KVOModel,进入到监听方法。

4. KVO基本原理

KVO是通过观察属性的set方法,但是前面demo中不设置属性值,只要调用willChangeValueForKey:和didChangeValueForKey:两个方法也会触发通知,这个可以通过demo验证,比如说KVOModel中有个成员变量value,直接更改value的值看效果。

@interface KVOModel : NSObject
{
    @public
    int value;
}
@end

注册时keyPath为value,然后去更改值

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    static int magicNum;
    _model->value = magicNum++;
}

会发现监听方法没有调用到,KVO实际上还是通过观察属性set的方法达到目的。 那如何当调用KVOModel类对象的set方法能够观察到,首先会想到两种方法,一种是分类去设计,另一种是通过继承,子类实现。
分类实现 可以直接在分类中重写属性对应的set方法,然后在分类set方法去通知外部,但是这种会有弊端,有的时候会有种需求,会重写set方法,然后加上自己的业务,如果KVO通过分类实现,就会覆盖掉原来的set方法,业务逻辑就永远调用不到,这种框架设计就会有问题。
子类实现 KVO底层实现通过子类实现,需要以下步骤:

  1. 创建一个子类,例如KVOModel,子类名字就会叫做NSKVONotifying_KVOModel。
  2. 重写set方法,例如观察name,底层就会重写setName方法。
  3. 外界改变isa指针。

可以通过查看KVOModel验证下系统的实现,在KVOModel创建后查看下对应的信息,如下:

KVOModel对象信息.png
调用完addObserver,再次查看KVOModel对象的信息,如下:
KVOModel对象信息.png

isa就会被改为NSKVONotifying_KVOModel,这个肯定是在调用addObserver方法中创建了这个子类,苹果的KVO没有开源,网络上有基于GNU开源的代码,会有共通之处,可以查看参考。
我的另一篇简书文章《自定义KVO》大致根据原理模拟实现了一个简单的KVO。

5. KVO容器观察

如果KVOModel中有个容器属性,这需要怎么观察到容器中数据改动。

@property (nonatomic, strong) NSMutableArray* arrayValue;

同样通过上述注册方法对arrayValue进行观察,然后每次点击屏幕的时候都给arrayValue添加一个元素,如下:

[_model.arrayValue addObject:@"1"];

会发现回调方法不会触发,这个由于KVO观察的是set方法,这边容器是add,所以就不会触发,KVO给开发者提供了mutableArrayValueForKey去拿容器对象,然后再调用add,这个时候就会观察到元素改变:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSMutableArray* tmpArray = [_model mutableArrayValueForKey:@"arrayValue"];
    [tmpArray addObject:@"1"];
}

通过打断点,查看tmpArray信息,刚开始定义的时候结构如下:
tmpArray信息.png

通过mutableArrayValueForKey方法获取赋值,再次查看tmpArray的结构信息,如下:
tmpArray信息.png
tmpArray类型改变了,很明显是个子类,所以应该是系统在子类中重写了add方法,然后调用willChangeValueForKey:和didChangeValueForKey:两个方法通知外部达到目的。

6. 结尾

苹果的KVO技术给我们提供了方便,但是用不适当,可能就会出现crash,观察Demo中,在初始化地方注册,然后用其他方法来监听,明显是分开的,如果代码量很大的时候,这种方式就比较不好,可读性就比较差点,并且在dealloc的时候,一定要remove监听,必须要一一对应,如果注册多了或者remove多了,都会crash,针对KVO这点缺点,可以对其进行封装,比如RAC(函数式响应式编程)框架,在github中有个开源框架ReactiveCocoa,函数式就是AFN,KVO封装可以结合Block去做,addObserver:forKeyPath:options:context:调用的时候就没有必要传self,因为通过block的时候,就不用根据self去调用监听方法observeValueForKeyPath:ofObject:change:context: 逻辑直接就是调用block,这样也就不用在dealloc的时候去remove观察,很方便使用。

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

推荐阅读更多精彩内容