首页 > 代码库 > object-c编程tips-kvokvc浅析

object-c编程tips-kvokvc浅析

KVC键值编码

一,概述

KVO,即:Key-Value Observing,它提供一种机制,当指定的对象的属性被修改后,则对象就会接受到通知。它在控制层和模型层之间的消息沟通中发挥很大作用。controller层的对象观察model层对象的属性,从而使得view对象可以通过controller层间接观察model的属性,解耦model和view。

例如demo工程IOSKvoUse

初始化:model对象将当前的viewController作为自己score属性的观察者。

1,在viewController中

[m_studentKVO addObserver:selfforKeyPath:@"m_stuScore"options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOldcontext:nil];

2,用户点击界面的button按钮,会使得model数据改变。

[m_studentKVO setValue:[NSStringstringWithFormat:@"%d",score]forKey:@"m_stuScore"];

3,   由于viewController注册成为观察者,故而观察者的observeValueForKeyPath会被调用,通知viewControllermodel数据被更新了。

4,   在观察者的回调方法中,可以刷新相应的界面。

if([keyPath isEqualToString:@"m_stuScore"])
{
        m_lable.text = [NSStringstringWithFormat:@"score:%@", [m_studentKVOvalueForKey:@"m_stuScore"]];    
}

二,KVO图解

1.    需要确定是否有KVO的场景,即是否有一个对象需要通知它的属性的改变到另一个对象。

PersonObject 需要知晓BankObject的属性accountBalance 的任何改变 .

2.     PersonObject 必须将自己注册成为 BankObject的 accountBalance 属性的观察者。通过发送addObserver:forKeyPath:options:context:消息.

3.     观察者通过实现 observeValueForKeyPath:ofObject:change:context: 方法对属性改变通知进行回应. 在这个方法中进行你的通知回应的个性化操作。如果父类实现了此方法,那么请注意在方法的最后调用父类的方法。

4.     如果你遵循KVO编程逻辑,那么   observeValueForKeyPath:ofObject:change:context: 方法会被自动调用。

三,使用方法

系统框架已经支持KVO,使用异常简单。

l   注册观察者:

- (void)registerAsObserver {
    /*
     Register 'inspector' to receive change notifications for the "openingBalance" property of
     the 'account' object and specify that both the old and new values of "openingBalance"
     should be provided in the observe… method.
     */
    [account addObserver:inspector
             forKeyPath:@"openingBalance"
                 options:(NSKeyValueObservingOptionNew |
                            NSKeyValueObservingOptionOld)
                    context:NULL];
}
接收通知 addObserver:forKeyPath:options:context:方法不会对观察者,被观察者,以及context进行任何的强引用,也就是说它们的生命周期需要你自己保证。

返回一个change词典:

1,          NSKeyValueChangeKindKey

普通属性改变:NSKeyValueChangeKindKey 返回NSKeyValueChangeSetting

集合对象改变: NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, 或者 NSKeyValueChangeReplacement

 

2,          NSKeyValueChangeOldKey 

返回对象属性改变前的值

3,          NSKeyValueChangeNewKey 

返回属性改变后的值

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
 
    if ([keyPath isEqual:@"openingBalance"]) {
        [openingBalanceInspectorField setObjectValue:
            [change objectForKey:NSKeyValueChangeNewKey]];
    }
    /*
    如果父类实现了此方法,不要忘记调用。
     NSObject 没有实现此方法.
     */
    [super observeValueForKeyPath:keyPath
                         ofObject:object
                           change:change
                           context:context];
}

删除观察者

- (void)unregisterForChangeNotification {
    [observedObject removeObserver:inspector forKeyPath:@"openingBalance"];
}


四,遵循KVO

类的属性必须是遵循KVC的。使用KVC的方法修改了属性并触发通知。观察者注册了自身并实现了观察者方法。

有两种方式实现KVO,一种是自动通知方式,由NSObject支持,对所有遵循KVC的类的属性有效。一种是手动实现的,手动实现意味着更多的可控性,但也意味着更多的编码。

1,自动通知

例如demo工程KVOSalary

你可以发出自动通知通过以下几个方式:遵循KVC的setter方法,KVC方法,集合代理。

使用setter方法
[account setName:@"Savings"];

// 使用 setValue:forKey:.
[account setValue:@"Savings" forKey:@"name"];

// 使用keypath, where 'account' is a kvc-compliant property of 'document'.
[document setValue:@"Savings" forKeyPath:@"account.name"];

// 使用 mutableArrayValueForKey:返回集合代理.
Transaction *newTransaction = [[Transaction alloc]init];
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];

2,手动通知

例如demo工程KVOSalaryManual

手动方式对如何和何时发送通知提供了更细粒度的控制。它可以用来减少不必要的通知,以及对多个属性发送一个通知。

通过覆盖NSObject的类方法 automaticallyNotifiesObserversForKey:实现手动通知,对需要进行手动通知的属性此方法需要返回NO,同时需要注意对于其他属性要调用父类的方法。在一个类中可以混合使用自动和手动通知。

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
 
    BOOL automatic = NO;
    if ([theKey isEqualToString:@"openingBalance"]) {
        automatic = NO;
    }
    else {
        automatic = [super automaticallyNotifiesObserversForKey:theKey];
    }
    return automatic;
}

手动通知的优点及要注意的地方

  •  对于不需要发送的通知可以不再发送。也就是说在发送通知前对传入进来的参数进行测试。

    if(_accountMoney !=accountMoney)
    {
        [self willChangeValueForKey:@"accountMoney"];
        _accountMoney = accountMoney;
        [self didChangeValueForKey:@"accountMoney"];
    }

  • 一个改变需要对应多个key。在set方法中加入多个key的改变。

    [self willChangeValueForKey:@"accountMoney"];
    [self willChangeValueForKey:@"itemChanged"];
    _accountMoney = accountMoney;
    _itemChanged++;
    [self didChangeValueForKey:@"accountMoney"];
    [self didChangeValueForKey:@"itemChanged"];


  • 集合类需要注意:不仅需要提供改变的类型,还需要提供要改变的范围。

    NSUInteger count = _transationsArray.count;
    NSIndexSet *set = [NSIndexSetindexSetWithIndexesInRange:NSMakeRange(0, count)];
 
    [self willChange:NSKeyValueChangeRemoval
     valuesAtIndexes:set forKey:@"transationsArray"];
    [_transationsArray removeObjectsAtIndexes:set];
    [self didChange:NSKeyValueChangeRemoval valuesAtIndexes:set forKey:@"transationsArray"];

五,key之间的依赖观察。

例如demo工程KVODependentKey

例如用户有一个fullName属性,此属性被其他观察者观察。它需要依赖firstName和lastName属性。也就是说当用户设置firstName或者lastName的时候,它的fullName观察者需要得到通知。

 

这从一个方面与上面讲述的itemChanged和accountMoney有相似之处。那个里面使用了nest通知机制。通过在定制的方法中同时调用两个didChange方法。

代码如下:

-(void)setAccountMoneyNestChangeNotification:(int)accountMoney
{
    [self willChangeValueForKey:@"accountMoney"];
    [self willChangeValueForKey:@"itemChanged"];
 
    _accountMoney =accountMoney;
    _itemChanged++;
 
    [self didChangeValueForKey:@"accountMoney"];
    [self didChangeValueForKey:@"itemChanged"];
}

它通过封装了一层方法实现了一个属性的改变引起多个属性的修改,强调的是一改则多改。这里强调的是多个属性的修改都会影响某一个属性,多改对一改,这里采用的一种依赖机制,fullName属性依赖多个key,用户只需要告诉KVO框架是如何依赖的即可。

 

如何建立依赖关系?

通过keyPathsForValuesAffectingValueForKey方法返回指定key所对应的依赖set集合,框架会自动建立fullName和另外两个属性的依赖。
+(NSSet*)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
    NSSet *keyPaths = [superkeyPathsForValuesAffectingValueForKey:key];
 
    if ([key isEqualToString:@"fullName"]) {
        NSArray *affectingKeys =@[@"lastName",@"firstName"];
        keyPaths =[keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
 
    return keyPaths;
}

六,观察者模式

1,概念

例如demo工程ObserverDesignPattern

观察者模式定义了对象之间的一对多依赖关系,当一个对象的状态发生改变的时候,所有依赖他的对象都得到通知并被自动更新。以QQ举例,你和你的qq好友时一族相互的依赖对象,当你的好友上线了,你可以看到你的好友上线的状态,当他离线了,你可以看到他的离线信息,当他更改了名字,你也可以看到他更改了名字。针对这种情况,你自身就是一个观察者,而你的好友是被观察的对象,一般称之为目标对象。你可能会问你你上线下线,你的好友也会知道呀,对的,这种情况下你自身就是一个目标对象,而你的好友就转换成了观察者了。也就是说观察者和目标对象只是在一种通知场景中的依赖关系的命名逻辑,观察者同时也可以是目标对象。下面的代码就是以这种策略思想实现的。

2,两个类

Observer观察者,或者称之为订阅者,当它观察的对象发出状态改变时候,它自己的状态改变方法会收到通知。

Subject目标:它是被观察的对象,有自己状态改变方法和的通知方法。

关于实现?

1,           基类目标对象封装了添加,删除,通知观察者的方法,派生类目标对象负责要观察的具体属性操作,并在需要通知的地方调用notifyAllObservers。

2,           基类观察者对象声明了函数update方法,它可以提供默认实现。像苹果的是observeValueForKeyPath是抛出异常。这是一个好的编程实践,因为它可以使错误更早的暴露出来。最好不要提供一个空的实现,这样可能由于编程者的错误而没有实现observeValueForKeyPath,但是编译器并不会将这种错误表现出来。

3,           常用的类框架图

 

4,           在哪里添加和删除观察者?

添加和删除观察者的操作最好能够封装到目标或者观察者类体内,而不要如demo例子中所使用的使用一个driver进行操作(我主要意图是希望使类结构清晰点)。一般情况下可在init中加入,在dealloc中删除。这样使用者不会因为编程的疏忽导致忘记最后的删除操作。

七,KVO的实现

例如demo工程KVOPrinciple

KVO是通过叫isa-swizzling技术实现的,基本的流程就是编译器自动为被观察对象创造一个派生类,并将被观察对象的isa指向这个派生类。如果用户注册了对某此目标对象的某一个属性的观察,那么此派生类会重写这个方法,并在其中添加进行通知的代码。这个需要OC运行时候的流程有一个基本了解。OC在发送消息的时候,会通过isa指针找到当前对象所属的类对象,而类对象中保存着当前对象的实例方法,因此在向此对象发送消息时候,实际上是发送到了派生类对象的方法,又由于编译器对派生类的方法进行了override,并添加了通知代码,因此会向注册的对象发送通知。注意派生类只重写注册了观察者的属性方法。

 

举例:

有一个Person类,它有三个属性firstName, lastName, personId,它是被观察对象。有一个PersonObserver类,它观察firstName和lastName,不观察personId。

理论来说,派生类会覆盖基类的setFirstName和setLastName方法,不覆盖setPersonId方法。通过运行时函数object_getClass(p)访问此person对象,会得到一个名字为NSKVONotifying_Person的类对象,如果直接通过[p class]方法访问,那么会看到它依然为person对象,这是因为派生类同样重写了class函数,当向p发送class消息时候,同样是找到了派生类的class方法。

通过分别打印不同类对象的函数表进行测试。对于派生类和基类取得其setFirstName的IMP实现,然后进行直接调用比较。现在对于基类使用IMP访问会崩溃,暂未知原因。对于派生类使用其IMP实现,确实可以正常发送通知,这表明确实是使用的派生类的实现。


八,总结

1,简介

键值编码是使用String标识符间接获取对象属性的机制。它是Cocoa编程的基础,包括Cocoa Data,应用脚本化,绑定技术,属性声明。脚本化和绑定技术是专门为OSX的Cocoa而用,你可以使用键值编码去简化编程代码,Jastor就使用统一的setValueForKey,以及valueForKey。

2,访问对象属性和KVC

2.1,KVC常用以下四个方法:

-(id)valueForKey:(NSString *)key;      

-(void)setValue:(id)value forKey:(NSString *)key;      - -(id)valueForKeyPath:(NSString*)keyPath;     

- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;

KVC通过字符串的key或者keypath定位对象的属性。Key有命名管理,以ASCII编码,小写字母开头,无空格,它一般就是属性的名字。KeyPath是一个用点分割的字符串用来逐层遍历最深层的属性值。

如上例子:

       [employee1setValue:@”lpstudy” forKey:@”name”];

       [employee1setValue:@[employee1, employee2, employee3]forKeyPath:@”manager.directerPorts”];

2.2,使用访问器方法

       如果有,访问器方法依然是有效的。如果没有相应的访问器方法,可以使用上面的setValue和valueForKey直接读取对象的属性值。

2.3,具有KVC能力

如何使一个类具有KVC功能,必须满足下列的其中一个:

l   类使用key声明了属性

@property (nonatomic ,readonly,strong)NSString *personName;

l   它实现了key名字的get方法和set方法。如果返回bool类型,isKey方法。

l   声明了一个实例变量,key或者_key

需要注意

l   valueForKey和valueForKeyPath 返回指定key,keyPath的value值。如果不存在此key,会调用valueForUndefinedKey:。它的默认实现是抛出NSUndefinedKeyException;子类可以复写这种行为。

l   尝试给一个非对象类型设置nil。对象默认调用setNilValueForKey此方法默认抛出NSInvalidArgumentException.你的程序可以覆盖这个方法给一个默认值,然后 setValue:forKey: 设置新值。

l   鉴权机制validatePersonName

2.4,搜索方式

以下选自于网络,我并没有进行相关的测试。

setValue:forKey的搜索方式:

1,首先搜索set<Key>:方法。如果成员用@property,@synthsize处理,因为@synthsize告诉编译器自动生成set<Key>:格式的setter方法,所以这种情况下会直接搜索到。

2,上面的setter方法没有找到,如果类方法accessInstanceVariablesDirectly返回YES(注:这是NSKeyValueCodingCatogery中实现的类方法,默认实现为返回YES),则按_<key>,_is<Key>,<key>,is<key>的顺序搜索成员的名值。

3,如果找到则设置成员的值,没有找到则调用setValue:forUndefinedKey:。

valueForKey的搜索方式

1.,按get<Key>、<key>、is<Key>的顺序查找getter方法,找到直接调用。如果是bool、int等内建值类型,会做NSNumber的转换,如果是其他类似于struct结构体类型则会进行NSValue的转换。

2,否则查找countOf<Key>、objectIn<Key>AtIndex:、<Key>AtIndexes格式的方法。 如果countOf<Key>和另外两个方法中的一个找到,那么就会返回一个可以响应NSArray所有方法的代理集合(collection proxy object)。发送给这个代理集合(collection proxy object)的NSArray消息方法,就会以countOf<Key>、objectIn<Key>AtIndex:、<Key>AtIndexes这几个方法组合的形式调用。还有一个可选的get<Key>:range:方法。

3,还没找到则继续查找countOf<Key>、enumeratorOf<Key>、memberOf<Key>:格式的方法。 如果这三个方法都找到,那么就返回一个可以响应NSSet所有方法的代理集合(collection proxy object)。发送给这个代理集合(collection proxy object)的NSSet消息方法,就会以countOf<Key>、enumeratorOf<Key>、memberOf<Key>:组合的形式调用。

4, 还是没查到,那么如果类方法accessInstanceVariablesDirectly返回YES,那么按_<key>,_is<Key>,<key>,is<key>的顺序直接搜索成员名。如果实例变量被找到,那么它的值会被以对象的形式返回(数值类型进行NSNumber的封装,其他的进行NSValue的封装)。

5. 再没查到,调用valueForUndefinedKey:。

 

查找有序集合成员,比如NSMutableArray

mutableArrayValueForKey:搜索方式如下:

1,搜索insertObject:in<Key>AtIndex:、removeObjectFrom<Key>AtIndex:或者insert<Key>:atIndexes、remove<Key>AtIndexes:格式的方法。

如果至少一个insert方法和至少一个remove方法找到,那么同样返回一个可以响应NSMutableArray所有方法的代理集合。那么发送给这个代理集合的NSMutableArray消息方法,以insertObject:in<Key>AtIndex:、removeObjectFrom<Key>AtIndex:、insert<Key>:atIndexes、remove<Key>AtIndexes:组合的形式调用。还有两个可选实现的接口:replaceObjectIn<Key>AtIndex:withObject:、replace<Key>AtIndexes:with<Key>:。

2,否则,搜索set<Key>:格式的方法,如果找到,那么发送给代理集合的NSMutableArray最终都会调用set<Key>:方法。

也就是说,mutableArrayValueForKey取出的代理集合修改后,用set<Key>:重新赋值回去。这样做效率会差很多,所以推荐实现上面的方法。

3,否则,那么如果类方法accessInstanceVariablesDirectly返回YES,那么按_<key>,<key>的顺序直接搜索成员名。如果找到,那么发送的NSMutableArray消息方法直接转交给这个成员处理。

4. 再找不到,调用setValue:forUndefinedKey:。

搜索无序集合成员,如:NSSet。

mutableSetValueForKey:搜索方式如下:

1. 搜索add<Key>Object:、remove<Key>Object:或者add<Key>:、remove<Key>:格式的方法,如果至少一个insert方法和至少一个remove方法找到,那么返回一个可以响应NSMutableSet所有方法的代理集合。那么发送给这个代理集合的NSMutableSet消息方法,以add<Key>Object:、remove<Key>Object:、add<Key>:、remove<Key>:组合的形式调用。还有两个可选实现的接口:intersect<Key>、set<Key>:。

2. 如果reciever是ManagedObejct,那么就不会继续搜索了。

3. 否则,搜索set<Key>:格式的方法,如果找到,那么发送给代理集合的NSMutableSet最终都会调用set<Key>:方法。也就是说,mutableSetValueForKey取出的代理集合修改后,用set<Key>:重新赋值回去。这样做效率会差很多,所以推荐实现上面的方法。

4. 否则,那么如果类方法accessInstanceVariablesDirectly返回YES,那么按_<key>,<key>的顺序直接搜索成员名。如果找到,那么发送的NSMutableSet消息方法直接转交给这个成员处理。

5. 再找不到,调用setValue:forUndefinedKey:。