iOS - KVO 底层详解及与 KVC 的关系

封面

一、KVO 简介

KVO(Key-Value Observing)是iOS提供的一种监听属性变化的机制。

二、使用场景

基本使用:

  1. 添加观察者
    任意定义一个包含了属性的类:
  @interface KVO : NSObject
  @property (nonatomic, assign) NSUInteger count;
  @property (nonatomic, copy)   NSString   *name;
  @end

添加一个对上述类实例对象的属性值监听者:

  KVO  *kvoObj = [KVO new];
  [kvoObj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];

上述代码对kvoObj的name属性进行了监听,其中监听的策略是NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld,表示属性值改变时通知内容里包含新的值和老的值;

typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
NSKeyValueObservingOptionNew = 0x01, //指明接受通知方法参数中的change字典中应该包含改变后的新值。
NSKeyValueObservingOptionOld = 0x02, //指明接受通知方法参数中的change字典中应该包含改变前的旧值。
NSKeyValueObservingOptionInitial = 0x04, //当指定了这个选项时,在addObserver:forKeyPath:options:context:消息被发出去后,甚至不用等待这个消息返回,监听者对象会马上收到一个通知
NSKeyValueObservingOptionPrior = 0x08 //当指定了这个选项时,在被监听的属性被改变前,监听者对象就会收到一个通知
}

另外,context这里直接用nil,其实这个参数可以用来传值或者使用静态变量来标志一个指定的通知。

  1. 添加观察者通知响应函数
    需要重写非正式协议NSKeyValueObserving的下述方法以接收属性值改变时发出的通知:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"%@", change);
}

这里只简单的打印出change里的信息;

  1. 属性改变时通知观察者
kvoObj.name = @"newName";

运行结果:

属性改变通知

打印出了change字典对象里的内容,new和old分别对应NSKeyValueObservingOptionNew和NSKeyValueObservingOptionOld选项;
其中,kind = 1表示改变的类型为设置类型,具体代码对应的类型如下:

enum {
 NSKeyValueChangeSetting = 1,
 NSKeyValueChangeInsertion = 2, //针对集合类型属性,表示插入元素到该集合对象
 NSKeyValueChangeRemoval = 3, //同上,表示移除元素从该集合对象
 NSKeyValueChangeReplacement = 4 //同上,表示从该集合对象属相总替换元素
};
typedef NSUInteger NSKeyValueChange;

需要注意,添加监听的方法addObserver:forKeyPath:options:context:并不会对监听和被监听的对象以及context做强引用,你必须自己保证他们在监听过程中不被释放。

  1. 移除观察者
    当不再需要监听该属性的时候,或者观察者需要被释放前,需要从被观察者队列中移除,否则被观察者继续发送通知则会导致野指针程序崩溃,具体实现如下:
- (void)dealloc {
    [kvoObj removeObserver:self forKeyPath:@"name"];
}

三、自己实现KVO

苹果官方文档:

Automatic key-value observing is implemented using a technique called
isa-swizzling...When an observer is registered for an attribute of an
object the isa pointer of the observed object is modified, pointing to
an intermediate class rather than at the true class. As a result the
value of the isa pointer does not necessarily reflect the actual class
of the instance.

大概意思就是说利用了isa_swizzling技术,大家都知道swizzling是一种OC级别的Hook技术,所以isa_swizzling就是一种isa Hook技术,在一个支持KVO的对象被添加了观察者,系统会为其生成一个子类,重写了setXXX方法(XXX为被监听的属性名),并将该实例的Isa指针指向了新的这个子类(class),这样对被观察者进行属性赋值的时候调用的是重写后的setXXX方法,而setXXX方法内部添加了通知机制;
那么我们自己手动来实现一个简单的KVO:

  • 3.1 自己实现一个添加观察者的方法:
//
//  SUKVO.h
//  DreamOneByOne
//
//  Created by He on 2017/7/16.
//  Copyright © 2017年 Sevenuncle. All rights reserved.
//

#import <Foundation/Foundation.h>

@protocol SUKVODelegate <NSObject>

-(void)su_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context;

@end

@interface SUKVO : NSObject

@property (copy, nonatomic) NSString *desc;

- (void)su_addObserver:(NSObject *)observer forKeyPath:(NSString *)path options:(NSKeyValueObservingOptions)options context:(void *)context;

@end

模仿NSKeyValueObserving协议(分类)定义添加观察者的方法以及一个代理用于接收通知。下面是其内部实现:

// SUKVO.m
#import "SUKVO.h"
#import "SUKVO_SubClass.h"
#import <objc/message.h>
@implementation SUKVO

- (void)su_addObserver:(NSObject *)observer forKeyPath:(NSString *)path options:(NSKeyValueObservingOptions)options context:(void *)context {
    
    //动态添加观察者对象
    objc_setAssociatedObject(self, "observer", observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
    //动态改变被观察者isa指针,使访问到改变后的setXXX方法
    object_setClass(self, [SUKVO_SubClass class]);
}
@end

当添加入一个观察者时,利用了runtime动态添加属性接口将该观察者加入该被观察实例对象上(实际上需要维护一个队列,用于记录所有观察该属性的观察者,这里为了简单),用于后续监听属性改变时给这个观察者发送消息;
然后,就是同样利用runtime改变该被观察者实例对象的isa指针(class),这样后续发送消息给被观察者均是往SUKVO_SubClass定义的方法里找(系统实现的子类为NSKVONotifying_原有类);

  • 3.2 接下来看一下SUKVO_SubClass子类的实现:
#import "SUKVO_SubClass.h"
#import <objc/message.h>

@implementation SUKVO_SubClass

- (void)setDesc:(NSString *)desc {
    [self willChangeValueForKey:@"desc"];  //改变之前通知
    [super setDesc:desc];
    [self didChangeValueForKey:@"desc"]; //改变之后通知

    //通知值改变,这里为了图方便,简单的直接发送改变通知,实际上系统的实现利用了消息通知机制
    id observer = objc_getAssociatedObject(self, "observer");
    if([observer respondsToSelector:@selector(su_observeValueForKeyPath:ofObject:change:context:)]) {
        [observer su_observeValueForKeyPath:@"desc" ofObject:self change:nil context:nil];
    }
}
@end

这样就实现了简单的KVO,不过系统为了满足可以添加多个观察者监听同一个属性的需求,不能像上述实现的这么简单,需要一个一个字典加队列,这样每一个属性对应一个观察者队列,然后由内部一个通知中心统一给观察者发送通知;

四、与 KVC 的关系

关于KVO和KVC之间是否有联系,在网络上搜索了一通,也没个定论,不过大众普遍认为KVO和KVC通常是有联系的;但是,当了解了KVO的实现机制后,如上面自己实现KVO中,发现并未用到KVC,所以部分人开始怀疑KVO真的是基于KVC实现的吗?
那么试想一个问题:

对于一个包含只读(readonly)属性的变量,为什么也能通过setVaule:forKey进行赋值?因为对于默认readonly属性,系统是不会生成set的属性赋值方法的?那根据KVO的原理,是无法进行键值改变的监听的?

为了验证上面这个问题,我们需要确认两个事情:

  1. 对于一个readonly属性并且同时对该属性进行了观察者监听,是否有setXXX方法?
  2. 是否能够使用KVC对readonly属性赋值?

如果上述不包含setXXX方法并且能够使用KVC对只读属性赋值,就说明KVC内部包含了对KVO的支持!

  • 下面开始验证第一个问题:

下面的KVCObject类包含了一个只读属性readonly和一个读写的属性,

@interface KVCObject : NSObject

@property (nonatomic, assign)           NSUInteger count;
@property (nonatomic, copy, readonly)   NSString   *name;
@property (nonatomic, copy)             NSString   *location;

@end

同时对一个KVCObject实例对象添加了对name属性进行监听的观察者:

kvcObj = [KVCObject new];
[kvcObj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
[kvcObj addObserver:self forKeyPath:@"location" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];

打印出此时kvcObj所在的类包含的实例方法:

KVCObject *tmpObj = kvcObj;
    Class currentClass = object_getClass(tmpObj);
    unsigned int methodCount;
    Method *methodList = class_copyMethodList(currentClass, &methodCount);
    int i = 0;
    for (; i < methodCount; i++) {
        NSLog(@"%@ - %@", [NSString stringWithCString:class_getName(currentClass) encoding:NSUTF8StringEncoding], [NSString stringWithCString:sel_getName(method_getName(methodList[i])) encoding:NSUTF8StringEncoding]);
    }
    if( [tmpObj respondsToSelector:@selector(setName:)]) {
        NSLog(@"name");
    }
    if( [tmpObj respondsToSelector:@selector(setLocation:)]) {
        NSLog(@"location");
    }

上面的代码打印出了一个对象添加了监听之后,生成的新的子类包含的实例方法列表:

NSKVONotifying_KVCObject类

可以看出,读写属性NSString * location生成了对应的setXXX方法,而只读属性name没有生成对应的setXXX方法,所以第一个问题得到验证。

  • 验证第二个问题:此时通过KVC改变只读属性的值,能够得到KVO值改变通知?
 [kvcObj setValue:@"newName" forKey:@"name"];

监听回调方法:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"%@", change);
}

输出结果:


KVC修改只读属性

可以看出,只读属性name在没有setName方法的情况下,通过KVC改变值得方式也得到了KVO通知,所以可以下结论,KVC内部的实现机制支持了KVO,KVO是依赖KVC的,并不像部分人怀疑的KVC和KVO之间毫无联系。

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

推荐阅读更多精彩内容