网上关于KVC和KVO的介绍一大片,基本用法这里就不介绍了,这篇主要聊聊KVC和KVO的实现,以及我们自己动手实现一套KVO。

KVC


KVC也就是key-value-coding,即键值编码,通常用来给某个对象的只读属性或者隐藏属性进行赋值,或者获取某个对象的因此属性的值。虽然我们通过runtime也可以达到这样的目的,但KVC无疑是一种更便捷的方式。

例如Person.m:

@interface Person ()
{
NSString *_name;
}
@property (nonatomic, assign) NSInteger age;
@end
@implementation Person
@end

Person类的.h文件中并没有暴露这两个属性,但是通过KVC,我们就可以修改以及读取这两个属性:

- (void)testKVC {
Person *p = [[Person alloc] init];
[p setValue:@10 forKey:@"age"];
NSLog(@"p的age--%@",[p valueForKey:@"age"]);
[p setValue:@"小王" forKey:@"_name"];
NSLog(@"p的name--%@",[p valueForKey:@"_name"]);
}

输出如下:

2017-08-31 15:40:08.105 OCDemo[65042:3097442] p的age--10
2017-08-31 15:40:08.105 OCDemo[65042:3097442] p的name--小王

在KVC中,无论调用- (void)setValue:(nullable id)value forKey:(NSString *)key还是- (void)setValue:(nullable id)value forKey:(NSString *)key方法,都是通过NSString对象来指定被操作属性的。

注意:Foundation框架会按照_<key>_is<Key><key>is<Key>的顺序查找成员变量。当然,KVO的操作首先是查找成员变量对应的getter/setter方法。

对于上述例子的[p valueForKey:@"age"]方法,底层的执行机制如下:

  1. 首先KVC调用方法的顺序为getAge->isAge->age
  2. 如果上述三个方法都没有找到,则找成员变量,顺序为_Age->_isAge->age->isAge
  3. 如果上面都没有找到,那么系统会执行该对象的- (void)setValue:(id)value forUndefinedKey:(NSString *)key,如果该对象没有实现此方法,程序异常退出。

对于上述例子的[p setValue:@10 forKey:@"age"]方法,底层的执行机制如下:

  1. 首先KVC调用setAge方法,然后调用setIsAge方法
  2. 如果没有setIsAge方法,则找成员变量,顺序为_Age->_isAge->age->isAge
  3. 如果还是没有,那么系统会执行该对象的- (id)valueForUndefinedKey:(NSString *)key方法,如果该对象没有实现此方法,程序异常退出。

另外,注意一点,当我们将上述setValue代码修改一下,如:[p setValue:[[Person alloc] init] forKey:@"age"],程序直接崩溃了,异常日志为:-[Person longLongValue]: unrecognized selector sent to instance 0x6100000330c0,说明,KVC在赋值的时候,是会根据基本数据类型的具体类型,来调用Value相应的intValuedoubleValue方法。因为NSNumberNSString都实现了这些方法,所以程序能正常运行。

还有一点,假如setValuevaluenil,程序则会报Terminating app due to uncaught exception 'NSInvalidArgumentException'异常,虽然苹果对nil做了很好的处理(这里要吐槽一下java的判空了),但是基本数据类型不能接受nil,因此,系统会自动调用setNilValueForKey方法,我们可以在这里做些处理。

KVC除了操作对象之外,还可以操作对象的复合属性,例如,在Person类中,又有一个Person属性。通过- (void)setValue:(id)value forKeyPath:(NSString *)keyPath- (id)valueForKeyPath:(NSString *)keyPath来操作复合属性。

@interface Person ()
{
NSString *_name;
NSInteger _age;
}
@property (nonatomic, strong) Person *p;
@end
- (void)testKVC {
Person *p = [[Person alloc] init];
[p setValue:[[Person alloc] init] forKey:@"p"];
[p setValue:@10 forKeyPath:@"p.age"];
NSLog(@"p的p.age--%@",[p valueForKeyPath:@"p.age"]);
}

通过KVC,我们可以很容易的操作成员变量,最常用的就是对私有变量赋值以及字典转模型。

KVO


KVO即key-value-observing,通常情况下,我们会利用KVO来监听某个对象的属性变化。

例如:

新建模型RGBColor,有以下四个属性:

@interface RGBColor : NSObject
@property (nonatomic, assign) double r;
@property (nonatomic, assign) double g;
@property (nonatomic, assign) double b;
@property (nonatomic, strong, readonly) UIColor *color;
@end

Foundation框架提供的表示属性依赖的机制如下:

+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key

更详细的如下:

+ (NSSet<NSString *> *)keyPathsForValuesAffecting<键名>

注意,第一个方法的优先级是大于第二个的。在我们的例子中如下:

+ (NSSet<NSString *> *)keyPathsForValuesAffectingColor {
return [NSSet setWithObjects:@"r",@"g",@"b", nil];
}
- (UIColor *)color {
return [UIColor colorWithRed:self.r green:self.g blue:self.b alpha:1.0];
}

具体使用如下:

- (void)testKVO {
self.rgbColor = [[RGBColor alloc] init];
[self.rgbColor addObserver:self forKeyPath:@"color" options:NSKeyValueObservingOptionInitial context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"color"]) {
self.view.backgroundColor = self.rgbColor.color;
}
}
- (IBAction)updateR:(UISlider *)sender {
self.rgbColor.r = sender.value;
}
- (IBAction)updateG:(UISlider *)sender {
self.rgbColor.g = sender.value;
}
- (IBAction)updateB:(UISlider *)sender {
self.rgbColor.b = sender.value;
}
手动通知 VS 自动通知

你会发现刚才的例子有点神奇,为什么修改rgb的值会触发observeValueForKeyPath,明明观察的是color。但是实际上发送的事情是,当RGBColor实例的-setR:等方法被调用的时候以下方法:

- (void)willChangeValueForKey:(NSString *)key

和:

- (void)didChangeValueForKey:(NSString *)key

会在运行-setR:中的代码之前以及之后被自动调用。但有些情况,我们希望关闭键值改变的通知或者达到某些条件再通知,我们需要做以下事情:

+ (BOOL)automaticallyNotifiesObserversOfB {
return NO;
}
- (void)setB:(double)b {
if (b > 0.5) {
[self willChangeValueForKey:@"b"];
}
_b = b;
if (b > 0.5) {
[self didChangeValueForKey:@"b"];
}
}

方法+ (BOOL)automaticallyNotifiesObserversOf<Key>会告诉系统是否需要关闭自动通知。这里我们进行了收到调用。

你可能会疑惑,自动通知的情况下,系统是如何调用-willChangeValueForKey:-didChangeValueForKey:的。

基本实现原理

当观察某对象时,KVO机制动态创建当前类的子类,即NSKVONotifying_<原类>,并为这个新类重写被观察属性keysetter方法。之后修改当前类的isa指向,所以,我们从应用层上看来,当前类是没有改变的。调用流程如下:



既然知道了KVO的实现原理,那么我们就可以自己来实现了。我们这里实现一个可以传Block的KVO。

首先,定义NSObject的分类:

"NSObject+KVO.h"
typedef void(^GGObserverBlock)(NSString * _Nullable newValue);
@interface NSObject (KVO)
- (void)gg_addObserver:(NSObject *_Nullable)observer
forKeyPath:(NSString *_Nullable)keyPath
options:(NSKeyValueObservingOptions)options
context:(nullable void *)context
block:(GGObserverBlock _Nullable )observerBlock;
@end

这里定义的block比较简单。再来看m文件的实现

NSString *const OriginalClass = @"OriginalClass";
NSString *const ObserverBlock = @"ObserverBlock";
@implementation NSObject (KVO)
- (void)gg_addObserver:(NSObject *_Nullable)observer
forKeyPath:(NSString *_Nullable)keyPath
options:(NSKeyValueObservingOptions)options
context:(nullable void *)context
block:(GGObserverBlock _Nullable )observerBlock {
if (keyPath == nil)return;
if (keyPath.length == 0)return;
//检测setter方法 并找到
BOOL responds = false;
SEL sel = NULL;
//1.检测属性是否定义setter方法
unsigned int propertyCount;
objc_property_t *p = class_copyPropertyList(self.class, &propertyCount);
for (int i = 0; i < propertyCount; i++) {
objc_property_t property_t = p[i];
const char*name = property_getName(property_t);
if ([keyPath isEqualToString:[NSString stringWithUTF8String:name]]) {
NSString *attrsName = [NSString stringWithUTF8String:property_getAttributes(property_t)];
//包含S 表示重写了setter方法
NSArray *items = [attrsName componentsSeparatedByString:@","];
for (NSString *name in items) {
if ([name hasPrefix:@"S"]) {
NSString *setterMethod = [name substringFromIndex:1];
sel = NSSelectorFromString(setterMethod);
responds = true;
break;
}
}
break;
}
}
free(p);
//2. 检测set<KeyPath>
if (!responds) {
sel = NSSelectorFromString([NSString stringWithFormat:@"set%@%@:",[[keyPath substringToIndex:1] uppercaseString],[keyPath substringFromIndex:1]]);
responds = class_respondsToSelector(self.class, sel);
}
//3. 检测setIs<keyPath>
if (!responds) {
sel = NSSelectorFromString([NSString stringWithFormat:@"setIs%@%@:",[[keyPath substringToIndex:1] uppercaseString],[keyPath substringFromIndex:1]]);
responds = class_respondsToSelector(self.class, sel);
}
//都没有找到 抛出异常
if (!responds) {
NSException *exception = [[NSException alloc] initWithName:@"NoKeyPath" reason:[NSString stringWithFormat:@"没有找到名为%@的属性,在%@类中",keyPath,self] userInfo:nil];
@throw exception;
}
//动态生成一个类
NSString *newClassName = [@"GGKVONotifying_" stringByAppendingString:NSStringFromClass(self.class)];
Class myClass = objc_allocateClassPair(self.class, (__bridge const void*)newClassName, 0);
//重写找到的setter方法
class_addMethod(myClass, sel, (IMP)setKeyPath,"v@:@");
objc_registerClassPair(myClass);
objc_setAssociatedObject(self, (__bridge const void*)OriginalClass, self.class, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
//修改本类的isa指针
object_setClass(self, myClass);
if (observerBlock) {
objc_setAssociatedObject(self, (__bridge const void*)ObserverBlock, observerBlock, OBJC_ASSOCIATION_COPY);
}
}
void setKeyPath(id self,SEL _cmd,NSString *value) {
//修改isa
Class originalClass = objc_getAssociatedObject(self, (__bridge const void*)OriginalClass);
//修改到原来的类
object_setClass(self, originalClass);
//调用setter方法
((void(*)(id,SEL, id))objc_msgSend)(self,_cmd,value);
//回调
GGObserverBlock observerBlock = objc_getAssociatedObject(self, (__bridge const void *)ObserverBlock);
if (observerBlock) {
observerBlock(value);
}
Class kvoClass = objc_getClass((__bridge const void*)[@"GGKVONotifying_" stringByAppendingString:NSStringFromClass(originalClass)]);
//修改回派生类
object_setClass(self, kvoClass);
}
@end

当然,这里只是简单的实现,具体还有很多的判断逻辑。