KVO, KVC详解

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”
的成员变量,

"_%s"、"_is%s"、"%s"、"is%s"
无论该变量是在接口类定义还是在类的实现部分定义,也无论用了什么访问修饰符,KVC都会对该成员变量进行操作。当既没有setter方法,没有属性、也没有上述成员变量时,程序会走下面这个函数

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

推荐阅读更多精彩内容