iOS KVC/KVO小结


本文对 KVC、KVO 相关知识进行全面的整理总结,介绍了相关的基本概念、使用方法、注意事项、实现原理等。后续如有更深的理解会继续整理总结。

简介

KVC ( Key-value coding 键值编码 ) 是一种由 NSKeyValueCoding 非正式协议启用的机制,对象采用该机制提供对其属性的间接访问。当对象符合键值编码时,通过字符串名称访问对象属性。
键值编码的机制也是其他 Cocoa 框架的基础,例如 KVO。

KVO ( Key-value observing 键值观察 ) 这一机制基于 NSKeyValueObserving 非正式协议,Cocoa 通过这个协议为所有遵守协议的对象提供了一种自动化的属性观察能力。对目标对象的某属性添加观察,当该属性发生变化时,通过触发观察者对象实现的 KVO 接口方法,来通知观察者。KVO 是 Cocoa 框架使用观察者模式的一种途径。

KVC

基本使用方法

KVC 提供了简洁的方法,来访问对象属性。

- (nullable id)valueForKey:(NSString *)key;  
- (void)setValue:(nullable id)value forKey:(NSString *)key;

上面两个方法,分别是对应于 getter 访问器的 valueForKey: 和对应于 setter 访问器的 setValue:forKey: 。

  • valueForKey: 首先查找以键 -getKey、 -key 或 -isKey 命名的 getter 方法。如果不存在 getter 方法(假设没有通过@synthesize提供存取方法),它将在对象内部查找名为 _key 或 key 的实例变量。如果最后没找调用 valueForUndefinedKey: 方法。
  • setValue:forKey: 首先查找以键 -setKey、 -_setKey 命名的 setter 方法,如果不存在 setter 方法,它将在类中查找名为 _key 或 key 的实例变量。如果最后没找到则调用 setValue:forUndefinedKey: 方法。

例如某对象有属性 name、age,我们就可使用上面两个方法进行访问、设置属性值。

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;

[obj setValue:@"jone" forKey:@"name"];
[obj setValue:@(10) forKey:@"age"];
id stAge = [obj valueForKey:@"age"];

对于属性是基本的数据类型时 (int, CGFloat) 是放入 NSNumber 或 NSValue 中来设置的。
相比直接访问,KVC的效率会稍低一点,所以只有当你非常需要它提供的可扩展性时才使用它。

其他使用方法

1、属性的属性的访问
KVC 还提供了访问属性的属性的操作方法:

- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;

例如下面两个类:

@interface Student : NSObject
@property (nonatomic, copy) NSString *name;
@end

@interface Teacher : NSObject
@property (nonatomic, strong) Student *student;
@end

//路径访问
[teacher setValue:@"haha" forKeyPath:@"student.name"];
id name = [teacher valueForKeyPath:@"student.name"];

2、多值访问
同时访问多个属性的方法:

[obj setValuesForKeysWithDictionary:@{@"name":@"Tom", @"age":@(2)}];
NSDictionary *values = [obj dictionaryWithValuesForKeys:@[@"name",@"age"]];

3、集合属性
KVC 同样适用于集合对象,可以通过 valueForKey: 和 setValue:forKey:(或它们的键路径方式)获取或设置集合对象。

//@property (nonatomic, copy) NSArray<Student *> *students;
id array = [teacher valueForKeyPath:@"students.name"];
//返回数组,包含属性 student.name

KVC 还提供了接口 mutableArrayValueForKey:、 mutableSetValueForKey: 来操作集合类型的属性。

//@property (nonatomic, copy) NSArray *items;

obj.items = @[@"a", @"b", @"c"];
NSMutableArray *items = [obj mutableArrayValueForKey:@"items"];
[items addObject:@"d"];
//添加后,同时也改变了 obj.items

4、运算符
运算符是一个特殊的 Key Path,可以作为参数传递给 valueForKeyPath:方法,注意只能是这个方法,如果传给了valueForKey:方法会崩溃。
运算符是一个以@开头的特殊字符串:

  • 简单集合运算符有 @avg,@count,@max,@min,@sum
  • 对象运算符,比集合运算符稍微复杂,能以数组的方式返回指定的内容,有两种 @distinctUnionOfObjects、@unionOfObjects ,前者会去除重复的以后返回,后者直接返回。
  • Array和Set操作符,这种情况更复杂了,说的是集合中包含集合的情况,有三种 @distinctUnionOfArrays、@unionOfArrays、@distinctUnionOfSets,前两个针对的集合是Arrays,后一个针对的集合是Sets。因为Sets中的元素本身就是唯一的,所以没有对应的 @unionOfSets 运算符。
NSNumber *value = [teacher valueForKeyPath:@"students.@max.age"];
NSNumber *count = [teacher valueForKeyPath:@"students.@count"];

 NSArray * array = [teacher valueForKeyPath:@"students.@distinctUnionOfObjects.age"];
 
 NSMutableArray *someStudents = [NSMutableArray array];
[someStudents addObject:@[st0, st1, st2]];
[someStudents addObject:@[st3, st4]];
id value = [someStudents valueForKeyPath:@"@distinctUnionOfArrays.age"];

异常情况

1、找不到对应的 key
当调用 setValue:forKey: 或者 valueForKey: 找不到对应 key 命名的属性时,就会 NSUnknownKeyException 异常崩溃,可以在对象里重写下面两个方法,防止崩溃。

- (id)valueForUndefinedKey:(NSString *)key {
    return nil;
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
}

2、将不是对象类型的属性设置为 nil
将对象赋值为 nil 这是可以的相当于把对象置空。但是当使用 setValue:forKey: 将非对象类型的属性值( int、CGFloat、结构体等),设置为 nil 时会 NSInvalidArgumentException 异常崩溃。我们可以重写方法 setNilValueForKey: 处理设置为 nil 的情况:

//@property (nonatomic, assign) int age;
//[obj setValue:nil forKey:@"age"];

- (void)setNilValueForKey:(NSString *)key {
    if ([key isEqualToString:@"age"]) {
        [self setValue:@(0) forKey:@"age"];
    } else {
        [super setNilValueForKey:key];
    }
}


KVO

KVO 是 Cocoa 框架使用观察者模式的一种途径。 KVC 是 KVO 技术实现的基础 ,参与 KVO 的对象需要符合 KVC 的要求和存取方法,也可以手动实现观察者通知。

使用方法

1、添加观察:

[obj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:(__bridge void *)self];
  • options 回调选项,NSKeyValueObservingOptionOld 表示获取旧值,NSKeyValueObservingOptionNew 表示获取新值,NSKeyValueObservingOptionInitial 表示在添加观察的时候就立马响应一个回调,NSKeyValueObservingOptionPrior 表示在被观察属性变化前后都回调一次。

  • context 可以是 C 指针或者一个对象引用,既可以当作一个唯一的标识来分辨被观察的变更,也可以向观察者提供数据。

2、观察回调:

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

/*
observe  key = name, obj = <Student: 0x6000016c6e80>, change = {
    kind = 1;
    new = Tony;
    old = Tom;
}
*/

change 是一个字典,对应的键有:NSKeyValueChangeKindKey、NSKeyValueChangeNewKey、NSKeyValueChangeOldKey、NSKeyValueChangeIndexesKey、NSKeyValueChangeNotificationIsPriorKey。

NSKeyValueChangeKindKey 指明了变更类型,设置、插入、移除、替换:

enum {
   NSKeyValueChangeSetting = 1,
   NSKeyValueChangeInsertion = 2,
   NSKeyValueChangeRemoval = 3,
   NSKeyValueChangeReplacement = 4
};
typedef NSUInteger NSKeyValueChange;

NSKeyValueChangeNotificationIsPriorKey 指明是变更前或变更后,触发的回调。

3、移除观察:

[obj removeObserver:self forKeyPath:@"name"];

移除观察,和移除通知比较类似,需要在不用继续观察的时候移除它,比如在控制器的 dealloc 方法里面释放,注意重复移除会 crash。

4、调试 KVO
可以打断点,在 lldb 中查看被观察对象的所有观察信息。

lldb po [obj observationInfo]

这会打印出有关谁观察谁之类的很多信息。

KVO 兼容

有两种方法可以保证变更通知被发出。自动发送通知是 NSObject 提供的,并且一个类中的所有属性都默认支持,只要是符合 KVC 的。
手动变更通知需要些额外的代码,但也对通知发送提供了额外的控制。可以通过重写子类 automaticallyNotifiesObserversForKey: 方法的方式控制子类一些属性的自动通知。

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"name"]) {
        return NO;
    } else {
        return [super automaticallyNotifiesObserversForKey:key];
    }
}

- (void)setName:(NSString *)name {
    if (![_name isEqualToString:name]) {
        [self willChangeValueForKey:@"name"];
        _name = [name copy];
        [self didChangeValueForKey:@"name"];
    }
}

//如果一个操作导致多个键变化,需要嵌套变更通知
- (void)setLastName:(NSString *)lastName {
    [self willChangeValueForKey:@"lastName"];
    [self willChangeValueForKey:@"fullName"];
    _lastName = [lastName copy];
    _fullName = [NSString stringWithFormat:@"Title %@", lastName];
    [self didChangeValueForKey:@"fullName"];
    [self didChangeValueForKey:@"lastName"];
}

当观察某个对象的集合属性时,当直接使用 obj.mutableArray 添加、删除、替换元素时,不会触发观察回调,需要手动添加代码 willChange:valuesAtIndexes:forKey: 和 didChange:valuesAtIndexes:forKey: 来通知集合属性发生了变化。或者使用 KVC 来操作集合属性。如下例子:

//某类集合属性
//@property (nonatomic, strong) NSMutableArray *myArray;

//添加观察
[obj addObserver:self forKeyPath:@"myArray" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];

//变更集合时,手动通知观察者
[self.obj willChange:NSKeyValueChangeRemoval valuesAtIndexes:[NSIndexSet indexSetWithIndex:0] forKey:@"myArray"];
[self.obj.myArray removeObjectAtIndex:0];
[self.obj didChange:NSKeyValueChangeRemoval valuesAtIndexes:[NSIndexSet indexSetWithIndex:0] forKey:@"myArray"];

//或者使用 KVC 操作集合,会自动通知观察者
NSMutableArray *array = [self.obj mutableArrayValueForKey:@"myArray"];
[array removeObjectAtIndex:0];

注册从属键

某些情况下,一个属性的值取决于另一个对象中的一个或多个其他属性的值。如果一个属性的值发生更改,则还应标记派生属性的值以进行更改。
例如,fullName 取决于 firstName 和 lastName。当 firstName 或 lastName 发生改变时,必须通知观察 fullName 属性的程序,因为它们影响这个属性的值。
重写 keyPathsForValuesAffectingValueForKey 来指定 fullName 属性依赖于lastName和firstName。

- (void)setLastName:(NSString *)lastName {
    _lastName = [lastName copy];
    _fullName = [NSString stringWithFormat:@"%@ %@", _firstName, lastName];
}

//重写指定 fullName 属性依赖于lastName
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"fullName"]) {
        NSArray *affectingKeys = @[@"lastName", @"firstName"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

//或者重写
+ (NSSet<NSString *> *)keyPathsForValuesAffectingFullName {
    return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}

基本原理

KVO 是基于 runtime 运行时来实现的,当你观察了某个对象的属性,内部会生成一个该对象所属类的子类,中间类,然后重写被观察属性的 setter 方法,当然在重写的方法中会调用父类的 setter 方法从而不会影响框架使用者的逻辑,之后会将该对象的 isa 指针指向新创建的这个类,最后会重写 -(Class)class; 方法,让使用者通过 [obj class] 查看当前对象所属类的时候会返回其父类,使其看似没有改变什么,让你觉得不需要添加额外的代码,就能使用 KVO。
Apple 并不希望过多暴露 KVO 的实现细节。想要深究实现细节,可查看文章
下面例子,通过 object_getClass() 方法查看观察前后的变化。

//object_getClass 获取 isa 指针指向的对象
//object_setClass 更改对象的 isa 指针指向。将对象设置为别的类类型,返回原来的Class
NSLog(@"1 --- %p %@ %p", obj, object_getClass(obj), [obj methodForSelector:@selector(setName:)]);

[obj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:(__bridge void *)self];

NSLog(@"2 --- %p %@ %p", obj, object_getClass(obj), [obj methodForSelector:@selector(setName:)]);


[self.obj removeObserver:self forKeyPath:@"name"];
NSLog(@"3 --- %p %@ %p", self.obj, object_getClass(self.obj), [self.obj methodForSelector:@selector(setName:)]);

//1 --- 0x60000225cea0  AStudent 0x1067f72a0
//2 --- 0x60000225cea0  NSKVONotifying_AStudent 0x7fff258e454b
//3 --- 0x60000225cea0  AStudent 0x1067f72a0

通过上面的打印结果可知,添加观察后,原本的类变成了 NSKVONotifying_AStudent,移除观察后又变回去了, setName: 方法也发生了变化。


References

KVC/KVO原理详解及编程指南
Objective-C中的KVC和KVO
透彻理解 KVO 观察者模式

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