iOS-KVC/KVO学习

KVC提供了一种间接存取对象属性的方法。而且与KVC有关的方法都是在NSObject中定义的,所以只要是继承自NSObject的子类都具有KVC功能。KVO则提供了一种观察指定对象的属性的方法,当观察的对象的属性变化时,观察者会收到通知。

Key-Value Coding(KVC)

KVC即指的NSKeyValueCoding,一个非正式的Protocol。它的最强大的功能则是自由存取对象的属性值,即使该属性值对外是不可见的(即那些没在.h中声明的属性或没提供getter/setter的私有属性)。并且KVO也是基于KVC实现的关键技术之一,还有Cocoa框架,Core data中也都有KVC的应用。

一个对象可能拥有很多属性,并且这些属性有些可能是外部可以直接访问的,或者是私有的属性。例如我们这里有一个LYSPerson对象包括name,age两个外部可见的属性和一个spouse在扩展中定义的私有属性。按照KVC的说法,LYSPerson对象分别有一个value值对应name,age和spouse的key。key只是一个字符串,它对应的值可以是任意对象,但必须是对象(所以属性为基本类型的需要NSNumber等进行封装)。
KVC提供的最基础功能则是设置一个对象的属性和获取一个对象的属性值两种。我们可以参考以下例子。

//LYSPerson.h
#import <Foundation/Foundation.h>

@interface LYSPerson : NSObject

//姓名
@property (nonatomic, copy) NSString *name;
//年龄
@property (nonatomic, assign) int age;

@end

//LYSPerson.m
#import "LYSPerson.h"

@interface LYSPerson ()

//配偶,为了避免强引用循环,将其设置为weak弱引用
@property (nonatomic, weak) LYSPerson *spouse;

@end

@implementation LYSPerson

//只是为了确定他们都正常释放了而已233333
- (void)dealloc
{
    NSLog(@"Died together.23333333.");
}

@end

存取属性值

LYSPerson *person = [LYSPerson new];
//通过属性自动生成的getter/setter来操作
person.name = @"Lin";
person.age = 16;

NSLog(@"%@ is %d years old.", person.name, person.age);

//通过KVC机制设置和获取对象的属性
//需要注意的是,KVC机制设置属性的值必须是一个对象
//所以简单类型的数据需要用NSNumber,NSValue等进行封装
[person setValue:@"LinYouSong" forKey:@"name"];
[person setValue:@18 forKey:@"age"];

NSLog(@"After KVC./n %@ is %@ years old./n",
      [person valueForKey:@"name"],
      [person valueForKey:@"age"]);

为了做对比我们还加入了属性自动生成的setter/getter方法的使用,我们可以看到对外部可见的属性,使用KVC可以达到setter/getter方法的效果。输出结果如下:

2016-02-28 15:32:29.641 KVCAndKVODemon[7952:925826] Lin is 16 years old.
2016-02-28 15:32:29.642 KVCAndKVODemon[7952:925826] After KVC.LinYouSong is 18 years old.

键值链(KeyPath)

KVC中还有一个特点就是可以通过键值链的方式存取对象的属性。比如说一个LYSPerson对象有一个属性spouse,其也是LYSPerson对象。所以通过spouse.name,spouse.age访问属性的属性(哈哈,有点神奇!)。用代码的写法就是:

[person valueForKeyPath:@"spouse.age"]
等价于 [[person valueForKey:@"spouse"] valueForKey:@"age"]

然后我们把刚才的例子扩展一下。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LYSPerson *person = [LYSPerson new];
        //通过属性自动生成的getter/setter来操作
        person.name = @"Lin";
        person.age = 16;
        //因为spouse属性是在扩展中定义的,对外不可见,这里访问不到该属性
        //person.spouse 不可访问

        NSLog(@"%@ is %d years old.", person.name, person.age);

        //通过KVC机制设置和获取对象的属性
        //需要注意的是,KVC机制设置属性的值必须是一个对象
        //所以简单类型的数据需要用NSNumber,NSValue等进行封装
        [person setValue:@"LinYouSong" forKey:@"name"];
        [person setValue:@18 forKey:@"age"];
        //虽然spouse属性是不可见的,但是KVC可以做到对私有属性的访问
        LYSPerson *wife = [LYSPerson new];
        wife.name = @"Beautiful Wife.";
        wife.age = 16;

        [person setValue:wife forKey:@"spouse"];
        [wife setValue:person forKey:@"spouse"];

        NSLog(@"After KVC.%@ is %@ years old.\n",
              [person valueForKey:@"name"],
              [person valueForKey:@"age"]);

        NSLog(@"And he's wife is a %@ years old girl, named %@.",
              [person valueForKeyPath:@"spouse.age"],
              [person valueForKeyPath:@"spouse.name"]);
    }
    return 0;
}

然后我们可以看到输出结果。这里我们可以看到KVC的强大之处了!它可以随心所欲设置属性的值!例子里还有个小注意点,spouse属性我们用了weak为了避免强引用循环,如果我们把其改成strong就会发现最后的两句输出没有了。(哈哈哈,双方互相包容才能一起到死啊,两个都强势势必纠缠不休,死不瞑目啊哈哈哈哈哈哈~~~~)

2016-02-28 15:40:06.030 KVCAndKVODemon[7988:930695] Lin is 16 years old.
2016-02-28 15:40:06.031 KVCAndKVODemon[7988:930695] After KVC.LinYouSong is 18 years old.
2016-02-28 15:40:06.031 KVCAndKVODemon[7988:930695] And he's wife is a 16 years old girl, named Beautiful Wife..
2016-02-28 15:40:06.031 KVCAndKVODemon[7988:930695] Died together.23333333.
2016-02-28 15:40:06.031 KVCAndKVODemon[7988:930695] Died together.23333333.

Key-Value Observing(KVO)

KVO是指当指定的对象的属性被修改时,允许对象接受通知的机制。我们可以想想这样一个场景,我们告诉LYSPerson对象,“我想要观察你的name属性,如果它发生变化,就通知我。”然后当name属性被修改时,LYSPerson对象就会发消息给我说:“你好,我的name属性有一个新值了。”然后我就可以对这个事件进行处理。
所以实现KVO的步骤很简单:

  1. 给对象添加观察者,指定观察的属性。
  2. 实现接收到值更新时的消息处理。
  3. 停止观察。

现在我们来看部分重点代码,完整代码可以参考源代码工程。

给对象添加观察者,指定观察的属性。这里该进行了属性的修改。

-(void)testKVO
{
    LYSPerson *person;

    for (int i = 12; i <= 20; i++) {
        person = [[LYSPerson alloc] init];
        person.age = i;
        person.name = [NSString stringWithFormat:@"KVOPerson%d", i];

        //设置观察age属性,并指定观察新值和旧值
        [person addObserver:self
                 forKeyPath:@"age"
                    options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                    context:CONTEXT_KVO_LYSPERSON_AGE_CHANGE];
        //保留被观察的对象
        [self.observedPerson addObject:person];
    }

    //然后我们修改LYSPerson对象的age属性值
    for (LYSPerson *person in self.observedPerson) {
        person.age += 1;
    }
}

实现接收到值更新时的消息处理。

/**
 *  重写该方法,处理指定观察值发生变化时的事件
 *
 *  @param keyPath 观察的key
 *  @param object  属性发生变化的对象
 *  @param change  发生的变化,新值旧值等信息都保存在字典中,根据键值取出需要的信息
 *  @param context context 描述符
 */
-(void)observeValueForKeyPath:(NSString *)keyPath
                     ofObject:(id)object
                       change:(NSDictionary<NSString *,id> *)change
                      context:(void *)context
{
    if (context == CONTEXT_KVO_LYSPERSON_AGE_CHANGE) {
        NSString *name = [object valueForKey:@"name"];
        //NSNumber *newAge = [object valueForKey:@"age"];
        NSNumber *newAge = [change objectForKey:NSKeyValueChangeNewKey];
        NSNumber *oldAge = [change objectForKey:NSKeyValueChangeOldKey];
        NSLog(@"Person named %@ has change his age from %@ to %@.", name, oldAge, newAge);
    }
}

停止观察

-(void)dealloc
{
    //移除所有观察事件
    for (LYSPerson *person in self.observedPerson) {
        [person removeObserver:self forKeyPath:@"age"];
    }
    //移除所有对象
    [self.observedPerson removeAllObjects];
}

然后我们来看输出结果。每个person对象的age属性被修改时都被通知了,并且在通知中可以获取各种需要的属性。

2016-02-28 16:37:06.042 KVCAndKVODemon[8166:958839] Person named KVOPerson12 has change his age from 12 to 13.
2016-02-28 16:37:06.043 KVCAndKVODemon[8166:958839] Person named KVOPerson13 has change his age from 13 to 14.
2016-02-28 16:37:06.043 KVCAndKVODemon[8166:958839] Person named KVOPerson14 has change his age from 14 to 15.
2016-02-28 16:37:06.043 KVCAndKVODemon[8166:958839] Person named KVOPerson15 has change his age from 15 to 16.
2016-02-28 16:37:06.043 KVCAndKVODemon[8166:958839] Person named KVOPerson16 has change his age from 16 to 17.
2016-02-28 16:37:06.043 KVCAndKVODemon[8166:958839] Person named KVOPerson17 has change his age from 17 to 18.
2016-02-28 16:37:06.043 KVCAndKVODemon[8166:958839] Person named KVOPerson18 has change his age from 18 to 19.
2016-02-28 16:37:06.043 KVCAndKVODemon[8166:958839] Person named KVOPerson19 has change his age from 19 to 20.
2016-02-28 16:37:06.043 KVCAndKVODemon[8166:958839] Person named KVOPerson20 has change his age from 20 to 21.

源代码

Github-iOS-Demon-KVCAndKVODemon

参考资料

  1. KVC 与 KVO 理解
  2. iOS:KVO/KVC 的概述与使用

PS:更高级参考资料

  1. KVO/KVC 实现机理分析