KVO、KVC的探究

来源:互联网 发布:聚划算计入淘宝搜索吗 编辑:程序博客网 时间:2024/05/21 11:36

前言

KVO(Key Value Observing)即键值监听,是一种观察者模式,当所观察的属性值被修改时,能通知充当观察者的对象的一种机制。常用方法如下:

// 对属性添加观察者- (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;// 属性值改变时回调的方法,由充当观察者的对象(通常为视图控制器)实现- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSString*, id> *)change context:(nullable void *)context;

使用KVO时一定要在收到改变的信息时有相关的处理的方法,如果没有相应的处理,运行时会崩溃(crash):message was received but not handled。

对于导航栏(在iOS7后,状态栏和导航栏合二为一,导航栏高度变为20+44=64。状态栏默认为透明,状态栏的背景色就取决于导航栏的背景色。)的渐变效果(如QQ中的好友动态界面),下拉刷新等效果可以使用KVO监听UISCrollView的contentOffset属性来实现。

注意 :在对象的属性被观察者监听的时候,被观察的对象一定不能够释放,也就是说:被观察的对象必须在移除观察者之后才能被释放。否则运行时会崩溃:XX was deallocated while key value observers were still registered with it.还有就是对于属性_key和key通过KVC取值和赋值是没有什么区别的,但是监听_key和key却是两种不同的行为,笔者猜想是因为重写的setter方法不同,也就意味着最后在被监听的属性值改变时所通知的对象也是不同的。


KVC(Key Value Code)即键值编码,是一种通过使用字符串标识符直接访问对象的(私有)成员变量或者属性的机制,KVC可以自动的将NSNumber或NSValue对象解包成相应的数值类型或结构体类型,以达到适配的目的。常用方法如下:

// 通过键/键路径取值(替代getter方法)- (nullable id)valueForKey:(NSString *)key;- (nullable id)valueForKeyPath:(NSString *)keyPath;// 对键/键路径赋值(替代setter方法)- (void)setValue:(nullable id)value forKey:(NSString *)key;- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;//对未定义的键的赋值、取值的处理方法,默认实现为抛出异常:NSUnknownKeyException,不适用于键路径的检查- (void)setValue:(id)value forUndefinedKey:(NSString *)key;- (id)valueForUndefinedKey:(NSString *)key;// 验证键/键路径对应的值是否可用,KVC不会自动调用键/键路径值验证方法(CoreData中会自动调用),需手动实现- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKeyPath:(NSString *)inKeyPath error:(out NSError **)outError;

注意:key和keyPath均可用于单层的属性的赋值、取值,而对于复合(多层)属性,即对象的属性又具有属性,而你想要访问的是更深层次的属性,此时就只能使用keyPath,表达式大致为:@“最外层属性.内层属性……”。集合运算符(Collection Operators)是一种特殊的keyPath,它是@开头的特殊字符串,格式如下:

更多介绍请点这里


撕下包装,探究实质

KVO

OC中的KVO来源于设计模式中的观察者模式,它的基本思想引入一段话来描述:

一个目标对象管理所有依赖于它的观察者对象,并在它自身的状态改变时主动通知观察者对象。这个主动通知通常是通过调用各观察者对象所提供的接口方法来实现的。观察者模式较完美地将目标对象与观察者对象解耦。

系统提供的KVO实现利用了动态地修改isa指针值的技术。在苹果的官方文档中有如下描述:

Key-Value Observing Implementation Details
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.

大致可以理解为:

当某个类的对象第一次被观察时,系统就会在运行时动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的 setter 方法。

派生类在被重写的 setter 方法中实现真正的通知机制,这么做是基于设置属性值时会调用 setter 方法,而通过重写就获得了 KVO 需要的通知机制。当然前提是要通过遵循 KVO 的属性设置方式来变更属性值(KVC),如果仅是直接修改属性对应的成员变量,是无法实现 KVO 的。

同时派生类还重写了 class 方法以“欺骗”外部调用者它就是起初的那个类,然后系统将这个对象的 isa 指针指向这个新诞生的派生类,因此这个对象就成为该派生类的对象了,此时只有通过运行时(用include导入objc/runtime.h)函数class_getName(object_getClass(XX))(isa指针已被保护不允许被访问)才能知道对象真正的类。因而在该对象上对 setter 的调用就会调用重写的 setter,从而激活键值通知机制。此外,派生类还重写了 dealloc 方法来释放资源。

因此我们就可以手动实现KVO,不过我们需要借助OC的runtime,它允许我们在程序运行时动态的创建新类、拓展方法、method-swizzling、绑定属性等等这些有趣的事情。如果想要深入理解OC的运行时,请点击这里。


自实现简易KVO

step1:派生子类,并将被监听的对象isa指针指向派生的新类

- (Class)createKVOClassWithOriginalClassName:(NSString *)originalClassName {    NSString *KVOClassName = [kClassPrefix_KVO stringByAppendingString:originalClassName];    Class observedClass = NSClassFromString(KVOClassName);    //学习苹果的做法,创建新类前,先判断此类是否已经存在    if (observedClass) {        return observedClass;    }    Class originalClass = object_getClass(self);    //创建新类的步骤1:为新派生出的子类分配存储空间    Class KVOClass = objc_allocateClassPair(originalClass, KVOClassName.UTF8String, 0);    //获取监听对象的class方法的实现,并替换为新类的class实现    Method classMethod = class_getInstanceMethod(originalClass, @selector(class));    const char *types = method_getTypeEncoding(classMethod);    //步骤2:为新类增加方法用class_addMethod,为新类增加变量用class_addIvar    class_addMethod(KVOClass, @selector(class), (IMP)KVO_Class, types);    //步骤3:注册这个新类,以便外界发现使用    objc_registerClassPair(KVOClass);    return KVOClass;}
// 2.在对象原本的类名前缀Observer_,表示派生(fork)出的子类,并将当前对象所属的类设置为派生的子类,即对象的isa指针指向这个派生的类    Class observedClass = object_getClass(self);    NSString *className = NSStringFromClass(observedClass);    if (![className hasPrefix:kClassPrefix_KVO]) {        observedClass = [self createKVOClassWithOriginalClassName:className];        object_setClass(self, observedClass);    }

step2:监听属性的有效性,是否能正常获取setter方法

// 1.获取被观察的属性的setter方法,如果没有则抛出异常,故此Demo只适用于观察属性,而系统的KVO可以观察私有的成员变量    SEL setterSelector = NSSelectorFromString(getSetter(key));    //self 表示被观察的对象    Method setterMethod = class_getInstanceMethod([self class], setterSelector);    if (!setterMethod) {        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat: @"unrecognized selector sent to instance %@", self] userInfo:nil];        return;    }

step3:重写setter方法

//从新类(或者父类)方法分发表中查找是否有被监听属性的setter方法的实现,没有则添加。IMP是implementation的缩写,它是OC方法实现代码块的地址,类似函数指针,通过它可以访问任意一个方法,并且可以免去发送消息的代价    if (![self hasSelector:setterSelector]) {        const char *types = method_getTypeEncoding(setterMethod);        class_addMethod(observedClass, setterSelector, (IMP)KVO_Setter, types);    }
static void KVO_Setter(id self, SEL _cmd, id newValue) {    NSString *setterName = NSStringFromSelector(_cmd);    NSString *getterName = getterForSetter(setterName);    if (!getterName) {        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"unrecognized selector sent to instance %p", self] userInfo:nil];        return;    }    id oldValue = [self valueForKey:getterName];    struct objc_super superClass = {        .receiver = self,        .super_class = class_getSuperclass(object_getClass(self))    };    //KVO核心部分,在改变值前后回调被观察对象的willChangeValueForKey:和didChangeValueForKey:方法    [self willChangeValueForKey:getterName];    //通过父类的setter方法设置新的值    void (*objc_msgSendSuperKVO)(void *, SEL, id) = (void *)objc_msgSendSuper;    objc_msgSendSuperKVO(&superClass, _cmd, newValue);    [self didChangeValueForKey:getterName];    NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge void *)KAssiociateObserver_KVO);    for (TestObserverInfo *info in observers) {        if ([info.key isEqualToString:getterName]) {            //异步回调handler            dispatch_async(dispatch_queue_create(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{                info.handler(self, getterName, oldValue, newValue);            });        }    }}

step4:释放动态绑定的监听者对象

- (void)removeManualObserver:(NSObject *)object forKey:(NSString *)key {    NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge void *)KAssiociateObserver_KVO);    TestObserverInfo *observerRemoved = nil;    for (TestObserverInfo *observerInfo in observers) {        if (observerInfo.observer == object && [observerInfo.key isEqualToString:key]) {            observerRemoved = observerInfo;            break;        }    }    [observers removeObject:observerRemoved];}

以上只是实现了简易的KVO,只能用于对象类型的属性监听,而系统的KVO比这个强大多了。这里是源代码,这里是我学习后的源代码,添加了很多注释,请各位看客原谅我这个厚颜无耻的搬运工,希望能帮助大家快速的理解。


KVC

贴上一段代码:

[_testStu setValue:[NSNumber numberWithInteger:12] forKey:@"age"];

编译器处理后结果:

SEL sel = sel_get_uid ("setValue:forKey:");  IMP method = objc_msg_lookup (object_getClass(_testStu),sel);  method(_testStu, sel, @12, @"age"); 

因此KVC内部原理可以理解为:当对象在调用setValue:forkey:时,首先会获取此方法的编号,然后在该对象所属的类所维护的分发表(dispatch table)中根据编号找到对应方法的实现体的入口(类似于函数指针),最后将该方法所需要的参数传进去并执行实现代码。Value:forKey与之大同小异,此处不再赘述。

前面已经说过,对于未定义key系统默认会抛出异常,那么对于一个给定的key从对象中取值或者赋值时,必然有一个对key的查找顺序。前面也说过KVC是直接访问属性值的一种机制,那么访问器方法(setter、getter)被闲置了吗?实际上并不是这样,虽然我们使用的是KVC的规定访问方法,但KVC实际上也是尽量在访问器方法的帮助下工作,毕竟setter、getter方法是标准的属性访问接口,如果有这样的便利条件不用而自己“填海造陆”就像是有公路可以走而你偏偏要自己造小路,这明显就会影响效率。所以为了设置或者返回对象的属性或者成员变量的值,KVC会按下面的顺序获取方法:

  1. 检查是否存在-key、-iskey(只针对布尔值有效)或者-getkey的getter方法,有则这些方法返回值;检查是否存在名为-setkey:的方法,并使用它做设置值。对于-getkey和-setkey:方法,将大写key字符串的第一个字母,并与Cocoa的方法命名保持一致;

  2. 如果上述方法不可用,则检查名为-_key、-_iskey(只针对布尔值有效)、-_getkey和-_setkey:方法;

  3. 如果没有找到访问器方法,将尝试直接访问名为:key或_key的实例变量;

  4. 如果仍为找到,则调用valueForUndefinedKey:和setValue:forUndefinedKey:方法


明好坏,知得失

通过上面的KVC的简单用法,或许还体会不到它带给了我们什么便利,如果只是简单的对属性赋值,用setter、getter方法岂不更好?

Apple官方例子:

@interface People: NSObject @property (nonatomic, strong) NSString *name; @property (nonatomic, strong) NSNumber *age; @end 

如上,假设一个类People,具有name和age两个属性。当我们要统计People实例即每一个人时,可能会这样做:

- (id)tableView:(UITableView *)tableview       objectValueForTableColumn:(id)column row:(NSInteger)row {     People *people = [peoleArray objectAtIndex:row];     if ([[column identifier] isEqualToString:@"name"]) {         return [people name];     }     if ([[column identifier] isEqualToString:@"age"]) {         return [people age];     }     // And so on. } 

有了KVC,可以直接这样:

People *people = [peopleArray objectAtIndex:row]; return [people valueForKey:[column identifier]];

无疑,KVC 简化了我们的代码。比如通过setValuesForKeysWithDictionary:方法,可以帮助我们快速给对象的属性匹配与字典中同名的key的值等。对象没有的属性将会调用- (void)setValue:(id)value forUndefinedKey:(NSString *)key;

有时我们需要访问对象的私有成员变量,但访问次数极少,所以我们并不希望它暴露给外界,此时我们就可以使用KVC。从一定程度上说,KVC破坏了数据的封装性,因为即使是对象的私有变量,KVC也可以“暴力”的读取或者给它赋值,当然这肯定会影响性能,从上面对象key的查找过程可以看出。我们也可以通过这个方法来禁止这种行为:

+ (BOOL)accessInstanceVariablesDirectly {    return NO;}

对于KVO的一些不足,可以参见这里,大致上对比其他的回调方式(block、delegate),重写-addObserver:forKeyPath:options:context:方法所引发的一系列问题。同时KVO的实现在创建子类、重写方法等等方面的内存消耗是比较大的,作者并开源了自实现的KVO:MAKVONotificationCenter。


总结

OC的运行时可以帮助我们解决很多的问题,这也是它的核心,在阅读一些开源框架时,就可以看到它们的身影,使用它们要建立在了解原理和性能消耗的基础上,对于经验尚浅的开发者使用它或许会起反作用,也许这就是它为什么在iOS高级工程师上是一个老生常谈的问题。当然学习需要坚持,知识得靠积累,一步一步来,相信结果不会差!

0 0