iOS面试题库——KVC与KVO

KVC与KVO

本文将会详解在面试中的常客——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:是通过\来获取属性的值。在一个对象实例中按get<Key><key>is<Key>_<key>顺序匹配。命中的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<Key>, objectIn<Key>AtIndex:, countOf<Key>, enumeratorOf<Key>, memberOf<Key>:,有兴趣的同学可以去Search Pattern for the Basic Getter查看详细的查询路径。
  2. 非Object对象包装成NSNumber或NSValue列表查看Representing Non-Object Values

1.1.2 setValue:forKey:

-setValue:forKey:-valueForKey:也是根据给定的\匹配方法名set<Key>:_set<Key>,如果命中,调用方法将Value作为参数传入。未命中则看+ accessInstanceVariablesDirectly是否返回YES,YES则按 _<key>, _is<Key>, <key>, is<Key>顺序匹配实例变量。如果命中直接将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 <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_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_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界的图: isa & super class.

回到KVO。前文提到的isa-swizzling对于了解过runtime的同学可能对此有点眼熟。method swizzling也是runtime的一种黑魔法。可以通过method swizzling进行方法的互换。回到KVO上面,我们猜测isa-swizzling就是类似method swizzling,只不过是对Class的交换。

在KVO中当一个监听者被注册被监听的对象上时。被监听对象的isa指针已经被更改了。被监听对象的isa指针被修改为指向为一个中间类。改中间类可能是该类的子类。重写了被监听对象的属性。然后在改属性值被修改时,会触发监听通知。

KVO的详细流程

  1. 监听者调用监听的方法。
  2. 被监听者派生一个中间类。被监听对象的isa指针指向派生类
  3. 被监听的属性发生变化,由中间类触发监听通知(具体方式未知)。
  4. 监听者收到通知。触发observeValueForKey:ofObject:change:context:

猜你喜欢

转载自blog.csdn.net/yuwuchaio/article/details/80701681