阿里、字节:一套高效的iOS面试题(六 - KVO)

1733

KVO

撸面试题中,文中内容基本上都是搬运自大佬博客及自我理解,可能有点乱,不喜勿喷!!!

原文题目来自:阿里、字节:一套高效的iOS面试题

runloop 一样,这也是标配的知识点了,同样列出几个典型问题

1、实现原理?

  1. 动态创建中间类;
  2. 重写指定属性的 -setter 方法,+class 方法;
  3. 通过 isa_swizzling 将被观察对象的 isa 指向刚创建的中间类

2、如何手动关闭 KVO?

重写 -automaticallyNotifiesObserversForKey: 方法

- (BOOL)automaticallyNotifiesObserverForKey:(NSString *)key {
    return NO;
}

3、通过 KVC 修改属性会触发 KVO 吗?

4、哪些情况下使用 KVO 会崩溃,怎么防护崩溃?

多次移除观察者 不移除观察者

在 init 与 dealloc 中成对使用 -addObserver:forKeyPath:options:context:-removeObserver:forKeyPath:context:

5、KVO 的优缺点

优点:

  1. 无侵入
  2. 使用简单

缺点:

  1. 容易奔溃;
  2. 过度依赖 String
  3. 注册观察者的代码与属性改变的上下文不同,通过 void * 传递上下文;
  4. 代码分散;
  5. 响应方法可能一大推分支;
  6. 需要自己处理 super 的 observe 事件。。。

搞事情~~~

1000 什么是 KVO?

KVO ,即 Key-Value-Observing,翻译过来就是 键值监听。这是苹果提供的一种设计模式,准确一点,是一种观察者设计模式(还有一个大哥:NSNotification)。

KVO 提供一种机制,观察者对象可以指定一个被观察者对象(person :Person 类)的一个属性(name : 类型为 NSString)。当 person.name 发生更改时,观察者对象会收到通知,同时可以做出相应处理。

使用 KVO 时,无需侵入被观察者类。也就是说,不需要为被观察者类添加任何代码即可正常使用观察期属性变化。

1001 如何使用 KVO?

如何简单使用,直接上代码:

- (void)test_addKVO {
    // 观察者 self 指定被观察者属性为 name,观察选项为 新值&旧值 ,无需上下文
    [self.person addObserver:self
                  forKeyPath:@"name"
                     options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
                     context:nil];
    
    // 观察者 self 指定被观察者属性为 age,观察选项为 新值&旧值 ,无需上下文
    [self.person addObserver:self
                  forKeyPath:@"age"
                     options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
                     context:nil];
}

/**
 * 移除 KVO 监视的属性
 */
- (void)test_removeKVO {
    [self.person removeObserver:self forKeyPath:@"name"];
    [self.person removeObserver:self forKeyPath:@"age"];
}

/**
 * 描述: 当被观察者指定竖向发生变化,即可在这里获得通知
 * 参数说明:
 *  keyPath(NSString *) : 键,改变值的属性的名字。
 *  object(id):  对象,值改变的属性所属的对象,谁的属性改变了
 *  change(NSDictionary<NSKeyValueChangeKey,id> *): 改变的内容,新值 or 旧值
 *  context(void *):值改变的上下文
 */
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    
    if ([keyPath isEqualToString:@"name"] && object == self.person) {
        
        NSString *theOldValue = [change objectForKey:NSKeyValueChangeOldKey];
        NSString *theNewValue = [change objectForKey:NSKeyValueChangeNewKey];
        NSLog(@"self.person.name 属性发生变化,旧值为:%@ >>> 新值为:%@", theOldValue, theNewValue);
        
        [self displayInfo];
        
        
    } else if ([keyPath isEqualToString:@"age"] && object == self.person) {
        
        NSNumber *theOldValue = [change objectForKey:NSKeyValueChangeOldKey];
        NSNumber *theNewValue = [change objectForKey:NSKeyValueChangeNewKey];
        NSLog(@"self.person.age 属性发生变化,旧值为:%@ >>> 新值为:%@", theOldValue, theNewValue);
        
        [self displayInfo];
        
    }  else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
    
}

看看测试结果:

使用过程拢共分三步:

  1. 添加 Observer

    - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context

  2. 收到 被 Observer 的属性改变消息时做出处理

    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context

  3. 移除 Observer

    - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath

具体参数上边代码也有说明。。。

如何设置依赖?

我觉得这种情况是很容易遇到的。。。

@property (nonatomic, assign)   NSString *fullname;
@property (nonatomic, strong)   NSString *firstname;
@property (nonatomic, strong)   NSString *lastname;

- (NSString *)fullname {
    return [NSString stringWithFormat:@"%@ %@", _firstname, _lastname];
}

不难看出, fullname 依赖于 firstname 和 lastname 两个属性。当 firstname 和 lastname 发生变化时,fullname 也会发生变化,照常理来说,应该要触发 fullname 的 KVO 才好。

此时,有两种解决方案:

  1. 重写 firstname 和 lastname 的 setter,手动触发 fullname 的 KVO。但是这样一来,我们需要修改三个属性的 setter。
  2. 设置依赖键。

可以通过 keyPathsForValuesAffecting<Key>keyPathsForValuesAffectingValueForKey: 来设置依赖键:

+ (NSSet *)keyPathsForValuesAffectingFullname {
    NSSet *keyPaths = [NSSet setWithObjects:@"firstname", @"lastname", nil];
    return keyPaths;
}

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    NSSet *moreKeyPaths = nil;

    if ([key isEqualToString:@"fullname"]) {
        moreKeyPaths = [NSSet setWithObjects:@"firstname", @"lastname", nil];
    }

    if (moreKeyPaths) {
        keyPaths = [keyPaths setByAddingObjectsFromSet:moreKeyPaths];
    }

    return keyPaths;
}

以上两个方法任意实现一个就可以了。它们告诉系统 fullname 属性依赖于哪些属性,并且都返回一个包含了当前属性依赖的 keyPath 的集合。值得注意的是,keyPathsForValuesAffectingValueForKey: 这个方法需要先向 super 查询 key 的依赖键,然后将自己依赖键添加到从 super 获得的集合中,这样得到的 key 的依赖键才完整。

更多使用说明

简单使用其实就这点。如果想要学习更深入的使用,请移步 KVO 和 KVC 的使用和实现。其中有更多关于 KVO / KVC 的解析。

注意 : 只能观察属性、不溢出观察者会崩溃、多次移除也会崩溃

KVO 的 addObserver 与 removeObserver 必须成对出现,否则将很可能导致奔溃。

苹果建议在 init 时 addObserver,在 dealloc 时 removeObserver。

如何触发 KVO?

我们修改被观察者的指定属性时,即可触发 KVO。

具体方式有很多,可不止点语法跟 setter。

// 点语法
self.person.name = @"李四";

// 调用 setter
[self.person setName:@"李四"];

// 使用 KVC
[self.person setValue:@"李四" forKey:@"name"];

// 使用 KVC
[self setValue:@"李四" forKey:@"person.name"];

// 通过 mutableArrayValueForKey: 方法获取代理对象,然后使用代理对象操作
// -mutableSetValueForKey:
// -mutableOrderedSetValueForKey:
Car *newCar = [[Car alloc] init];
NSMutableArray *hisCars = [self.person mutableArrayValueForKey:@"cars"];
[newCar addObject:newCar];

如何手动触发 KVO

调用 - (void)willChangeValueForKey:(NSString *)key 以及 - (void)didChangeValueForKey:(NSString *)key

实例:

- (void)setName:(NSString *)name {
    if (name == _name) return;
    [self willChangeValueForKey: @"name"];
    _name = name;
    [self didChangeValueForKey: @"name"];
}

如何控制当前对象的自动调用过程?

如果想控制当前对象的 KVO 自动调用过程,也就是有上边的两个方法发起的 KVO 调用。那么可以重写下面这个方法,

- (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    BOOL rst = YES;
    if ([key isEqualToString:@"name"]) {
        rst = NO;
    } else {
        rst = [super automaticallyNotifiesObserversForKey:key];
    }
    
    return rst;
}

1002 KVO 的实现原理

KVO 是通过 isa-swizzling 技术来实现的。。。

看一下官方文档:

Automatic key-value observing is implemented using a technique called isa-swizzling.

KVO 是使用 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.

isa 指针指向这个对象的类,这个维护一个分发表。而这个分发表包含了这个类的方法的实现,以及其他数据。

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.

当一个被观察者的一个属性被一个观察者订阅时,这个被观察者的 isa 指针被修改。此时 isa 指针指向一个中间类,而非这个实例真正的类。从结论上来讲,isa 指针不一定非要之上实例的实际类。

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.

因此,应该使用 class 方法来确定实例对象的类,而不是依靠 isa 指针。

简单来说:KVO 使用 isa-swizzling 来实现。当一个实例的属性被观察时,这个对象的 isa 指向一个动态创建的中间类。

KVO 具体的实现原理其实也不难:

  1. 当一个对象(被观察者对象,假定它时 Person 类的实例)第一次被观察时,一个新的类(NSKVONotifying_Person 类)会动态被创建(利用 runtime)。这个新类 继承自被观察类(Person 类);
  2. 同时重写被观察属性的 setter 方法,在赋值语句前后加上相应的通知(同时还会重写);
  3. 最后,将被观察对象的 isa 指针指向这个新创建的类(利用 runtime),也就是 NSKVONotifying_Person

验证一下(仅观察 name 属性):

首先,重写 Person 类的 description 方法:

- (NSString *)description {
    
    NSLog(@"实例地址:%p", self);
    
    IMP nameIMP = class_getMethodImplementation(object_getClass(self), @selector(setName:));
    IMP ageIMP = class_getMethodImplementation(object_getClass(self), @selector(setAge:));
    NSLog(@"方法地址:setName:%p --- setAge:%p", nameIMP, ageIMP);
    
    Class classMethodClass = [self class];
    Class runtimeClass = object_getClass(self);
    Class runttimeSuperClass = class_getSuperclass(runtimeClass);
    NSLog(@"类信息:classMethodClass: %@ --- runtimeClass: %@ --- runttimeSuperClass: %@", classMethodClass, runtimeClass, runttimeSuperClass);
    
    NSLog(@"方法列表");
    unsigned int count;
    Method *methodList = class_copyMethodList(runtimeClass, &count);
    for (NSInteger i = 0; i < count; ++i) {
        Method method = methodList[i];
        NSString *methodName = NSStringFromSelector(method_getName(method));
        NSLog(@"方法名:%@", methodName);
    }
    NSLog(@"");
    
    return @"";
}

然后测试一下:

Person *person1 = [[Person alloc] initWithName:@"张三" age:@18];
    Person *person2 = [[Person alloc] initWithName:@"李四" age:@19];
    
    [person1 description];
    [person2 description];
    
    
    NSLog(@"======================订阅 person1.name============================");
    
    [person1 addObserver:self
              forKeyPath:@"name"
                 options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
                 context:nil];
    
    [person1 description];
    [person2 description];

看一些结果:

实例地址:0x28122b860
方法地址:setName:0x1041308ac --- setAge:0x104130904
类信息:classMethodClass: Person --- runtimeClass: Person --- runttimeSuperClass: NSObject
方法列表
方法名:initWithName:age:
方法名:description
方法名:name
方法名:.cxx_destruct
方法名:setName:
方法名:age
方法名:setAge:

实例地址:0x28122b780
方法地址:setName:0x1041308ac --- setAge:0x104130904
类信息:classMethodClass: Person --- runtimeClass: Person --- runttimeSuperClass: NSObject
方法列表
方法名:initWithName:age:
方法名:description
方法名:name
方法名:.cxx_destruct
方法名:setName:
方法名:age
方法名:setAge:

======================订阅 person1.name============================
实例地址:0x28122b860
方法地址:setName:0x1c381e000 --- setAge:0x104130904
类信息:classMethodClass: Person --- runtimeClass: NSKVONotifying_Person --- runttimeSuperClass: Person
方法列表
方法名:setName:
方法名:class
方法名:dealloc
方法名:_isKVOA

实例地址:0x28122b780
方法地址:setName:0x1041308ac --- setAge:0x104130904
类信息:classMethodClass: Person --- runtimeClass: Person --- runttimeSuperClass: NSObject
方法列表
方法名:initWithName:age:
方法名:description
方法名:name
方法名:.cxx_destruct
方法名:setName:
方法名:age
方法名:setAge:

对比一下:

person2 没有任何改变,就不理他了

对象 第一次 第二次
person1 实例地址:0x28122b860 实例地址:0x28122b860
方法地址:setName:0x1041308ac --- setAge:0x104130904 方法地址:setName:0x1c381e000 --- setAge:0x104130904
类信息:classMethodClass: Person --- runtimeClass: Person --- runttimeSuperClass: NSObject 类信息:classMethodClass: Person --- runtimeClass: NSKVONotifying_Person --- runttimeSuperClass: Person
方法列表 方法列表
方法名:initWithName:age: 方法名:setName:
方法名:description 方法名:class
方法名:name 方法名:dealloc
方法名:.cxx_destruct 方法名:_isKVOA
方法名:setName:
方法名:age
方法名:setAge:

观察 person1.name 之后:

  1. setName: 方法的地址改变了(0x1041308ac -> 0x1c381e000)
  2. person1 是 NSKVONotifying_Person 类的实例;
  3. NSKVONotifying_Person 类是 Person 类的子类;
  4. NSKVONotifying_Person 类重写了三个方法:setName:、class、dealloc;
  5. NSKVONotifying_Person 类新增了一个方法:_isKVOA。

从上边五个不同可以看出来,person1 已经不是一个纯正的 Person 实例了。第一点 setName: 的地址改变也可以从第二三四点解释。都不是同一个方法了。当然地址改变了。而 _isKVOA 这个方法可以当做是使用 KVO 的标记,我们可以通过这个方法来判断当前类是否是 KVO 动态生成的类。

我们发现,setAge: 函数并没有重写。我们可以得出 KVO 只会重写被观察属性 setter 方法的结论。

对 class_getMethodImplementation、object_getClass 等方法不理解的朋友建议去研究研究 runtime。

1003 动手实现 KVO

当然只是实现通知时机了,怎么可能写整个机制(手动狗头)。。。

根据原理总结一下实现步骤:

  1. 是否存在该属性的 setter。可行性验证,KVO 只使用于属性;
  2. 创建中间类。判断该中间类是否已存在,重写 class 方法;
  3. 添加新的 setter。调用当前类的 setter 并通知观察者。
  4. 存储观察者信息。无非就是储存 observer、keyPath、block 这三个信息了。

是否有可以改进的点?

  1. 使用 block 来代替回调函数;

先创建两个常量字符串:

NSString *const LyKVOClassNamePrefix = @"LyKVONotifying_";
NSString *const kLyKVOAssociatedObservationInfos = @"LyKVOAssociatedObservationInfos";

开始~~~

- (void)ly_addObserver:(NSObject *)observer
            forKeyPath:(NSString *)keyPath
             withBlock:(LyKVOBlock)block
{
    // 1、检查 setter 是否存在
    NSString *setterStr = setterForGetter(keyPath);
    SEL setterSelector = NSSelectorFromString(setterStr);
    Method setterMethod = class_getInstanceMethod([self class], setterSelector);
    if (nil == setterMethod) {
        NSString *reason = [NSString stringWithFormat:@"Object %@ does not has a setter for key %@", self, keyPath];
        @throw [NSException exceptionWithName:NSInvalidArgumentException
                                       reason:reason
                                     userInfo:nil];
        
        return;
    }
    
    Class runtimeClass = object_getClass(self);
    NSString *runtimeClasssName = NSStringFromClass(runtimeClass);
    
    // 2、动态创建中间类(如果是第一次被 KVO 订阅,则此时还不是 KVO 类)
    if (![runtimeClasssName hasPrefix:LyKVOClassNamePrefix]) {
        runtimeClass = [self createKVOClassWithOriginalClassName:runtimeClasssName];
        object_setClass(self, runtimeClass);
    }
    
    // 3、添加我们的 setter(如果这个类还没有实现这个属性的 setter)
    if (![self hasImplementateSelector:setterSelector]) {
        const char *types = method_getTypeEncoding(setterMethod);
        class_addMethod(runtimeClass, setterSelector, (IMP)kvo_setter, types);
    }
    
    LyObservationInfo *info = [[LyObservationInfo alloc] initWithObserver:observer
                                                                  keyPath:keyPath
                                                                    block:block];
    
    NSMutableArray *observationInfos = objc_getAssociatedObject(self, (__bridge const void *)(kLyKVOAssociatedObservationInfos));
    if (!observerInfos) {
        observerInfos = [NSMutableArray array];
        objc_setAssociatedObject(self, (__bridge const void *)(kLyKVOAssociatedObservationInfos), observerInfos, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    [obserationInfos addObject:info];
}

细看一下:

第一步,检查是否存在该属性的 setter: 先通过 setterForGetter 方法获取提供属性(假定为 name)相应的 setter 方法的名字。只需要将属性的第一个字母 n 变为大写 N ,并分别在前后加上 "set" 和 ":" 即可。如此操作,我们就获得了我们需要的 setName: 方法。然后在使用 class_getInstanceMethod 方法来获取 setName: 的方法 Method。如果不存在,则意味着该类不存在这个属性(或者不存在这个属性的 setter)。

NSString * setterForGetter(NSString *getter) {
    if (nil == getter || getter.length < 5) {
        return nil;
    }
    
    NSString *firstLetter = [[getter substringToIndex:1] uppercaseString];
    NSString *remainingLetters = [getter substringFromIndex:1];
    
    NSString *setter = [NSString stringWithFormat:@"set%@%@:", firstLetter, remainingLetters];
    
    return setter;
}

第二步,创建中间类 : 指定一个 KVO 中间类前缀(就像苹果一样 LyKVOClassNamePrefix)。先通过 object_getClass(self) 来检查我们正在操作的这个实例是否已经是我们需要的中间类。然后,通过NSClassFromString() 检查我们需要的这个中间类是否已经被创建过了。如果能获取到该类,则证明类已经存在,无需再次创建。当然,在创建这个中间类的时候,我们需要重写这个类的 class 方法(学习苹果,隐藏这个中间类)。

- (Class)createKVOClassWithOriginalClassName:(NSString *)originalClassName {
    // 中间类类名
    NSString *kvoClassName = [LyKVOClassNamePrefix stringByAppendingString:originalClassName];
    Class kvoClass = NSClassFromString(kvoClassName);
    
    // 中间类已已存在,直接返回
    if (kvoClass) return kvoClass;
    
    // 中间类不存在,创建
    Class originalClass = NSClassFromString(originalClassName);
    kvoClass = objc_allocateClassPair(originalClass, kvoClassName, 0);
    
    // 重写 class 方法
    Method classMethod = class_getInstanceMethod(kvoClass, @selector(class));
    const char *types = method_getTypeEncoding(classMethod);
    // 这里确定 kvoClass 没有实现 class 这个方法,所以直接添加
    class_addMethod(kvoClass, @selector(class), (IMP)kvo_class,types);
    
    // 向 runtime 注册这个中间类
    objc_registerClassPair(kvoClass);
    
    return kvoClass;
}


Class kvo_class(id self, SEL _cmd) {
    return objc_getSuperclass(object_getClass(self));
}

动态创建新的类需要使用到 objc_runtime.h 中的 objc_allocateClassPair(Class superclass, const char *name, size_t extrayBytes) 方法。这个方法需要传入父类、名字以及额外的空间(通常为 0),返回值为创建的类。接下来可以为这个添加方法以及变量(我们仅仅只是重写了一个 class 方法)。最后,通过 objc_registerClassPair() 来告诉 Runtime 这个新类的存在。

注意 : 我们在“重写” class 方法时,我们使用了 class_addMethod() 方法 而不是 class_replaceMethod() ,更没有使用 method_exchangeImplementations()。这是因为这个新类其实本身是没有 class 方法的,它的 class 方法时从父类那里继承来的(不太明白这句话含义的朋友可以研究下 method-swizzling )。

第三步,重写 setter 方法。调用原有的 setter 方法(来自这个中间类的父类),然后通过调用传入的 block 来通知每个观察者。

先检查这个中间类是否已经实现了该属性的 setter 方法(毕竟这个属性可能不是第一次被观察了)。如果没有实现,添加就完事了。

void kvo_setter(id self, SEL _cmd, id newValue) {
    NSString *setterName = NSStringFromSelector(_cmd);
    NSString *getterName = getterForSetter(getterName);
    
    if (!getterName) {
        NSString *reson = [NSString stringWithFormat:@"Object %@ does not have setter %@", self, setterName];
        @throw [NSException exceptionWithName:NSInvalidArgumentException
                                       reason:reason
                                     userInfo:nil];
        return ;
    }
    
    id oldValue = [self valueForKey:getterName];
    
    struct objc_super superclass = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self))
    };
    
    // 将 objc_msgSendSuper 函数指针转换类型,使编译器不报错。
    /// 从 Xcode 6 开始,新的 LLVM 会对 objc_msgSendSuper 以及 objc_msgSend 做严格的类型检查
    /// 如果不转换类型,编译器会报 too many arguments 的错误。
    void (*objc_msgSendSuperCasted)(void *, SEL, id) = (void *)objc_msgSendSuper;
    
    objc_msgSendSuperCasted(self, _cmd, newValue);
    
    // 遍历所有观察者并调用其 block
    NSArray *observationInfos = objc_getAssociatedObject(self, (__bridge const void *)(kLyKVOAssociatedObservationInfos));
    for (LyObservationInfo *info in observationInfos) {
        if ([info.keyPath isEqualToString:getterName]) {
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                each.block(self, getterName, oldValue, newValue);
            });
        }
    }
}

通过 objc_msgSendSuper() 方法可以给父类发送消息,这里我们调用了父类中名为 keyPath 属性的 setter。但是,在调用之前做了一下类型转换。具体原因注释中也写明了:如果不做类型转换的话,那么 Xcode 6 之后的 LLVM 编译器将会报 too many arguments 的错误。

第四步,存储观察者信息。这里选择利用 runtime 将观察者的相关新保存在 associatedObject 中(当然也可以创建一个全局变量来管理 observer,但是这样对每次查找 observer 的性能有所影响)。我们将观察这信息封装在 LyObservationInfo 类中。

@interface LyObservationInfo : NSObject

@property (nonatomic, weak) NSObject *observer;
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, copy) LyKVOBlock block;

- (instancetype)initWithObserver:(NSObject *)observer
                         keyPath:(NSString *)keyPath
                           block:(LyKVOBlock)block;

@end


@implementation LyObservationInfo 

- (instancetype)initWithObserver:(NSObject *)observer
                         keyPath:(NSString *)keyPath
                           block:(LyKVOBlock)block 
{
    self = [super init];
    if (self) {
        _observer = observer;
        _keyPath = keyPathl
        _block = block;
    }
    return self
}

@end

注意 这里实现的 KVO 功能最好不要拿到项目中使用,毕竟只是简单实现。。。

1004 KVO 的缺点

  1. 过度依赖 String;
  2. 注册观察者的代码和属性改变的代码上下文不同,通过 void * 传递上下文;
  3. 非常容易导致 App 崩溃(不移除崩溃,多次移除也崩溃);
  4. 观察与响应太过分散;
  5. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context 很可能存在非常多的分支。。。。
  6. 需要自己处理 superclass 的 observe 事件(所以最开始的演示部分多了一个 if 分支);

1005 如何优雅地使用 KVO

介绍一下 Facebook 开源的 KVOController 。这个框架可以优雅地解决以上缺点。

仅需要如下代码即可实现 KVO:

[self.KVOController observe:self.person
                    keyPath:@"name"
                    options:NSValueObservingOptionNew | NSValueObservingOptionOld
                      block:^(id _Nullable observer, id Nonnull object, NSDictionary<NSString *, id> *_Nonnull change) { 
                        NSLog(@"%@", change);
}];

KVOController 的优点:

  1. 不需要手动移除观察者;
  2. 注册观察者与事件发生的代码上下文相同,无需跨方法传参;
  3. 使用 block 来替代方法调用;
  4. 每一个 keyPath 对应一个属性,不需要反复的 if 判断。

为 NSObject 提供 KVOController

KVOController 其实是对 KVO 的封装,其实现非常简单。KVOController 通过分类的方式提供了两个属性:KVOControllerKVOControllerNonRetaining

@interface NSObject (FBKVOController)

@property (nonatomic, strong) FBKVOController *KVOController;
@property (nonatomic, strong) FBKVOController *KVOControllerNonRetaining;

@end

从名字就可以看出来,KVOControllerNonRetaining 在使用时不会持有观察者,而 KVOController 就会持有观察者了。

如何为已有的类添加属性呢?对,就是 Objc 的 Runtime 。

- (FBKVOController *)KVOController {
    id controller = objc_getAssociatedObject(self, NSObjectKVOControllerKey);
    
    if (nil == controller) {
        controller = [FBKVOController controllerWithObserver:self];
        self.KVOController = controller;
    }
    
    return controller;
}

- (void)setKVOController:(FBKVOController *)KVOController {
    objc_setAssociatedObject(self, NSObjectKVOControllerKey, KVOController, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (FBKVOController *)KVOControllerNonRetaining {
    id controller = objc_getAssociatedObject(self, NSObjectKVOControllerNonRetainingKey);
    
    if (nil == NSObjectKVOControllerNonRetainingKey) {
        controller = [FBKVOController controllerWithObserver:self retainObserved:NO];
        self.KVOController = controller;
    }
    
    return controller;
}

- (void)setKVOControllerNonRetainng:(FBKVOController *)KVOControllerNonRetaining {
    objc_setAssociatedObject(self, NSObjectKVOControllerNonRetainingKey, KVOControllerNonRetaining, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

两个属性均通过 objc_setAssociatedObject() 方法按照指定的 key 存储,获取时也是使用了 objc_getAssociatedObject() 。不同的就是也就只有 FBKVOController 的初始化这一点。

初始化 KVOController

在分类 NSObject(FBKVOController) 中我们初始化了 KVOController,可以看到 KVOController 是 FBKVOController 的实例。作为 KVO 的管理者,他必须持有当前对象有关 KVO 的信息。

在 FBKVOController 中,存储 KVO 信息便是 NSMapTable(一个更高效的 NSDictionary,详解见 NSMapTable: 不只是一个能放weak指针的 NSDictionary)。

这里借用一下 大佬的图

FBKVOController 是线程安全的,其持有一把 pthread_mutex_t 的锁,用于在操作 _objectInfosMap 时使用。

NSObject 的属性 KVOControlerKVOControllerNonRetaining 唯一的区别就在于是否会持有观察者。

- (instancetype)initWithObserver:(nullable id)observer
                  retainObserved:(BOOL)retainObserved
{
    self = [super init];
    if (self) {
        _observer = observer;
        NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory | NSPointerFunctionsObjectPersonality: NSPointerFunctionsWeakMemory | NSPointerFunctionsObjectPersonality;
        _objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory | NSPOinterFunctionsObjectPersonality capacity:0];
        pthread_mutex_init(&_lock, null);
    }
    
    return self;
}

整理一下:

keyOptions valueOptions
KVOControler NSPointerFunctionsWeakMemory | NSPointerFunctionsObjectPersonality NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality
KVOControllerNonRetaining NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality

对比下来,两者区别在于初始化时生成 NSMapTable 实例时 keyOptions 传入的是 NSPointerFunctionsStrongMemory 还是 NSPointerFunctionsWeakMemory

KVOControler 如何运作

前边已经提过,使用 KVOController 非常简单,只需要调用 observe:keyPath:options:block: 方法即可。

去掉注释与容错处理。

- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
{
  _FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];

  [self _observe:object info:info];
}

KVOController 将观察者的一切信息都保存在一个数据结构 _FBKVOInfo 中了。

如何存储 Observatin 信息: _FBKVOInfo

再次借用大佬的图

如果查看 _FBKVOInfo 这个类的具体实现,我们发现其内部几乎全是初始化方法。但是,他重写了 -hash-isEqual: 方法。其实一看也就明白了,-hash 方便其在 NSMaptable 中的存取,-isEqual 用于判等。

另外一个就是 state 这个属性,其实主要用于协调 -observe:info-unobserve:info:。。。

如何 observe

继续用一下大佬的图

这是调用 -observer:keyPath:options:block: 方法来添加观察者的函数调用栈:

通过这张图与源码结合来看:

1、NSObject 调用 -observe:keyPath:options:block: 开始观察某个属性;

2、创建一个 _FBKVOInfo 实例来存储此次观察的信息,并调用私有方法 -_observe:info

_FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];
[self _observe:object info:info];

3、保存第二步创建的 _FBKVOInfo 实例,并调用 [[_FBKVOSharedController sharedController] observe:object info:info]

// 加锁
pthread_mutex_lock(&_lock);

NSMutableSet *infos = [_objectInfosMap objectForKey:object];

// 检查这个 info 是否存在
_FBKVOInfo *existingInfo = [infos member:info];
if (nil != existingInfo) {
    // 如果已经存在,不要再次 添加观察

    // 解锁,返回
    pthread_mutex_unlock(&_lock);
    return;
}

// 延迟创建 infos 的集合
if (nil == infos) {
  infos = [NSMutableSet set];
  [_objectInfosMap setObject:infos forKey:object];
}

// 保存 info
[infos addObject:info];

// 解锁
pthread_mutex_unlock(&_lock);

[[_FBKVOSharedController sharedController] observe:object info:info];

这里就用到了上边说过的两个方法的重写: -hash-isEqual

通过自身持有的 _objectInfosMap 来判断当前即将添加的观察者是否已经存在。如果存在,拒绝再次观察。

再次借用 大佬的图 来看一下这个 NSMapTable 类型的 _objectInfosMap 的内部结构。

4、执行私有类 _FBKVOSharedController(单例模式) 的 -observe:Info: 方法来实现真正的 KVO 注册;

- (void)observe:(id)object info:(nullable _FBKVOInfo *)info
{
    if (nil == info) {
      return;
    }

    // 添加 info
    pthread_mutex_lock(&_mutex);
    [_infos addObject:info];
    pthread_mutex_unlock(&_mutex);

    // 调用原生 KVO 添加观察者
    [object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];

    if (info->_state == _FBKVOInfoStateInitial) {
        info->_state = _FBKVOInfoStateObserving;
    } else if (info->_state == _FBKVOInfoStateNotObserving) {
        // 若 NSKeyValueObservingOptionInitial 是 NSKeyValueObservingOptions 中的一个
        // 或者 在回调中 移除观察者。
        // 此时,Foundation 的 KVO 已经将这个对象注册成为一个观察者,
        我们可以安全地移除这个观察者。
        [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
    }
}

到这里,KVO 已经注册完成

注意 : 在这里,所有观察者都变为 [_FBKVOSharedController sharedInstance] 这个单例,并且将传入的 info 作为上下文传递给了 Foudation 的 KVO。为什么这样呢?继续往下看

5、分发 Foundation 的 KVO 事件

- (void)observeValueForKeyPath:(nullable NSString *)keyPath
                      ofObject:(nullable id)object
                        change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change
                       context:(nullable void *)context
{
    _FBKVOInfo *info;

    {
        // 在 _infos 中查找 context,如果存在,带出一个强引用
        pthread_mutex_lock(&_mutex);
        info = [_infos member:(__bridge id)context];
        pthread_mutex_unlock(&_mutex);
    }
  
    if (nil != info) {

        // 从强引用中拿到 FBKVOController
        FBKVOController *controller = info->_controller;
        if (nil != controller) {

            // 从强引用中拿到 observer
            id observer = controller.observer;
            if (nil != observer) {

                // 分发自定义的 block 或者 action,默认 block
                if (info->_block) {
                    NSDictionary<NSKeyValueChangeKey, id> *changeWithKeyPath = change;
                    
                    // 如果存在多个 keyPath 通知观察,为了简洁将 keyPath 添加到 change 中
                    if (keyPath) {
                        NSMutableDictionary<NSString *, id> *mChange = [NSMutableDictionary dictionaryWithObject:keyPath forKey:FBKVONotificationKeyPathKey];
                        [mChange addEntriesFromDictionary:change];
                        changeWithKeyPath = [mChange copy];
                    }
                    info->_block(observer, object, changeWithKeyPath);
                } else if (info->_action) {
                    [observer performSelector:info->_action withObject:change withObject:object];
                } else {
                    [observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context];
                }
            }
        }
    }
}

[_FBKVOSharedController sharedInstance] 这个单例会根据 - observeValueForKeyPath:ofObject:change:context: 回调中接收到的 context 来判断到底该把这次 KVO 分发给谁。具体怎么做的呢?

info = [_infos member:(__bridge id)context];

数据结构限制了我的想象。。。。

这就拿到了我们所需要的,也就是但是注册观察者的一切信息。这样就可以准确分发了。

同时,根据注册时传入的 block / action 自动选择回调方式。

亮点来了: 为了防止注册时没有传入 block / action, 它还直接会调用 Foundation 中 KVO 提供的 -observeValueForKeyPath:ofObject:change:context: 方法。。。

再来一张 大佬的图

如何移除观察者

前边也说过了,使用 KVO 时需要我们自行移除观察者,否则很容易引起崩溃。但是 KVOController 不需要啊。

我们看一下 KVOController 内的 dealloc 方法

- (void)dealloc
{
    [self unobserveAll];
    pthread_mutex_destroy(&_lock);
}

- (void)unobserveAll
{
    [self _unobserveAll];
}

- (void)_unobserveAll
{
    // 加锁
    pthread_mutex_lock(&_lock);

    NSMapTable *objectInfoMaps = [_objectInfosMap copy];

    // 清空 _objectInfosMap
    [_objectInfosMap removeAllObjects];

    // unlock
    pthread_mutex_unlock(&_lock);

    _FBKVOSharedController *shareController = [_FBKVOSharedController sharedController];

    for (id object in objectInfoMaps) {
        // 移除所有注册过的观察者
        NSSet *infos = [objectInfoMaps objectForKey:object];
        [shareController unobserve:object infos:infos];
    }
}

再点进去看一下 _FBKVOSharedController 的 -unobserve:infos

- (void)unobserve:(id)object infos:(nullable NSSet<_FBKVOInfo *> *)infos
{
    if (0 == infos.count) {
        return;
    }

    // 移除 infos
    pthread_mutex_lock(&_mutex);
    for (_FBKVOInfo *info in infos) {
        [_infos removeObject:info];
    }
    pthread_mutex_unlock(&_mutex);

    // 从 Foundation 的 KVO 移除观察者
    for (_FBKVOInfo *info in infos) {
        if (info->_state == _FBKVOInfoStateObserving) {
            [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
        }
        info->_state = _FBKVOInfoStateNotObserving;
    }
}

对象的 KVOController 在析构时,会清空 _objectInfosMap,并交待小弟移除所有和自己相关的 KVO。 [_FBKVOSharedController sharedController] 这个小弟太辛苦了,干啥都需要它~~~

当然,我们也可以提前移除观察者,通过这两个方法:

- (void)_unobserve:(id)object info:(_FBKVOInfo *)info;
- (void)_unobserve:(id)object;

到这里,KVOController 的完整流程就整理清除了。看下来,其实就是创建了一个单例 [_FBKVOShareController sharedInstance] 来替我们作为 KVO 的观察者了,只不过这个单例很聪明,能记住我们想要的每一个操作_FBKVOInfo

参考

KVO 和 KVC 的使用和实现

关于KVO看这篇就够了

深入理解 KVO

KVO原理分析及使用进阶

如何自己动手实现 KVO

KVC 和 KVO

【深入浅出Cocoa】详解键值观察(KVO)及其实现机理

KVO Considered Harmful

KVO - 原理解析和应用

如何优雅地使用 KVO

KVOController

NSMapTable: 不只是一个能放weak指针的 NSDictionary