欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

iOS面试题库——KVC与KVO

程序员文章站 2022-04-13 15:33:06
...

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界的图: iOS面试题库——KVC与KVO.

回到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:
相关标签: KVO KVC 原理