KVC、KVO小结

KVO属性依赖

example

看一个例子:我们的模型类 LabColor 代表一种 lab色彩空间里的颜色。和 RGB 不同,这种色彩空间有三个元素 l, a, b。我们要做一个用来改变这些值的滑块和一个显示颜色的方块区域。

上面三个slider会改变LabColor的l、a、b三个属性值,并将获取最新的颜色值显示在视图上。

下面是LabColor模型的代码逻辑

@interface LabColor : NSObject

/// The @b L component in LAB color space
@property (nonatomic) double lComponent;
/// The @b a component in LAB color space
@property (nonatomic) double aComponent;
/// The @b b component in LAB color space
@property (nonatomic) double bComponent;

/// The @b red component in sRGB color space
@property (nonatomic, readonly) double redComponent;
/// The @b green component in sRGB color space
@property (nonatomic, readonly) double greenComponent;
/// The @b blue component in sRGB color space
@property (nonatomic, readonly) double blueComponent;

@property (nonatomic, strong, readonly) UIColor *color;

@end
- (id)init
{
    self = [super init];
    if (self) {
        self.lComponent = 75 + (arc4random_uniform(200) * 0.1 - 10.);
        self.aComponent = 0 + (arc4random_uniform(200) * 0.1 - 10.);
        self.bComponent = 0 + (arc4random_uniform(200) * 0.1 - 10.);
    }
    return self;
}
- (double)redComponent;
{
    return D65TristimulusValues[0] * inverseF(1./116. * (self.lComponent + 16));
}
- (double)greenComponent
{
    return D65TristimulusValues[1] * inverseF(1./116. * (self.lComponent + 16) + 1./500.*self.aComponent);
}
- (double)blueComponent
{
    return D65TristimulusValues[2] * inverseF(1./116. * (self.lComponent + 16) - 1./200.*self.bComponent);
}
- (UIColor *)color
{
    return [UIColor colorWithRed:self.redComponent * 0.01 green:self.greenComponent * 0.01 blue:self.blueComponent * 0.01 alpha:1.];
}

自然的,我们希望l、a、b三个属性值改变的时候,color可以自动地改变,然后在color改变的时候通知LabColor的Color属性观察者。但是通过代码可以发现,l、a、b改变时并不会马上set一个新的color,而是在getColor时以最新的lab属性来计算。通常我们可以通过重写这三个属性的set方法来实现,但是KVO为我们提供了更简单,更自动化的方式。

从LabColor类的代码中可以看到,color属性的值依赖于红、绿、蓝三个元素,而红绿蓝三个元素依赖于lComponentbComponentaComponent中的一个或多个。依照这个逻辑关系,为属性之间添加KVO依赖:

+ (NSSet *)keyPathsForValuesAffectingRedComponent
{
    return [NSSet setWithObject:@"lComponent"];
}

+ (NSSet *)keyPathsForValuesAffectingGreenComponent
{
    return [NSSet setWithObjects:@"lComponent", @"aComponent", nil];
}

+ (NSSet *)keyPathsForValuesAffectingBlueComponent
{
    return [NSSet setWithObjects:@"lComponent", @"bComponent", nil];
}

+ (NSSet *)keyPathsForValuesAffectingColor
{
    return [NSSet setWithObjects:@"redComponent", @"greenComponent", @"blueComponent", nil];
}

这些方法在属性被定义时就会生成,当我们.m中输入方法名时Xcode会给出友好的提示。

这样,当l、a、b改变时,会触发red、green、blue的改变,从而触发color的改变,外界或者内部只需要观察color的变化就行了。


观察属性变化

外部观察

ViewController拥有LabColor类的模型对象作为属性:

@interface ViewController ()
@property (nonatomic, strong) LabColor *labColor;
@end

在控制器中添加模型对象的观察者:

- (void)addObserver:(NSObject *)anObserver
         forKeyPath:(NSString *)keyPath
            options:(NSKeyValueObservingOptions)options
            context:(void *)context

在控制器中实现KVO回调方法:

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context

在恰当的时机移除观察者:

- (void)removeObserver:(NSObject *)anObserver
            forKeyPath:(NSString *)keyPath

当只观察一个属性时,完成这些比较简单,但当观察的属性越来越多,控制器内的代码也将明显地臃肿起来。

自观察

最好为观察者定义一个单独的context

static int const _PrivateColorContext;

添加自己为观察者:

[self addObserver:self 
       forKeyPath:@"color"
          options:options
          context:(void *)&_PrivateColorContext];

自己实现观察者回调:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    
    if (context == &KMPrivateColorContext) {
      ...
    }
    else {
        if ([super respondsToSelector:@selector(observeValueForKeyPath:ofObject:change:context:)]) {
            [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
        }
    }
}

通过这个context可以判断是否是自观察,这将确保我们写的子类都是正确的。如此一来,子类和父类都能安全的观察同样的键值而不会冲突。否则我们将会碰到难以 debug 的奇怪行为。

添加自观察后,可以通过代理或者Block的方式将结果传递给外部。



观察选项

  • NSKeyValueObservingOptionInitial, 是否在观察者注册完成前,立刻发送通知观察者。即addObserver...完成前,观察者回调会马上执行一次。例如,有时候我们需要在第一次运行代码的时候也更新UI,就可以使用这个观察选项。
  • NSKeyValueObservingOptionPrior,在不指定此选项的情况下,通知会在属性值改变后发送,并在其中包含属性的旧值和新值。指定此选项后回调会分两次执行,属性值改变前和属性值改变后。通知的信息字典中,会包含键为NSKeyValueChangeNotificationIsPriorKey的NSNumber值,其布尔值可以用来判断是改变前还是改变后。
  • NSKeyValueObservingOptionNew,通知中会包含属性的新值。
  • NSKeyValueObservingOptionOld,通知中会包含属性的旧值。


自动通知和手动通知

默认情况下,Foundation会为自动调用属性变化通知的相关方法:

- (void)willChangeValueForKey:(NSString *)key;

- (void)didChangeValueForKey:(NSString *)key;

有一些情况下,我们想控制键值改变的通知是否发送:

+ (BOOL)automaticallyNotifiesObserversForLComponent;
{
    return NO;
}

- (void)setLComponent:(double)lComponent;
{
    if (_lComponent == lComponent) {
        return;
    }
    [self willChangeValueForKey:@"lComponent"];
    _lComponent = lComponent;
    [self didChangeValueForKey:@"lComponent"];
}

这种情况下,我们应该尽量使用accessor方法改变键值。并在直接使用成员变量时,考虑是否调用willChange...didChange...来触发通知。



KVO 和线程

一个需要注意的地方是,KVO 行为是同步的,并且KVO的触发和观察者的回调在同样的线程上。没有队列或者 Run-loop 的处理。手动或者自动调用 -didChangeVal... 会触发 KVO 通知。

所以,当我们试图从其他线程改变属性值的时候我们应当十分小心,除非能确定所有的观察者都用线程安全的方法处理 KVO 通知。通常来说,我们不推荐把 KVO 和多线程混起来。如果我们要用多个队列和线程,我们不应该在它们互相之间用 KVO。

KVO 是同步运行的这个特性非常强大,只要我们在单一线程上面运行(比如主队列 main queue),KVO 会保证下列两种情况的发生:

首先,如果我们调用一个支持 KVO 的 setter 方法,如下所示:

self.exchangeRate = 2.345;

KVO 能保证所有 exchangeRate 的观察者在 setter 方法返回前被通知到。

其次,如果某个键被观察的时候附上了 NSKeyValueObservingOptionPrior 选项,直到 观察回调方法-observe... 被调用之前, exchangeRate 的 accessor 方法都会返回同样的值。



键值验证 (KVV)

KVV 也是 KVC API 的一部分。这是一个用来验证属性值的 API,只是它光靠自己很难提供逻辑和功能。
通常我们会重写属性的setter来对值进行验证,但其实不用重写setter,KVV就可以为我们做到:

- (BOOL)validate<Value>:(inout id  _Nullable __autoreleasing *)ioValue for<Key>:(NSString *)inKey error:(out NSError * _Nullable __autoreleasing *)outError

- (BOOL)validate<Value>:(inout id  _Nullable __autoreleasing *)ioValue for<KeyPath>:(NSString *)inKeyPath error:(out NSError * _Nullable __autoreleasing *)outError

// e.g:
- (BOOL)validateEmail:(NSString **)emailP error:(NSError * __autoreleasing *)error
{
    if (*emailP == nil) {
        *emailP = @"";
        return YES;
    } else {

        NSArray *components = [*emailP componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
        *emailP = [[components componentsJoinedByString:@""] lowercaseString];
        return YES;
    }
}

这个方法在Xcode中并不会直接根据属性名给出提示,我们需要自己手动完成。
这个KVV方法会在set时被调用。



KVC

访问属性、成员变量、键路径

KVC 能让我们通过以下的形式访问属性:

@property (nonatomic, copy) NSString *name;
NSString *n = [object valueForKey:@"name"];
[object setValue:@"Daniel" forKey:@"name"];



不仅可以访问作为对象属性,而且也能访问一些标量(例如 int 和 CGFloat)和 struct(例如 CGRect)。Foundation 框架会为我们自动封装它们。举例来说,如果有以下属性:

@property (nonatomic) CGFloat height;

我们可以这样设置它:

[object setValue:@(20) forKey:@"height"];

但是当我们在KVC中对标量或struct传入nil时

[object setValue:nil forKey:@"height"];

程序将会崩溃并抛出这样一个异常

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '[<object 0x600000050ad0> setNilValueForKey]: could not set nil as the value for the key height.'

我们可以重写setNilValueForKey:方法来处理这样的异常

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

KVC 同样允许我们通过关系来访问对象。假设 person 对象有属性 address,address 有属性 city,我们可以这样通过 person 来访问 city:

[person valueForKeyPath:@"address.city"]


简化代码

KVC 允许我们用属性的字符串名称来访问属性,字符串在这儿叫做键。有些情况下,这会使我们非常灵活地简化代码。

假设我们有这么一个Contact模型:

@interface Contact : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickname;
@property (nonatomic, copy) NSString *email;
@property (nonatomic, copy) NSString *city;

@end

还有一个 视图控制器,含有四个对应的 UITextField 属性:

@interface DetailViewController ()

@property (weak, nonatomic) IBOutlet UITextField *nameField;
@property (weak, nonatomic) IBOutlet UITextField *nicknameField;
@property (weak, nonatomic) IBOutlet UITextField *emailField;
@property (weak, nonatomic) IBOutlet UITextField *cityField;

@end

我们可以简化更新 UI 的逻辑。首先我们需要两个方法:一个返回 model 里我们用到的所有键的方法,一个把键映射到对应的文本框的方法:

- (NSArray *)contactStringKeys;
{
    return @[@"name", @"nickname", @"email", @"city"];
}

- (UITextField *)textFieldForModelKey:(NSString *)key;
{
    return [self valueForKey:[key stringByAppendingString:@"Field"]];
}

有了这个,我们可以从 model 里更新文本框,如下所示:

- (void)updateTextFields;
{
    for (NSString *key in self.contactStringKeys) {
        [self textFieldForModelKey:key].text = [self.contact valueForKey:key];
    }
}

我们也可以用一个 action 方法让四个文本框都能实时更新 model:

- (IBAction)fieldEditingDidEnd:(UITextField *)sender
{
    for (NSString *key in self.contactStringKeys) {
        UITextField *field = [self textFieldForModelKey:key];
        if (field == sender) {
            [self.contact setValue:sender.text forKey:key];
            break;
        }
    }
}

当然,这样的简化是建立在模型和UI的属性名相同的情况下,在一些如上文提到的业务逻辑中可以使用,场景比较有限,但不失为一种方法。


不需要 @property 的 KVC

我们可以实现一个支持 KVC 而不用 @property 和 @synthesize 或是自动 synthesize 的属性。最直接的方式是添加 -<key>-set<Key>: 方法。例如我们想要 name ,我们这样做:

- (NSString *)name;
- (void)setName:(NSString *)name;

这完全等于 @property 的实现方式,当然类的内部需要实现上面两个方法。在外部就可以直接使用KVC进行存取:

[object setVelue:@"Jack" forKey:@"name"];
NSString *name = [object valueForKey:@"name"];


KVC的集合操作

先看一个示例代码,此代码获取集合中元素amount属性的平均值:

[self.transactions valueForKeyPath:@"@avg.amount"]

KVC支持一些集合操作,当我们给一个支持KVC的对象发送valueForKeyPath:方法时,可以在keyPath中包含一些集合操作符。集合操作符总是以@符号开头。

完整的带集合操作符的keyPath格式:

Operator key path format

Left key path 指定集合的路径,如果是直接给集合发送消息,这个路径可以省略(像上面的示例代码)。
Collection operator 集合运算符,如@avg。
Right key path 指定集合运算符应该操作的属性,所有的集合运算符都需要Right key path,除了@count。(如示例代码中的amount)


集合运算符类型:

  • 聚合运算符(Aggregation Operators),以某种方式合并集合的对象,并返回一个右键路径中命名的属性的数据类型匹配的对象。 @count运算符是一个异常 - 它没有正确的键路径,并且始终返回一个NSNumber实例。
  • Array运算符(Array Operators),符返回一个NSArray实例,包含原集合的一些子集。
  • 嵌套运算符(Nesting Operators),在包含其他集合的集合上工作,并返回一个NSArray或NSSet实例,具体取决于运算符,以某种方式组合嵌套集合的对象。

具体的文档可以参考官方文档: KeyValueCoding-CollectionOperators

示例:
有一个数组transactions里面包含的元素是Transaction模型:

@interface Transaction : NSObject
 
@property (nonatomic) NSString* payee;   // To whom
@property (nonatomic) NSNumber* amount;  // How much
@property (nonatomic) NSDate* date;      // When
 
@end

聚合运算符:

@avg
这个操作符会读取Right key path对应属性的值,转换成double类型(如果是nil转换成0),然后计算平均值,将结果以NSNumber形式展示。

NSNumber *transactionAverage = [self.transactions valueForKeyPath:@"@avg.amount"];



@count
这个操作符会返回集合的元素个数,如果指定了Right key path,则Right key path会被忽略。

NSNumber *numberOfTransactions = [self.transactions valueForKeyPath:@"@count"];



@max
这个操作符会对元素中Right key path指定的属性值进行最大值的查找,大小的对比会使用compare:方法,所以Right key path对应的属性类型必须能够相应这个方法才有意义,nil会被忽略。

NSDate *latestDate = [self.transactions valueForKeyPath:@"@max.date"];


@min
同@max,但查找最小值。

NSDate *earliestDate = [self.transactions valueForKeyPath:@"@min.date"];



@sum
这个操作会读取Right key path对应的值,转换为double(nil转换为0)进行累加,累加结果为NSNumber对象。

NSNumber *amountSum = [self.transactions valueForKeyPath:@"@sum.amount"];


数组运算符

使用数组运算符,当节点对象是nil的时候,valueForKeyPath:方法会抛出异常。


@distinctUnionOfObjects
返回数组,数组包含Right key path对应值(标量会被封装成对象,如int -> NSNumber),数组中的元素是唯一的(去重)。
因为数组中会包含Right key path对应的值,所以当它为nil时会抛出异常。

NSArray *distinctPayees = [self.transactions valueForKeyPath:@"@distinctUnionOfObjects.payee"];

上面代码返回的数组,包含self.transactions中每个元素的payee值,并且值是唯一的。

@unionOfObjects
返回数组,数组包含所有Right key path对应元素值(标量会被封装成对象,如int -> NSNumber)。与@distinctUnionOfObjects不同,这个操作不会去重。

NSArray *payees = [self.transactions valueForKeyPath:@"@unionOfObjects.payee"];

上面代码返回的数组,包含self.transactions中每个元素的payee值,值可能是重复的。

嵌套运算符

使用嵌套运算符,当节点对象是nil的时候,valueForKeyPath:方法会抛出异常。


示例数据:

NSArray* moreTransactions = @[<# transaction data #>]; // 元素为Transactionx实例
NSArray* arrayOfArrays = @[self.transactions, moreTransactions]; // 包含数组的数组

@distinctUnionOfArrays
返回数组,数组以keyPath对应的值为元素(标量会被封装成对象,如int -> NSNumber),且是唯一的。

NSArray *collectedDistinctPayees = [arrayOfArrays valueForKeyPath:@"@distinctUnionOfArrays.payee"];

上面代码返回数组,数组包含self.transactions和moreTransactions中所有元素(Transaction类型)的payee属性值,并且数组元素唯一。

@unionOfArrays
返回数组,数组以keyPath对应的值为元素 (标量会被封装成对象,如int -> NSNumber)。

NSArray *collectedPayees = [arrayOfArrays valueForKeyPath:@"@unionOfArrays.payee"];

上面代码返回数组,数组包含self.transactions和moreTransactions中所有元素(Transaction类型)的payee属性值,数组元素可能重复。

@distinctUnionOfSets
返回NSSet,包含keyPath对应的唯一值。
这个操作与@distinctUnionOfArrays类似,但不同的是,它操作的对象是NSSet of NSSet,而 @distinctUnionOfArrays操作的对象是NSArray of NSArray。



常见的 KVO 错误和调试

常见错误

获取、设置了不存在的key或keyPath,可以override下面方法

- (id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(id)value forUndefinedKey:(NSString *)key;

当然还有前面提到的setNilValueForKeyPath:方法。

调试 KVO

你可以在 lldb 里查看一个被观察对象的所有观察信息。

(lldb) po [observedObject observationInfo]

<NSKeyValueObservationInfo 0x60800003ed00> (
<NSKeyValueObservance 0x6080002435a0: Observer: 0x6000002414d0, Key path: window, Options: <New: YES, Old: NO, Prior: NO> Context: 0x0, Property: 0x6080002434b0>
)

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

这个信息的格式不是公开的,我们不能让任何东西依赖它,因为苹果随时都可以改变它。不过这是一个很强大的排错工具。


通过集合代理对象来实现 KVC

虽然我们无法像对待一般的对象一样用 KVC 深入集合内部(NSArray 和 NSSet 等),但是通过集合代理对象, KVC 也让我们实现一个兼容 KVC 的集合。这是一个颇为高端的技巧。

当我们在对象上调用 -valueForKey: 的时候,它可以返回 NSArray,NSSet 或是 NSOrderedSet 的集合代理对象。这个类没有实现通常的 -<Key> 方法,但是它实现了代理对象所需要使用的很多方法。

如果我们希望一个类支持通过代理对象的 contacts 键返回一个 NSArray,我们可以这样写:

- (NSUInteger)countOfContacts;
- (id)objectInContactsAtIndex:(NSUInteger)idx;

这样做的话,当我们调用 [object valueForKey:@"contacts”] 的时候,它会返回一个由这两个方法来代理所有调用方法的 NSArray 对象。这个数组支持所有正常的对 NSArray 的调用。换句话说,调用者并不知道返回的是一个真正的 NSArray, 还是一个代理的数组。

对于 NSSet 和 NSOrderedSet,如果要做同样的事情,我们需要实现的方法是:

NSArray NSSet NSOrderedSet
-countOf<Key> -countOf<Key> -countOf<Key>
-enumeratorOf<Key> -indexIn<Key>OfObject:
-memberOf<Key>:
以下两者二选一 以下两者二选一
-objectIn<Key>AtIndex: -objectIn<Key>AtIndex:
-<key>AtIndexes: -<key>AtIndexes:
- - -
可选(增强性能) 可选(增强性能)
-get<Key>:range: -get<Key>:range:

可选 的一些方法可以增强代理对象的性能。

虽然只有特殊情况下我们用这些代理对象才会有意义,但是在这些情况下代理对象非常的有用。想象一下我们有一个很大的数据结构,调用者不需要(一次性)访问所有的对象。

举一个(也许比较做作的)例子说,我们想写一个包含有很长一串质数的类。如下所示:

@interface Primes : NSObject

@property (readonly, nonatomic, strong) NSArray *primes;

@end



@implementation Primes

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]);
}

@end

我们将会运行以下代码:

Primes *primes = [[Primes alloc] init];
NSLog(@"The last prime is %@", [primes.primes lastObject]);

这将会调用一次 -countOfPrimes 和一次传入参数 idx 作为最后一个索引的 -objectInPrimesAtIndex:。为了只取出最后一个值,它不需要先把所有的数封装成 NSNumber 然后把它们都导入 NSArray。

可变的集合

我们也可以在可变集合(例如 NSMutableArray,NSMutableSet,和 NSMutableOrderedSet)中用集合代理。

访问这些可变的集合有一点点不同。调用者在这里需要调用以下其中一个方法:

- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;
- (NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key;

一个窍门:我们可以让一个类用以下方法返回可变集合的代理:

- (NSMutableArray *)mutableContacts;
{
    return [self mutableArrayValueForKey:@"wrappedContacts"];
}

一个常见的需求:观察可变集合,然后在观察回调中刷新列表。就是通过上面的方法返回集合代理,然后在回调中刷新UITableView。

完整的代理还需要实现键 wrappedContacts 的一些方法。我们需要实现上面的不变集合的两个方法,还有以下的几个:

NSMutableArray / NSMutableOrderedSet NSMutableSet
至少实现一个插入方法和一个删除方法 至少实现一个插入方法和一个删除方法
-insertObject:in<Key>AtIndex: -add<Key>Object:
-removeObjectFrom<Key>AtIndex: -remove<Key>Object:
-insert<Key>:atIndexes: -add<Key>:
-remove<Key>AtIndexes: -remove<Key>:
可选(增强性能)以下方法二选一 可选(增强性能)
-replaceObjectIn<Key>AtIndex:withObject: -intersect<Key>:
-replace<Key>AtIndexes:with<Key>: -set<Key>:

上面提到,这些可变集合代理对象和 KVO 结合起来也十分强大。KVO 机制能在这些集合改变的时候把详细的变化放进 change 字典中。

有批量更新(需要传入多个对象)的方法,也有只改变一个对象的方法。我们推荐选择相对于给定任务来说最容易实现的那个来写,虽然我们有一点点倾向于选择批量更新的那个。

在实现这些方法的时候,我们要对自动和手动的 KVO 之间的差别十分小心。Foundation 默认自动发出十分详尽的变化通知。如果我们要手动实现发送详细通知的话,我们得实现这些:

-willChange:valuesAtIndexes:forKey:
-didChange:valuesAtIndexes:forKey:

或者这些:

-willChangeValueForKey:withSetMutation:usingObjects:
-didChangeValueForKey:withSetMutation:usingObjects:

我们要保证先把自动通知关闭,否则每次改变 KVO 都会发出两次通知。




网上存在一些KVO集合的方法:
将集合封装在模型中

@interface ArrayModel : NSObject

@property (nonatomic, strong) NSMutableArray *datas;

@end

添加观察者

[self.arrModel addObserver:self forKeyPath:@"datas" options:NSKeyValueObservingOptionNew context:nil];

在改变集合元素时,使用

[[self.arrModel mutableArrayValueForKey:@"datas"] addObject:object]];
[[self.arrModel mutableArrayValueForKey:@"datas"] removeLastObject]];

以此取代原来使用的的[self.arrModel.datas addObject:object],并在回调中执行操作,如更新UI等。

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void *)context{
  // update UI
}

最后在恰当的时候移除观察者。



KVO实现原理

Apple 文档中关于KVO 实现的说明:

Automatic key-value observing is implemented using a technique called isa-swizzling.

The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table.
This dispatch table essentially contains pointers to the methods the class implements, among other data.

When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, 
pointing to an intermediate class rather than at the true class. 
As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.

You should never rely on the isa pointer to determine class membership.
Instead, you should use the class method to determine the class of an object instance.

里面说到,KVO使用了isa-swizzling技术,当一个对象的属性被注册了观察者时,这个对象的isa指针会被修改,指向一个内部类而不是真正的类对象上,因此这个时候,对象的isa指针并不真正反映它所属的类,开发者不应该依靠依靠isa指针来判断对象的类型,而应该使用class方法。

一个类的属性被注册了观察者时,会在运行时创建一个子类NSKVONotifying_MYClass,并重写对应属性的setter

// 原类
@Interface Sark : NSObject
@property (nonatomic, assign) NSUInteger age;
@end
// 当实例添加了age属性的观察者时
@Interface NSKVONotifying_Sark : Sark
@end

- (void)setAge:(NSUInteger)age {
    [self willChangeValueForKey:@"age"];
    [super setAge:age];
    [self didChangeValueForKey:@"age"];
}

当一个Sark实例的age属性被观察后,会创建出一个子类,重写set方法,并将Sark实例的isa指针指向新创建的子类。之后,如果age属性改变,就会触发KVO了。

此文章内容为学习objccn.io上关于KVO的期刊文章后,根据文章内容做的一些整理和略微的补充。
objccn.io: KVC 和 KVO
示例代码1:lab-color-space-explorer
示例代码2:contact-editor

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

推荐阅读更多精彩内容

  • KVC 什么是KVC? KVC(Key-value coding)是一种通过字符串去识别并间接存取(access)...
    4d1487047cf6阅读 445评论 0 1
  • KVC和KVO的学习 KVC介绍KVC的本质就是键值编码定义:在对象创建完成之后,动态(牵扯到运行时)给对象的属性...
    大白简先生阅读 266评论 0 1
  • KVC 综述 通常,我们使用“.语法”去给对象赋值,而KVC是使用字符串描述对象属性或属性路径从而实现赋值。NSO...
    六横六竖亚阅读 362评论 0 0
  • 在编程中,最常见的就是程序的流程取决于你所使用的各种变量和属性的值,根据变量和属性的值确定后面运行的代码,有时会检...
    pro648阅读 1,592评论 2 27
  • KVC/KVO 概念 KVC : 即 Key-Value-Coding,用于键值编码。作为 cocoa 的一个标准...
    满脸胡茬的小码农阅读 1,902评论 2 8