KVC/KVO是观察者模式的一种实现,在Cocoa中是以被万物之源NSObject类实现的NSKeyValueCoding/NSKeyValueObserving非正式协议的形式被定义为基础框架的一部分。从协议的角度来说,KVC/KVO本质上是定义了一套让我们去遵守和实现的方法。
当然,KVC/KVO实现的根本是Objective-C的动态性和runtime,另外,KVC/KVO机制离不开访问器方法的实现,这在后文中也有解释。
KVO详解
该类必须支持KVC,使用属性的 setter getter 方法,或 key-path;(需要实现与该属性对应的getter 和 setter 方法和其他一些可选的方法。NSObject类已经帮我们实现了这些,只要你的类继承自NSObject,并且使用正常方式创建属性,这些属性都是支持KVO的;(实例变量 检测不到变化))
KVO通知发出方式分为“手动”和“自动”两种方式;
- 自动:自动通知由NSObject默认实现了,一般情况下不需要我们写额外代码,属性改变后通知自动发出;
- 手动:可自行控制,重写该方法(NSObject类目方法)
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey;
可控制具体针对哪些属性;
网上好多说手动实现,需要写下面两个方法,但是实际代码验证过程发现,不写也没事;
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
有时候会存在这样一种情况,一个属性的改变依赖于别的一个或多个属性的改变,也就是说当别的属性改了,这个属性也会发出通知
需要我们重写下面方法:
// 当 B 或 C 属性变化时,A属性也会发出通知并监听到(依赖键)
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@“A”]) {
NSArray *affectingKeys = @[@“B”,@“C”];
keyPaths = [keyPaths setByAddingObjectsFromArray: affectingKeys];
}
return keyPaths;
}
- KVO是基于runtime 实现的;
- 当某个类的属性被第一次观察时,系统会在运行期动态的创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的setter方法。派生类在被重写的setter方法内实现真正的通知机制;
- 如果原类为 A,那么生成的派生类名为NSKVONotifying_A
object_getClassName(“类对象”) 获取到 NSKVONotifying_A 类名- 每个类对象(类的实例)都有一个isa指针 指向当前类,当一个类对象的第一次被观察,系统会将isa指针指向动态生成的派生类,从而给被监控属性赋值时执行派生类的setter方法;(参考3)
当手动创建时:系统会发出下面警告
KVO failed to allocate class pair for name NSKVONotifying_LabColor, automatic key-value observing will not work for this class
详解KVC
NSKeyValueCoding:一种通过名称或键 间接访问对象属性或者给对象赋值的机制,而不需要明确的存取方法。这样就可以在运行时动态在访问和修改对象的属性。
NSObject 的类目方法。所有继承自 NSObject的都支持KVC 这个机制;
KVC是怎么寻找Key的?
例如:personal类 name属性
setter
1、程序优先调用属性 set 方法,代码通过 setter 方法完成设置。
2、如果没有找到 setName:方法,KVC机制会检查,该方法
+(BOOL)accessInstanceVariablesDirectly
方法有没有返回YES,默认会返回YES,则这个时候KVC机制会搜索该类里有没有命名为“_name”、“_isName”、“name”、“isName”,
的成员变量,
- (void)setValue:(id)value forUndefinedKey:(NSString *)key;
3、当重写+(BOOL)accessInstanceVariablesDirectly
函数并返回NO时,那么只要没有该属性时,即使声明了相关的成员变量,也会执行该setValue: forUndefinedKey:
函数;
GET 搜索模式
1、 这是valueForKey:的默认实现,给定一个key当做输入参数,开始下面的步骤,在这个接收valueForKey:方法调用的类内部进行操作。
通过getter方法搜索实例,例如get, , is, _的拼接方案。按照这个顺序,如果发现符合的方法,就调用对应的方法并拿着结果跳转到第五步。否则,就继续到下一步。
2、 如果没有找到简单的getter方法,则搜索其匹配模式的方法countOf、objectInAtIndex:、AtIndexes:。
如果找到其中的第一个和其他两个中的一个,则创建一个集合代理对象,该对象响应所有NSArray的方法并返回该对象。否则,继续到第三步。
代理对象随后将NSArray接收到的countOf、objectInAtIndex:、AtIndexes:的消息给符合KVC规则的调用方。
当代理对象和KVC调用方通过上面方法一起工作时,就会允许其行为类似于NSArray一样。
3、如果没有找到NSArray简单存取方法,或者NSArray存取方法组。则查找有没有countOf、enumeratorOf、memberOf:命名的方法。
如果找到三个方法,则创建一个集合代理对象(NSKeyValueSet),该对象响应所有NSSet方法并返回。否则,继续执行第四步。
此代理对象随后转换countOf、enumeratorOf、memberOf:方法调用到创建它的对象上。实际上,这个代理对象和NSSet一起工作,使得其表象上看起来是NSSet。
4、如果没有发现简单getter方法,或集合存取方法组,以及接收类方法accessInstanceVariablesDirectly是返回YES的。搜索一个名为_、_is、、is的实例,根据他们的顺序。
如果发现对应的实例,则立刻获得实例可用的值并跳转到第五步,否则,跳转到第六步。
5、如果取回的是一个对象指针,则直接返回这个结果。
如果取回的是一个基础数据类型,但是这个基础数据类型是被NSNumber支持的,则存储为NSNumber并返回。
如果取回的是一个不支持NSNumber的基础数据类型,则通过NSValue进行存储并返回。
6、如果所有情况都失败,则调用valueForUndefinedKey:方法并抛出异常,这是默认行为。但是子类可以重写此方法。
关于nil值的处理
(1)如果属性基本类型是(int、float、double)且传入对应的参数,如果value设置一个nil,就会发生异常。
(2)可通过重写-(void)setNilValueForKey:(NSString *)key
系统函数,来做相关异常处理;
KVC 集合运算符
- Simple Collection Operators 简单的集合操作符
- Object Operators 对象操作符
- Array and Set Operators 数组/集合操作符
1、@count 返回一个值为集合中对象总数的NSNumber对象;
2、@avg 首先把集合中的每个对象都转换为double类型,然后计算其平均值,并返回这个平均值的NSNumber对象;
3、@max 使用compare:方法来确定最大值,并返回最大值的NSNumber对象.所以为了保证其正常比较,集合中所有的对象都必须支持和另一个对象的比较,保证其可比性;
4、@min 原理和@max一样,其返回的是集合中的最小值的NSNumber对象;
5、@sum 首先把集合中的每个对象都转换为double类型,然后计算其总和,并返回总和的NSNumber对象;
Simple Collection Operators 简单的集合操作符
// kvc 集合操作(基本数据类型)
KVCCollectionOperatorsTest *collectionTest = [[KVCCollectionOperatorsTest alloc] init];
collectionTest.name = @"测试1";
collectionTest.count = 10;
KVCCollectionOperatorsTest *collectionTest2 = [[KVCCollectionOperatorsTest alloc] init];
collectionTest2.name = @"测试2";
collectionTest2.count = 20;
KVCCollectionOperatorsTest *collectionTest3 = [[KVCCollectionOperatorsTest alloc] init];
collectionTest3.name = @"测试3";
collectionTest3.count = 30;
NSArray *collectionArr = @[collectionTest, collectionTest2, collectionTest3];
NSNumber *countNum = [collectionArr valueForKeyPath:@"@count"];
NSNumber *avgNum = [collectionArr valueForKeyPath:@"@avg.count"];
NSNumber *maxNum = [collectionArr valueForKeyPath:@"@max.count"];
NSNumber *minNum = [collectionArr valueForKeyPath:@"@min.count"];
NSNumber *sunNum = [collectionArr valueForKeyPath:@"@sum.count"];
// 若操作对象(数组/集合)内的元素本身就是 NSNumber 对象,那么可以这样写.
NSArray *collectionArr2 = @[@(collectionTest.count), @(collectionTest2.count), @(collectionTest3.count)];
NSNumber *countNum2 = [collectionArr2 valueForKeyPath:@"@count"];
NSNumber *avgNum2 = [collectionArr2 valueForKeyPath:@"@avg.self"];
NSNumber *maxNum2 = [collectionArr2 valueForKeyPath:@"@max.self"];
NSNumber *minNum2 = [collectionArr2 valueForKeyPath:@"@min.self"];
NSNumber *sunNum2 = [collectionArr2 valueForKeyPath:@"@sum.self"];
Object Operators 对象操作符
-
@unionOfObjects:
获取数组中每个对象的属性的值,放到一个数组中并返回,但不会去重; -
@distinctUnionOfObjects:
获取数组中每个对象的属性的值,放到一个数组中并返回,会对数组去重.所以,通常这个对象操作符可以用来对数组元素的去重,快捷高效;
NSArray *unionOfObjects = [collectionArr valueForKeyPath:@"@unionOfObjects.name"];
NSArray *distinctUnionOfObjects = [collectionArr valueForKeyPath:@"@distinctUnionOfObjects.name"];
NSLog(@"unionOfObjects = %@",unionOfObjects);
NSLog(@"distinctUnionOfObjects = %@",distinctUnionOfObjects);
// 输出
2018-04-16 16:59:11.891950+0800 KVODemo[8751:365605] unionOfObjects = (
aaa,
bbb,
aaa
)
2018-04-16 16:59:12.791130+0800 KVODemo[8751:365605] distinctUnionOfObjects = (
aaa,
bbb
)
Array and Set Operators 数组和集合操作符
数组和集合操作符作用对象是嵌套的集合,也就是说,是一个集合且其内部每个元素是一个集合。数组和集合操作符包括 @distinctUnionOfArrays
,@unionOfArrays
,@distinctUnionOfSets:
-
@distinctUnionOfArrays
、@unionOfArrays
返回一个数组,其中包含这个集合中每个数组对于这个操作符右面指定的 keyPath 进行操作之后的值。distinct 会移除重复的值。 -
@distinctUnionOfSets
和@distinctUnionOfArrays
差不多,但是它期望的是一个包含着NSSet对象的NSSet,并且会返回一个NSSet对象。因为集合不能包含重复的值,所以只有distinct操作;
/*
数组和集合操作符
* @distinctUnionOfArrays:
* @unionOfArrays:
* @distinctUnionOfSets:
*/
KVCCollectionOperatorsTest *arrAndSetTest = [[KVCCollectionOperatorsTest alloc] init];
arrAndSetTest.name = @"aaa";
arrAndSetTest.count = 10;
KVCCollectionOperatorsTest *arrAndSetTest2 = [[KVCCollectionOperatorsTest alloc] init];
arrAndSetTest2.name = @"bbb";
arrAndSetTest2.count = 20;
KVCCollectionOperatorsTest *arrAndSetTest3 = [[KVCCollectionOperatorsTest alloc] init];
arrAndSetTest3.name = @"aaa";
arrAndSetTest3.count = 30;
KVCCollectionOperatorsTest *arrAndSetTest4 = [[KVCCollectionOperatorsTest alloc] init];
arrAndSetTest4.name = @"arrAndSetTest";
arrAndSetTest4.count = 10;
KVCCollectionOperatorsTest *arrAndSetTest5 = [[KVCCollectionOperatorsTest alloc] init];
arrAndSetTest5.name = @"arrAndSetTest2";
arrAndSetTest5.count = 20;
KVCCollectionOperatorsTest *arrAndSetTest6 = [[KVCCollectionOperatorsTest alloc] init];
arrAndSetTest6.name = @"arrAndSetTest3";
arrAndSetTest6.count = 30;
NSArray *arrayAndSetCollectionArr = @[arrAndSetTest, arrAndSetTest2, arrAndSetTest3];
NSArray *arrayAndSetCollectionArr2 = @[arrAndSetTest4, arrAndSetTest5, arrAndSetTest6];
NSMutableArray *totalCount = [NSMutableArray array];
[totalCount addObject:arrayAndSetCollectionArr];
[totalCount addObject:arrayAndSetCollectionArr2];
NSLog(@"--- %@",[totalCount valueForKeyPath:@"@unionOfArrays.name"]);
NSLog(@"+++ %@",[totalCount valueForKeyPath:@"@distinctUnionOfArrays.name"]);
输出
2018-04-17 10:23:41.399298+0800 KVODemo[1280:55204] --- (
aaa,
bbb,
aaa,
arrAndSetTest,
arrAndSetTest2,
arrAndSetTest3
)
2018-04-17 10:23:41.936263+0800 KVODemo[1280:55204] +++ (
bbb,
aaa,
arrAndSetTest,
arrAndSetTest2,
arrAndSetTest3
)