[iOS] KVC 和 KVO

开发iOS经常会看见KVO和KVC这两个概念,特地了解了一下。
我的新博客wossoneri.com link

KVC Key Value Coding

KVC是一种用间接方式访问类的属性的机制。比如你要给一个类中的属性赋值或者取值,可以直接通过类和点运算符实现,当然也可以使用KVC。不过对于私有属性,点运算符就不起作用,因为私有属性不暴露给调用者,不过使用KVC却依然可以实现对私有属性的读写。

先看一下KVC的一部分源码,当然只能看到头文件:

// NSKeyValueCoding.h

@interface NSObject(NSKeyValueCoding)

+ (BOOL)accessInstanceVariablesDirectly;

- (nullable id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;

- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;

- (NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key NS_AVAILABLE(10_7, 5_0);

- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;

- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKeyPath:(NSString *)inKeyPath error:(out NSError **)outError;
- (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath;
- (NSMutableOrderedSet *)mutableOrderedSetValueForKeyPath:(NSString *)keyPath NS_AVAILABLE(10_7, 5_0);
- (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath;

- (nullable id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
- (void)setNilValueForKey:(NSString *)key;
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;

@end


@interface NSArray<ObjectType>(NSKeyValueCoding)

- (id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKey:(NSString *)key;

@end


@interface NSDictionary<KeyType, ObjectType>(NSKeyValueCoding)

- (nullable ObjectType)valueForKey:(NSString *)key;

@end


@interface NSMutableDictionary<KeyType, ObjectType>(NSKeyValueCoding)

- (void)setValue:(nullable ObjectType)value forKey:(NSString *)key;

@end


@interface NSOrderedSet<ObjectType>(NSKeyValueCoding)

- (id)valueForKey:(NSString *)key NS_AVAILABLE(10_7, 5_0);
- (void)setValue:(nullable id)value forKey:(NSString *)key NS_AVAILABLE(10_7, 5_0);

@end


@interface NSSet<ObjectType>(NSKeyValueCoding)

- (id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKey:(NSString *)key;

@end

可以看到这个类里面包含了对类NSObject,NSArray,NSDictionary,NSMutableDictionary,NSOrderedSet,NSSet的拓展。拓展的方法基本上为

- (id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKey:(NSString *)key;

也就是说,基本上Objective-C里所有的对象都支持KVC操作,操作包含如上两类方法,动态读取和动态设值。

好多地方说是NSObject实现了NSKeyValueCoding协议。而代码里是类的拓展。这两种说法是相通的嘛?

举个🌰,新建一个Command line程序:

// Account.h
@interface Account : NSObject

@property (nonatomic, assign) float balance;

@end

// Account.m
@implementation Account {
    float salaryPerDay;
}

@synthesize balance = _balance;

- (void)setBalance:(float)balance {
    NSLog(@"set balance invoked");
    _balance = balance;
}

- (float)balance {
    NSLog(@"get balance invoked");
    return _balance;
}

@end

// Person.h
@class Account;

@interface Person : NSObject {
    @private
    int _age;
}

@property (nonatomic, copy) NSString *name;
@property (nonatomic, retain) Account *account;

- (void)showMessage;

@end

// Person.m
#import "Person.h"

@implementation Person {
    NSString *_sex;
}

- (void)showMessage {
    NSLog(@"name = %@, age = %d, sex = %@", _name, _age, _sex);
}

@end

// main
int main(int argc, const char * argv[]) {
    @autoreleasepool {
      
        Person *person1 = [[Person alloc] init];
        [person1 setValue:@"Wossoneri" forKey:@"name"];
        [person1 setValue:@25 forKey:@"age"];       //私有变量也可以访问
        [person1 setValue:@"male" forKey:@"sex"];   //私有变量也可以访问
        
        [person1 showMessage];
        
        Account *account1 = [[Account alloc] init];
        person1.account = account1;
        
        [person1 setValue:@1000.0 forKeyPath:@"account.balance"];
        [person1 setValue:@300.0 forKeyPath:@"account.salaryPerDay"];

        NSLog(@"Person1`s balance is : %.2f", [[person1 valueForKeyPath:@"account.balance"] floatValue]);
        NSLog(@"Person1`s salary is : %.2f", [[person1 valueForKeyPath:@"account.salaryPerDay"] floatValue]);

    }
    return 0;
}

// 输出
name = Wossoneri, age = 25, sex = male
set balance invoked
get balance invoked
Person1`s balance is : 1000.00
Person1`s salary is : 300.00

代码说明:

  • Person类里用旧方法声明私有变量_age以及直接添加的私有成员变量_sex,同时声明一个开放的属性_name
  • 对于_name,O-C会直接为其生成对应的settergetter,所以可以通过点运算符操作属性,比如
person1.name = @"Wossoneri";
  • 可以看到KVC可以对私有变量进行操作。对于当前类的直接成员变量,把变量名作为key来访问,否则要写成keyPath来访问。
  • KVC运行时首先会优先调用属性的gettersetter,这一点可以在代码输出的第二行和第三行看到,如果没有,就会优先搜索_property,不存在则搜索property,如果仍然没有,就会调用setValue:forUndefinedKey:valueForUndefinedKey:方法

KVO Key Value Observing

KVO其实是一种观察者模式,利用它可以很容易实现视图组件和数据模型的分离,当数据模型的属性值改变之后作为监听器的视图组件就会被激发,激发时就会回调监听器自身。

放一部分NSKeyValueObserving.h对于NSObject的拓展代码

@interface NSObject(NSKeyValueObserving)

- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSString*, id> *)change context:(nullable void *)context;

@end

@interface NSObject(NSKeyValueObserverRegistration)

- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context NS_AVAILABLE(10_7, 5_0);
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

@end

从拓展名称就可以看出,使用KVO需要注册监听器,也需要删除监听器。监听过程需要使用observeValueForKeyPath回调方法。
所以使用方法就可以推测出个大概来:

  1. addObserver方法注册一个监听器
  2. 复写observeValueForKeyPath回调,获得监听到的信息,做对应操作。
  3. 使用结束removeObserver,这很重要。

然后对上面代码做一些改动,我需要对Account对象的balance做监听,当balance内容改变,我要做输出处理。

#pragma mark - For KVO
- (void)setAccount:(Account *)account {
    _account = account;
    //add observer
    [_account addObserver:self
               forKeyPath:@"balance"
                  options:NSKeyValueObservingOptionNew
                  context:nil];
}

//override
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSString *,id> *)change
                       context:(void *)context {
    
    if ([keyPath isEqualToString:@"balance"]) {
        NSLog(@"keyPath = %@, object = %@, newValue = %.2f, context = %@", keyPath, object, [[change objectForKey:@"new"] floatValue], context);
    }
}

- (void)dealloc {
    [_account removeObserver:self forKeyPath:@"balance"];
}

// 输出
name = Wossoneri, age = 25, sex = male
set balance invoked
get balance invoked
keyPath = balance, object = <Account: 0x1003001d0>, newValue = 1000.00, context = (null)
set balance invoked
get balance invoked
keyPath = balance, object = <Account: 0x1003001d0>, newValue = 4000.00, context = (null)
get balance invoked
Person1`s balance is : 4000.00
Person1`s salary is : 300.00

Swift的KVO与KVC

Swift版本的的就看这篇文章吧,内容很详细。
漫谈 KVC 与 KVO

思考

学一个东西,要想有收获,少不了的就是思考。

对于KVC来说,使用的优势我认为是在于可以读写私有成员变量,比如一些特殊情况下需要改变私有变量,而大多数情况不需要,这时候就没必要把私有变量开放出来了,用KVC就可以。(当然开放接口也行了)

对于KVO,我觉得其运行原理就是一回调了。这个声明一个协议或者写一个block应该也是可以实现相同功能的。所以什么时候使用它,需要在实战中去想想才行。

这个知识点我没怎么用过,但我会在以后的编程实践中思考这方面的内容,考虑在不同情形下怎么才能玩好它。有收获了再回来做补充。您若有相关经验,也希望能和我分享一下。

参考
iOS开发系列--Objective-C之KVC、KVO

posted @ 2016-04-30 22:05  Wossoneri  阅读(562)  评论(0编辑  收藏  举报