KVC & KVO 小结

KVC

什么是KVC?

KVC(Key-value coding)是一种通过字符串去识别并间接存取(access)对象属性的机制, 该机制区别于直接通过存取方法(accessor)和实例变量去访问. 本质上, KVC定义了实现存取方法的模式与方法签名.

KVC可用来访问三种不同类型的对象值: attribute, 一对一关系, 一对多关系.

KVC方法

读:

  • -valueForKey:
  • -valueForKeyPath:
  • -dicitionaryWithValuesForKeys:

对应key的值不存在时将发送消息valueForUndefinedKey:给自己, 该方法默认实现抛出NSUndefinedKeyException. 可自行重写该方法.

写:

  • -setValue:ForKey:
  • -setValue:ForKeyPath:
  • -setValuesForKeysWithDicitonary:

若指定的key不存在调用者将被发送消息setValue:forUndefinedKey:, 同样该方法默认抛出NSUndefinedKeyException.

若把nil赋给一个非对象类型的属性, 调用者被发送setNilValueForKey:消息, 该方法默认抛出NSInvalidArgumentException. 自己可重写该方法来实现正确的赋值. 例如:

// MyModel.h
@interface MyModel : NSObject
@property (nonatomic, assign) BOOL hidden;
@property (nonatomic, assign, readonly) NSInteger num;
@end


// MyModel.m
@implementation MyModel
- (void)setNilValueForKey:(NSString *)key {
    if ([key isEqualToString:@"num"]) {
        [self setValue:@0 forKey:@"num"];
    } else if ([key isEqualToString:@"hidden"]) {
        [self setValue:@NO forKey:@"hidden"];
    } else {
        [super setNilValueForKey:key];
    }
}
@end

KVC与点语法访问方法

两种方法可同时并存使用.
如下例所示, 定义了一个类:

@interface MyClass
@property NSString *stringProperty;
@property NSInteger integerProperty;
@property MyClass *linkedInstance;
@end

用KVC访问属性如下:

MyClass *myInstance = [[MyClass alloc] init];
NSString *string = [myInstance valueForKey:@"stringProperty"];
[myInstance setValue:@2 forKey:@"integerProperty"];

以下两种方式结果是一样的:

MyClass *anotherInstance = [[MyClass alloc] init];
myInstance.linkedInstance = anotherInstance;
myInstance.linkedInstance.integerProperty = 2;
MyClass *anotherInstance = [[MyClass alloc] init];
myInstance.linkedInstance = anotherInstance;
[myInstance setValue:@2 forKeyPath:@"linkedInstance.integerProperty"];

KVC兼容(Key-value coding compliant)

KVC兼容是对于类中某个特定属性(property)来说的, 所谓KVC兼容实际指的是某个属性可通过-valueForKey:, -setValue:forKey:KVC方法去访问属性.

在Objective-C 2.0里的@property实质上也就是一对setter, getter访问器方法加一个实例变量.

先看个例子:
//MyModel.m

@implementation

...
{
    NSObject *_myObj;
}

- (NSObject *)myObj {
    if (!_myObj) {
        _myObj = [[NSObject alloc] init];
    }
    return _myObj;
}

- (void)setMyObj:(NSObject *)obj {
    _myObj = obj;
}

@end

调用时,

NSLog(@"MyModel myObj: %@", model.myObj);
NSObject *obj = [[NSObject alloc] init];
NSLog(@"auto obj: %@", obj);
[model setValue:obj forKey:@"myObj"];
NSLog(@"MyModel myObj: %@", model.myObj);

打印出来:

2016-06-30 15:19:26.991 TestKVC[19217:20399799] MyModel myObj: <NSObject: 0x7fea43424fe0>
2016-06-30 15:19:26.991 TestKVC[19217:20399799] auto obj: <NSObject: 0x7fea45813d90>
2016-06-30 15:19:26.991 TestKVC[19217:20399799] MyModel myObj: <NSObject: 0x7fea45813d90>

以上, 对于myObj这个属性来说, 它就是KVC兼容的.

其实, 没有实例变量也是可以的.

// MyModel.m
...
- (NSObject *)noSuchObj {
    NSLog(@"getter method");
    return nil;
}

- (void)setNoSuchObj:(NSObject *)obj {
    NSLog(@"setter method");
}

调用时,

//invoke getter accessor
[model valueForKey:@"noSuchObj"];
//invoke setter accessor
[model setValue:@"nothing" forKey:@"noSuchObj"];
//same as the above one for dot syntax
model.noSuchObj = @"nothing";

打印出来是,

2016-06-30 15:19:26.992 TestKVC[19217:20399799] getter method
2016-06-30 15:19:31.439 TestKVC[19217:20399799] setter method
2016-06-30 15:19:38.882 TestKVC[19217:20399799] setter method

实际上-valueForKey:, -setValue:forKey:之类的KVC方法在运行时会按照一定的顺序去调用遵循特定方法签名的访问器方法或直接访问实例变量.

例如-setValue:forKey:这个方法的实现大概是这样的:

  • 查看调用对象所属类是否实现了-set<Key>:这样的访问器方法, 有则调用;
  • 若没有, 调用者类方法-accessInstanceVariablesDirectly放回YES, 然后依次搜索看是否存在命名方式为 _<key>, _is<Key>, <key>, is<Key>这样的实例变量, 有则直接赋值于它.
  • 若遵循这种命名形式的访问器方法和实例变量都无, 则调用setValue:forUndefinedKey:方法.

一对多关系属性的KVC, 集合代理

先看个例子:

//MyModel.m

...

static int32_t const primes[] = {
    2, 101, 233, 383, 3, 103, 239, 389, 5, 107, 241, 397, 7, 109,
    251, 401, 11, 113, 257, 409, 13, 127, 263, 419, 17, 131, 269,
    421, 19, 137, 271, 431, 23, 139, 277, 433, 29, 149, 281, 439,
    31, 151, 283, 443, 37, 157, 293, 449, 41, 163, 307, 457, 43,
    167, 311, 461, 47, 173, 313, 463, 53, 179, 317, 467, 59, 181,
    331, 479, 61, 191, 337, 487, 67, 193, 347, 491, 71, 197, 349,
    499, 73, 199, 353, 503, 79, 211, 359, 509, 83, 223, 367, 521,
    89, 227, 373, 523, 97, 229, 379, 541, 547, 701, 877, 1049,
    557, 709, 881, 1051, 563, 719, 883, 1061, 569, 727, 887,
    1063, 571, 733, 907, 1069, 577, 739, 911, 1087, 587, 743,
    919, 1091, 593, 751, 929, 1093, 599, 757, 937, 1097, 601,
    761, 941, 1103, 607, 769, 947, 1109, 613, 773, 953, 1117,
    617, 787, 967, 1123, 619, 797, 971, 1129, 631, 809, 977,
    1151, 641, 811, 983, 1153, 643, 821, 991, 1163, 647, 823,
    997, 1171, 653, 827, 1009, 1181, 659, 829, 1013, 1187, 661,
    839, 1019, 1193, 673, 853, 1021, 1201, 677, 857, 1031,
    1213, 683, 859, 1033, 1217, 691, 863, 1039, 1223, 1229,
};

- (NSUInteger)countOfPrimes;
{
    return (sizeof(primes) / sizeof(*primes));
}

- (id)objectInPrimesAtIndex:(NSUInteger)idx;
{
    NSParameterAssert(idx < sizeof(primes) / sizeof(*primes));
    return @(primes[idx]);
}

在上述MyModel类的实现文件里, 加上了一个装有一堆质数的静态数组, 以及定义了两个方法.

瞧这两方法是否看起来类似于NSArray的原始(primitive)方法 -count-objectAtInIndex:呢?

使用时:

//invoke collection accessors
id proxy = [model valueForKey:@"primes"];
NSLog(@"MyModel last prime: %@", [proxy lastObject]);

这里我们是把primes当做一个NSArray来使用的. 注意上面例子中并没有声明有primes这么一个属性的, 也无这么一个实例变量对象. 我们可以通过valueForKey:去获取, 那么它一定是实现KVC了.

实际上, 这里valueForKey:方法内部大概是这样的:

  • 依次查找get<Key>, <key>, is<Key>形式的访问器方法.
  • 找不到则查找匹配模式countOf<Key>objectIn<Key>AtIndex:(或objectsAtIndexes:)的方法. 若找到, 则返回一个集合代理对象(collection proxy object), 该对象可使用所有NSArray的方法.
  • 找不到则查找countOf<Key>, enumeratorOf<Key>, memberOf<Key>:, 同理如上, 不过对应的是NSSet, 无序集合.
  • 再找不到则查找匹配命名_<key>, _is<Key>, <key>, or is<Key>的实例变量.
  • 否则调用valueForUndefinedKey:.

上例中的proxy就是一个集合代理对象, 调用[proxy class]得知它是一个NSKeyValueArray类型.

集合代理对象里, 包括了有序与无序, 可变与不可变的不同情况, 其分别也对应于实现不同的方法以KVC兼容. 但它们机理都是类似的.

集合运算符

Operator key path format
Operator key path format
  • 简单运算符:@avg, @count, @max, @min. @sum.
  • 对象运算符:@distinctUnionOfObjects, @unionOfObjects.
  • 数组,集合运算符:@distinctUnionOfArrays, @unionOfArrays, @distinctUnionOfSets.

上例中,

id proxy = [model valueForKey:@"primes"];
NSLog(@"MyModel primes count: %lu", [proxy count]);
NSLog(@"MyModel primes count: %@", [model valueForKeyPath:@"primes.@count"]);

打印的两个count结果都是一样的, 都是201.

KVO

接收一个属性的KVO通知, 需三个条件:

  • 被观察的属性是KVO兼容的.(KVO Compliant)
  • 注册观察者:发送消息addObserver:forKeyPath:options:context:到被观察对象.
  • 观察着对象需实现observeValueForKeyPath:ofObject:change:context:方法.

而属性为KVO兼容的, 亦需满足三个条件:

  • 它是KVC兼容的;
  • 所在类会为该属性发送KVO通知;
  • 依赖键需注册.

看个例子. 以下两方法都在都一观察者类中, self即observer.

// 观察者类中注册键值观察

- (void)registerAsObserver {
    self.model = [[MyModel alloc] init];
    
    [self.model addObserver:self
            forKeyPath:@"num"
               options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
               context:nil];
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [self.model setValue:@"999" forKey:@"num"];
    });
}
// 实现处理KVO通知方法

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSString *,id> *)change
                       context:(void *)context {
    if (object == self.model) {
        if ([keyPath isEqualToString:@"num"]) {
            NSLog(@"old num:%@", change[NSKeyValueChangeOldKey]);
            NSLog(@"new num:%@", change[NSKeyValueChangeNewKey]);
        }        
    }
}

这里打印被观察属性值变化的新旧值.

KVO的自动与手动通知

以上例子为自动通知. 实际上苹果系统framework中类的属性都支持发送自动通知.

而手动通知可实现精细的控制通知发送. 手动通知通过重写NSObject的automaticallyNotifiesObserversForKey:方法判断是否自动.

用法如下例所示:

//MyModel.m
...

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

NO表示键为num的属性为手动KVO.
重写其setter方法:

//MyModel.m
...

- (void)setNum:(NSInteger)num {
    if (num != _num) {
        [self willChangeValueForKey:@"num"];
        _num = num;
        [self didChangeValueForKey:@"num"];
    }
}

参考资料:

Key-Value Observing Programming Guide
Key-Value Coding Programming Guide
KVC 和 KVO objc中国

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

推荐阅读更多精彩内容