开发技巧|iOS面试题库——KVC与KVO

KVC与KVO

  • KVC与KVO
    • 1.1KVC
      • 1.1.1 valueForKey:
      • 1.1.2 setValue:forKey:
    • 1.2 KVO
      • 1.2.1 使用
      • 1.2.2 原理

本文将会详解在面试中的常客——KVO实现的原理,在了解KVO之前我们要对KVC进行一个全面的了解,毕竟连官方文档都提到过:
important: In order to understand key-value observing, you must first understand key-value coding.
1.1KVC KVC全称:Key-value coding(键-值编码),通过KVC机制我们可以间接的访问对象的属性。而KVC之所以能够访问属性是因为对象遵守了一个非正式的NSKeyValueCoding协议(NSObject开始就遵守了此协议,所以继承自NSObject的对象都可以使用KVC)。开发中我们都知道在对于属性可以使用gettersetter或者直接使用实例变量来进行直接访问和修改。但这些访问方式是需要依靠属性的get方法、set方法、变量名。随着对象定义的属性增加或者变动。编译器生成的这些gettersetter会越来越多。KVC则是通过是用字符串的名字Key来对属性进行访问和修改。
KVC中最关键的两个方法:
-valueForKey: -setValue:forKey:

1.1.1 valueForKey:
-valueForKey:是通过\来获取属性的值。在一个对象实例中按getis_顺序匹配。命中的Value的类型如果是对象直接返回。如果命中的Value是能被包装成NSNumber的数值类型。包装成NSNumber返回。不支持NSNumber的数值类型则包装成NSValue返回。如果没用命中调用-valueForUndefinedKey:抛出异常,valueForUndefinedKey:可在子类中重写忽略抛出的异常,自己处理。
@interface Person @property (nonatomic, assign) CGFloat height; @end

p.height = 119.0; NSNumber *height = [p valueForKey:@"height"]; //CGFloat 包装成NSNumber。

注意
  1. 上述的查找过程中省略了很多其他情况下的查找 countOf, objectInAtIndex:, countOf, enumeratorOf, memberOf:,有兴趣的同学可以去Search Pattern for the Basic Getter查看详细的查询路径。
  2. 非Object对象包装成NSNumber或NSValue列表查看Representing Non-Object Values
1.1.2 setValue:forKey:
-setValue:forKey:-valueForKey:也是根据给定的\匹配方法名set:_set,如果命中,调用方法将Value作为参数传入。未命中则看+ accessInstanceVariablesDirectly是否返回YES,YES则按 _, _is, , is顺序匹配实例变量。如果命中直接将Value给变量赋值。NO则调用-setValue:forUndefinedKey:抛出异常。-setValue:forUndefinedKey:也可以被子类重写。
// setValue: forKey:使用 [myAccount setValue:@(100.0) forKey:@"currentBalance"];

1.2 KVO 1.2.1 使用
KVO在iOS中是观察者模式的一种表现。我们可以使用KVO让某个对象成为另外一个对象的监听者。当被监听对象的属性发生改变时,KVO就会通知监听者。
关于KVO的使用网上有很多教程,KVO使用主要是三个步骤:
  1. 调用addObserver:forKeyPath:options:context:注册成为监听者。
  2. 监听者实现observeValueForKey:ofObject:change:context:方法。
  3. 调用removeObserver:forKeyPath:移除监听
前面说过KVO的实现是建立在KVC的基础上的。即被监听的属性必须能满足KVC的,才能是用KVO来监听。
KVO的自动触发监听通知的方法系列:
// Call the accessor method. [account setName:@"Savings"]; // Use setValue:forKey:. [account setValue:@"Savings" forKey:@"name"]; // Use a key path, where 'account' is a kvc-compliant property of 'document'. [document setValue:@"Savings" forKeyPath:@"account.name"]; // Use mutableArrayValueForKey: to retrieve a relationship proxy object. Transaction *newTransaction = <#Create a new transaction for the account#>; NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"]; [transactions addObject:newTransaction];

KVO的手动触发监听通知:
// 关闭balance的自动触发 + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey { BOOL automatic = NO; if ([theKey isEqualToString:@"balance"]) { automatic = NO; } else { automatic = [super automaticallyNotifiesObserversForKey:theKey]; } return automatic; }

要实现手动触发监听,你要执行在变更前执行willChangeValueForKey:方法,在变更后执行didChangeValueForKey:方法:
- (void)setBalance:(double)balance { [self willChangeValueForKey:@"balance"]; _balance = balance; [self didChangeValueForKey:@"balance"]; }

1.2.2 原理
>
Automatic key-value observing is implemented using a technique called isa-swizzling.
上面这句话在Key-Value Observing Implementation Details提到。意思就是KVO的是通过一种叫isa-swizzling的技术实现的。
查看NSObject.h文件。
@interface NSObject { Class isa; }

control + command点击Class,看到Class实际上是一个指向obj_class结构体的指针。
typedef struct objc_class *Class; struct objc_class { struct objc_class * isa; // 原始的代码 Class isa; #if !__OBJC2__ Class _Nullable super_classOBJC2_UNAVAILABLE; const char * _Nonnull nameOBJC2_UNAVAILABLE; long versionOBJC2_UNAVAILABLE; long infoOBJC2_UNAVAILABLE; long instance_sizeOBJC2_UNAVAILABLE; struct objc_ivar_list * _Nullable ivarsOBJC2_UNAVAILABLE; struct objc_method_list * _Nullable * _Nullable methodListsOBJC2_UNAVAILABLE; struct objc_cache * _Nonnull cacheOBJC2_UNAVAILABLE; struct objc_protocol_list * _Nullable protocolsOBJC2_UNAVAILABLE; #endif} OBJC2_UNAVAILABLE;

可以看到在objc_class结构体中还有一个isa,即类中还有一个指向objc_class的指针。类的isa指向的是元类(metaClass)的。如果将对象看成是通过类的实例。那么类就是元类的实例咯?
这篇文章中我们仅了解什么是Class isaobjc_class结构体中的其它部分会单开一篇runtime的文章来写。
typedef struct objc_object *id; struct objc_object { struct objc_class * isa; // 原始的代码 Class isa; };

idobj中可以代表个对象。id又是一个objc_object结构体的指针,且objc_object结构体中的isa指向的是该对象的类。
在这里是时候祭出火遍runtime界的图: 开发技巧|iOS面试题库——KVC与KVO
文章图片
.
回到KVO。前文提到的isa-swizzling对于了解过runtime的同学可能对此有点眼熟。method swizzling也是runtime的一种黑魔法。可以通过method swizzling进行方法的互换。回到KVO上面,我们猜测isa-swizzling就是类似method swizzling,只不过是对Class的交换。
在KVO中当一个监听者被注册被监听的对象上时。被监听对象的isa指针已经被更改了。被监听对象的isa指针被修改为指向为一个中间类。改中间类可能是该类的子类。重写了被监听对象的属性。然后在改属性值被修改时,会触发监听通知。
【开发技巧|iOS面试题库——KVC与KVO】KVO的详细流程
  1. 监听者调用监听的方法。
  2. 被监听者派生一个中间类。被监听对象的isa指针指向派生类
  3. 被监听的属性发生变化,由中间类触发监听通知(具体方式未知)。
  4. 监听者收到通知。触发observeValueForKey:ofObject:change:context:

    推荐阅读