iOS开发iOS DeveloperiOS学习笔记

KVC & KVO 小结

2016-06-30  本文已影响152人  4d1487047cf6

KVC

什么是KVC?

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

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

KVC方法

读:

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

写:

若指定的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:这个方法的实现大概是这样的:

一对多关系属性的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:方法内部大概是这样的:

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

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

集合运算符

Operator key path formatOperator key path format

上例中,

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兼容的, 亦需满足三个条件:

看个例子. 以下两方法都在都一观察者类中, 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中国

上一篇 下一篇

猜你喜欢

热点阅读