夜半无事--探究KVO的实现

KVO 全称是Key-Value Observing,即键值观察者。是苹果官方提供的一种事件通知机制。 键值观察提供了一种机制,该机制容许将其余对象的特定属性的更改通知对象。对于应用程序中模型层和控制器层之间的通讯特别有用。控制器对象一般观察模型对象的属性,而视图对象经过控制器观察模型对象的属性。可是,此外,模型对象能够观察其余模型对象(一般用于肯定从属值什么时候更改),甚至能够观察自身(再次肯定从属值什么时候更改)。 您能够观察属性,包括简单属性,一对一关系和一对多关系。一对多关系的观察者被告知所作更改的类型,以及更改涉及哪些对象。 KVO最大的优点在于不须要修改其内部代码便可实现监听,可是有利有弊,最大的问题也是出自这里。php

基础使用

  • 本文只说在自动观察的状况下的原理,KVO实际上有手动观察的状态,可是原理和自动观察同样,就再也不多说了。

通常状况下,咱们使用KVO有如下三种步骤:html

    1. 经过 -(void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context; 方法注册观察者,观察者能够接收keyPath属性的变化事件,而且使用context加入信息;
    1. 实现 -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context 方法,当keypath对应的元素发生变化时,会发生回调;
    1. 若是再也不须要监听,则须要使用 -(void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context; 方法来释放掉。

这里稍微提一下NSKeyValueObservingOptions的种类:git

NSKeyValueObservingOptionNew = 0x01, 提供更改前的值
NSKeyValueObservingOptionOld = 0x02, 提供更改后的值
NSKeyValueObservingOptionInitial = 0x04, 观察最初的值(在注册观察服务时会调用一次触发方法)
NSKeyValueObservingOptionPrior = 0x08 分别在值修改先后触发方法(即一次修改有两次触发)
复制代码

好比说,我建立了一个Fish类程序员

#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Fish : NSObject
@property (nonatomic,strong)NSString *color;
@property (nonatomic,strong)NSString *price;
@end
NS_ASSUME_NONNULL_END
复制代码

而后在viewController.m文件中,这样添加观察者github

self.saury = [[Fish alloc]init];
    [self.saury setValue:@"blue" forKey:@"color"];
    [self.saury addObserver:self forKeyPath:@"color" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:(__bridge void * _Nullable)([NSString stringWithFormat:@"yellow"])];
复制代码

这里我在context中加入了一个字符串,这也是KVO的一种传值方式。 接着咱们实现监听:安全

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if([keyPath isEqualToString:@"color"]) {
        NSString *str = (__bridge NSString *)(context);
        NSLog(@"___%@",str);
    }
}
复制代码

最后把它移除数据结构

-(void)dealloc {
    //移除监听
    [self.saury removeObserver:self forKeyPath:@"price" context:(__bridge void * _Nullable)([NSString stringWithFormat:@"yellow"])];
}
复制代码

看起来通常都是这么使用的。多线程

好了,到这里,就该吐槽一下KVO的不少坑爹的地方了。app

    1. 每次都必须在可靠准确的时间点手动移除观察者;
    1. 传递上下文使用context时很是别扭,由于这个是个void指针,须要神奇的桥接; 好比说我要传递一个字符串,添加观察者的时候使用 (__bridge void * _Nullable)([NSString stringWithFormat:@"yellow"]) ,而后在接收的时候,须要使用(__bridge NSString *)来转换过来。
    1. 若是有多个观察者,在手动移除的时候须要鉴别context来分别移除;
    1. addObserver和removeObserver须要是成对的,若是remove多了就会发生crash,若是少remove了,就会在再次接收到回调的时候发生crash;
    1. 一旦被观察的对象和属性不少时,就要分门别类的用if方法来分辨,代码写的奇丑无比。
    1. KVO的实现是经过setter方法,使用KVO必须调用setter,直接访问属性对象是没有用的。
    1. KVO在多线程的状况下并不安全。KVO是在setter的线程上得到通知,咱们使用的时候必定要注意线程的问题。这里是官方的解读,还有其余的文章来阐述这个事实。

固然,这个问题实际上很是广泛并且持续时间很是久,久到GUN的时代就有了,吐槽的文章也是不少,好比这个。这么多的缺点,也是各类KVO的封装,好比说KVOController诞生的主要缘由。ide

KVO实现原理

官方文档中有这样一句话。

Automatic key-value observing is implemented using a technique called 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. 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. 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. 自动键值观察是使用isa-swizzling实现的。 isa指针,顾名思义,指向对象的类,它保持一个调度表。该调度表实质上包含指向该类实现的方法的指针以及其余数据。 在为对象的属性注册观察者时,将修改观察对象的isa指针,指向中间类而不是真实类。结果,isa指针的值不必定反映实例的实际类。 您永远不要依靠isa指针来肯定类成员。相反,您应该使用该class方法来肯定对象实例的类。

配合demo代码,阐明了KVO的实现原理:

  • 当某个类的属性对象被观察的时候,系统就会在运行期动态的建立一个派生类NSKVONotifying_xx。在这个派生类中重写被观察属性的setter方法和Class方法,dealloc,_isKVO方法,而后这个isa指针指向了这个新建的类(注意!Class方法指向的仍是原有的类名)。派生类在被重写的setter方法中实现了真正的通知机制,而和原有的对象隔离开来。
  • KVO的实如今上层也依赖于 NSObject 的两个方法:willChangeValueForKey:didChangeValueForKey: 。在一个被观察属性改变以前,调用 willChangeValueForKey: 记录旧的值。在属性值改变以后调用 didChangeValueForKey:,从而 observeValueForKey:ofObject:change:context: 也会被调用。

固然,究竟是不是,看一下源码不就知道了。

查看源码

尴尬的是,在runtime的源码当中,咱们是找不到有关kvo的东西的。那么该怎么办呢? 这里要先讲一点历史了。

早在1985 年,Steve Jobs 离开苹果电脑(Apple) 后成立了NeXT 公司,并于1988 年推出了NeXT 电脑,使用NeXTStep 为操做系统。这也是如今Cocoa里面不少NS开头的类名的源头。在当时,NeXTStep 是至关先进的系统。 以Unix (BSD) 为基础,使用PostScript 提供高品质的图形界面,并以Objective-C 语言提供完整的面向对象环境。 尽管NeXT 在软件上的优异,其硬体销售成绩不佳,不久以后,NeXT 便转型为软件公司。1994 年,NeXT 与Sun(Sun Microsystem) 合做推出OpenStep 界面,目标为跨平台的面向对象程式开发环境。NeXT 接着推出使用OpenStep 界面的OPENSTEP 系统,可在Mach, Microsoft Windows NT, Sun Solaris 及HP/UX 上执行。1996 年,苹果电脑买下NeXT,作为苹果电脑下一代操做系统的基础。 OPENSTEP 系统便演进成为MacOS X 的Cocoa 环境。 在1995 年,自由软体基金会(Free Software Fundation) 开始了GNUstep 计划,目的在使用OpenStep 界面,以提供Linux/BSD 系统一个完整的程式发展环境,而GNUstep最初是GNU开发人员努力复制技术上雄心勃勃的NeXTSTEP的程序员友好功能。GNUstep是要早于Cocoa的实现的。咱们能够从GNUstep的实现代码中,来参考KVO的设计思路。 你能够点击这里来找到GNUstep的源码,或者也能够直接查看我下载下来的文件,咱们能够很惊奇的发现,至少在NSKeyValueObserving.h文件中,不少函数名是同样的。

  • 固然还有不少不一样,好比说对于context的支持就少不少,remove方法就没有支持context的函数。

1. - addObserver: forKeyPath: options: context: 的实现过程

这个方法在**NSObject (NSKeyValueObserverRegistration)**中。

- (void) addObserver: (NSObject*)anObserver
          forKeyPath: (NSString*)aPath
             options: (NSKeyValueObservingOptions)options
             context: (void*)aContext {
    GSKVOInfo             *info;
    GSKVOReplacement      *r;
    NSKeyValueObservationForwarder *forwarder;
    NSRange               dot;

    //初始化
    setup();
    //使用递归锁保证线程安全--kvoLock是一个NSRecursiveLock
    [kvoLock lock];
    // Use the original class
    //从全局NSMapTable中获取某个类的KVO子类Class
    r = replacementForClass([self class]);
    /* * Get the existing observation information, creating it (and changing * the receiver to start key-value-observing by switching its class) * if necessary. */
    //从全局NSMapTable中获取某个类的观察者信息对象,并经过改变它的类来改变接收器以开始观察关键值
    info = (GSKVOInfo*)[self observationInfo];
    //若是没有信息(不存在)就建立一个观察者信息对象实例。
 
    if (info == nil) {
        info = [[GSKVOInfo alloc] initWithInstance: self];
        //保存到全局NSMapTable中。
        [self setObservationInfo: info];
        //将被观察的对象的isa修改成新的KVO子类Class
        object_setClass(self, [r replacement]);
    }
    /* * Now add the observer. * 开始处理观察者 */
    dot = [aPath rangeOfString:@"."];
    //string里有没有.
    if (dot.location != NSNotFound) {
        //有.说明多是成员变量
        forwarder = [[NSKeyValueObservationForwarder alloc]initWithKeyPath: aPath
                                                                  ofObject: self
                                                                withTarget: anObserver
                                                                   context: aContext];
        [info addObserver: anObserver
               forKeyPath: aPath
                  options: options
                  context: forwarder];
    } else {
        //根据key 找到对应的setter方法,而后根据类型去获取GSKVOSetter类中相对应数据类型的setter方法
        [r overrideSetterFor: aPath];
        /* 这个是GSKVOInfo里的方法 * 将keyPath 信息保存到GSKVOInfo中的paths中,方便之后直接从内存中取。 */
         [info addObserver: anObserver
               forKeyPath: aPath
                  options: options
                  context: aContext];
    }
    //递归锁解锁
    [kvoLock unlock];
}
复制代码

咱们接着来分段看。

setup();

NSString *const NSKeyValueChangeIndexesKey = @"indexes";
NSString *const NSKeyValueChangeKindKey = @"kind";
NSString *const NSKeyValueChangeNewKey = @"new";
NSString *const NSKeyValueChangeOldKey = @"old";
NSString *const NSKeyValueChangeNotificationIsPriorKey = @"notificationIsPrior";

static NSRecursiveLock    *kvoLock = nil;
static NSMapTable    *classTable = 0;//NSMapTable若是对key 和 value是弱引用,当key 和 value被释放销毁后,NSMapTable中对应的数据也会被清除。
static NSMapTable    *infoTable = 0;
static NSMapTable       *dependentKeyTable;
static Class        baseClass;
static id               null;

#pragma mark----- setup
static inline void setup() {
    if (nil == kvoLock) {
        //这是一个全局的递归锁NSRecursiveLock
        [gnustep_global_lock lock];
        if (nil == kvoLock) {
            kvoLock = [NSRecursiveLock new];
            /* * NSCreateMapTable建立的是一个NSMapTable,一个弱引用key-value容器, */
            null = [[NSNull null] retain];
            classTable = NSCreateMapTable(NSNonOwnedPointerMapKeyCallBacks,
                                          NSNonOwnedPointerMapValueCallBacks, 128);
            infoTable = NSCreateMapTable(NSNonOwnedPointerMapKeyCallBacks,
                                         NSNonOwnedPointerMapValueCallBacks, 1024);
            dependentKeyTable = NSCreateMapTable(NSNonOwnedPointerMapKeyCallBacks,
                                                 NSOwnedPointerMapValueCallBacks, 128);
            baseClass = NSClassFromString(@"GSKVOBase");
        }
        [gnustep_global_lock unlock];
    }
}
复制代码

建立了classTable、infoTable、dependentKeyTable来存储类名、观察者的信息、依赖者对应的key。

[kvoLock lock];

为了保证线程安全,这里使用了递归锁。 递归锁的特色是:能够容许同一线程屡次加锁,而不会形成死锁。**递归锁会跟踪它被lock的次数。每次成功的lock都必须平衡调用unlock操做。**只有全部达到这种平衡,锁最后才能被释放,以供其它线程使用。 这个很符合咱们对于KVO的理解。

r = replacementForClass([self class]);

static GSKVOReplacement *replacementForClass(Class c) {
    GSKVOReplacement *r;
    //建立
    setup();
    //递归锁
    [kvoLock lock];
    //从全局classTable中获取GSKVOReplacement实例
    r = (GSKVOReplacement*)NSMapGet(classTable, (void*)c);
    //若是没有信息(不存在),就建立一个保存到全局classTable中
    if (r == nil) {
        r = [[GSKVOReplacement alloc] initWithClass: c];
        NSMapInsert(classTable, (void*)c, (void*)r);
    }
    //递归锁解锁
    [kvoLock unlock];
    return r;
}
复制代码

这里咱们发现了 r = [[GSKVOReplacement alloc] initWithClass: c]; 方法,它是GSKVOReplacement里的方法。它有三个成员变量。

{
    Class         original;       /* The original class 原有类*/
    Class         replacement;    /* The replacement class 替换类*/
    NSMutableSet  *keys;          /* The observed setter keys 被观察者的key*/
}
复制代码

接着往下看。

- (id) initWithClass: (Class)aClass {
    NSValue        *template;
    NSString        *superName;
    NSString        *name;
    ...
    original = aClass;
    /* * Create subclass of the original, and override some methods * with implementations from our abstract base class. * 建立原始类的子类,并使用抽象基类中的实现重写某些方法。 */
    superName = NSStringFromClass(original);
    name = [@"GSKVO" stringByAppendingString: superName];
    template = GSObjCMakeClass(name, superName, nil);
    GSObjCAddClasses([NSArray arrayWithObject: template]);
    replacement = NSClassFromString(name);
    //这个baseClass是GSKVOBase
    GSObjCAddClassBehavior(replacement, baseClass);
    /* * Create the set of setter methods overridden. * 建立重写的setter方法集。 */
    keys = [NSMutableSet new];
    return self;
}
复制代码

-(id)initWithClass:(Class)aClass 函数中,传入的原始class便是original,而原有的类名,会在前面拼接一个 "GSKVO" 字符串以后变成替代类的类名。 而经过 GSObjCAddClassBehavior 方法,则会在将GSKVOBase的方法拷贝到replacement中去。 而GSKVOBase中有什么方法呢?

- (void) dealloc;
- (Class) class;
- (Class) superclass;
- (void) setValue: (id)anObject forKey: (NSString*)aKey;
- (void) takeStoredValue: (id)anObject forKey: (NSString*)aKey;
- (void) takeValue: (id)anObject forKey: (NSString*)aKey;
- (void) takeValue: (id)anObject forKeyPath: (NSString*)aKey;
复制代码

最关键的dealloc、class、superclass、setter方法都被重写。 class、superclass方法都被加了一层class_getSuperclass,以免干扰,仍是能直接获取到正确的class名。

这里结束不谈,回 - addObserver: forKeyPath: options: context: 。 接着咱们建立观察者信息,并插入到infoTable中去。 而后经过object_setClass方法将修改class名称,将被观察的对象的isa修改成新的KVO子类Class。

if (dot.location != NSNotFound)

这里就颇有意思了,咱们须要查看,keyPath里是否是有.。 若是有.,说明多是成员变量,咱们须要递归的向下筛选。 举个🌰, 好比说,咱们要查看Computer中的成员变量NoteBook的属性brand。 你须要观察的keyPath其实是NoteBook.brand。 那咱们要先观察NoteBook的属性变化,在往下观察brand的变化。

keyForUpdate = [[keyPath substringToIndex: dot.location] copy];
remainingKeyPath = [keyPath substringFromIndex: dot.location + 1];
复制代码

而若是没有.的问题,咱们就能够根据key,直接找到对应的setter方法,-(void)overrideSetterFor函数。而后根据类型去获取GSKVOSetter类中相对应数据类型的setter方法。 好比以下代码:

- (void) setter: (void *)val {
    NSString    *key;
    Class        c = [self class];//GSKVOSetter继承的事NSObject,因此这里获取的仍是原有的父类,并未被改写
    void        (*imp)(id,SEL,void*);
    //获取真正的函数地址--原始的setter方法
    imp = (void (*)(id,SEL,void*))[c instanceMethodForSelector: _cmd];

    key = newKey(_cmd);
    if ([c automaticallyNotifiesObserversForKey: key] == YES) {
        // pre setting code here
        [self willChangeValueForKey: key];
        (*imp)(self, _cmd, val);
        // post setting code here
        [self didChangeValueForKey: key];
    } else {
        (*imp)(self, _cmd, val);
    }
    RELEASE(key);
}
复制代码

GSKVOInfo 的- addObserver: forKeyPath: options: context:

而后,咱们会发现,诶?怎么又是一个添加观察者? 这个其实是一个GSKVOInfo里的函数。 在这里建立、存储KVO的信息,并处理一些细节问题:

在上面,我特意提过NSKeyValueObservingOptions的种类。 里面有个NSKeyValueObservingOptionInitial属性,当使用它的时候,须要在注册观察服务时会调用一次触发方法。这个时候就能够直接在判断完以后调用 -observeValueForKeyPath:ofObject:change:context 方法。

2. -observeValueForKeyPath: ofObject: change: context:

这是一段很长的代码

- (void) observeValueForKeyPath: (NSString *)keyPath
                       ofObject: (id)anObject
                         change: (NSDictionary *)change
                        context: (void *)context {
  if (anObject == observedObjectForUpdate) {
      [self keyPathChanged: nil];
    } else {
      [target observeValueForKeyPath: keyPathToForward
                            ofObject: observedObjectForUpdate
                              change: change
                             context: contextToForward];
    }
}

- (void) keyPathChanged: (id)objectToObserve {
    if (objectToObserve != nil) {
        [observedObjectForUpdate removeObserver: self forKeyPath: keyForUpdate];
        observedObjectForUpdate = objectToObserve;
        [objectToObserve addObserver: self
                          forKeyPath: keyForUpdate
                             options: NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                             context: target];
    }
    if (child != nil) {
        [child keyPathChanged:
        [observedObjectForUpdate valueForKey: keyForUpdate]];
    } else {
        NSMutableDictionary *change;
        change = [NSMutableDictionary dictionaryWithObject:[NSNumber numberWithInt: 1]forKey:  NSKeyValueChangeKindKey];
        if (observedObjectForForwarding != nil) {
            id oldValue;
            oldValue = [observedObjectForForwarding valueForKey: keyForForwarding];
            [observedObjectForForwarding removeObserver: self
                                             forKeyPath:keyForForwarding];
            if (oldValue) {
                [change setObject: oldValue
                           forKey: NSKeyValueChangeOldKey];
            }
        }
        observedObjectForForwarding = [observedObjectForUpdate valueForKey:keyForUpdate];
        if (observedObjectForForwarding != nil) {
            id newValue;
            [observedObjectForForwarding addObserver: self
                                          forKeyPath: keyForForwarding
                                             options: NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                                             context: target];
            //prepare change notification
            newValue = [observedObjectForForwarding valueForKey: keyForForwarding];
            if (newValue) {
                [change setObject: newValue forKey: NSKeyValueChangeNewKey];
            }
        }
        [target observeValueForKeyPath: keyPathToForward
                              ofObject: observedObjectForUpdate
                                change: change
                               context: contextToForward];
        }
}
@end
复制代码

咱们发现,无论怎样都是要调用 - (void) keyPathChanged: ,因此能够越过observeValueForKeyPath直接来看 - (void) keyPathChanged: 函数。

- (void) keyPathChanged: (id)objectToObserve {
    if (objectToObserve != nil) {
        [observedObjectForUpdate removeObserver: self forKeyPath: keyForUpdate];
        observedObjectForUpdate = objectToObserve;
        [objectToObserve addObserver: self
                          forKeyPath: keyForUpdate
                             options: NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                             context: target];
    }
    if (child != nil) {
        [child keyPathChanged:[observedObjectForUpdate valueForKey: keyForUpdate]];
    } else {
        NSMutableDictionary *change;
        change = [NSMutableDictionary dictionaryWithObject:[NSNumber numberWithInt: 1]
                                                    forKey:NSKeyValueChangeKindKey];
        if (observedObjectForForwarding != nil) {
            id oldValue;
            oldValue = [observedObjectForForwarding valueForKey: keyForForwarding];
            [observedObjectForForwarding removeObserver: self
                                             forKeyPath:keyForForwarding];
            if (oldValue) {
                [change setObject: oldValue
                           forKey: NSKeyValueChangeOldKey];
            }
        }
        observedObjectForForwarding = [observedObjectForUpdate valueForKey:keyForUpdate];
        if (observedObjectForForwarding != nil) {
            id newValue;
            [observedObjectForForwarding addObserver: self
                                          forKeyPath: keyForForwarding
                                             options: NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                                             context: target];
            //prepare change notification
            newValue = [observedObjectForForwarding valueForKey: keyForForwarding];
            if (newValue) {
                [change setObject: newValue
                           forKey: NSKeyValueChangeNewKey];
            }
        }
        [target observeValueForKeyPath: keyPathToForward
                              ofObject: observedObjectForUpdate
                                change: change
                               context: contextToForward];
        }
}

复制代码

这段是个很长的代码,做用的将须要的数据不断的填充进应该的位置: 里面四个主要的参数,实际上就是方法 **-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> )change context:(void )context 里的数据。

3. - removeObserver: forKeyPath: context:;

这个方法则实现的简单了一些,只有基础方法,而没有根据context删除指定observer的方法,算是一个缺陷。

- (void) removeObserver: (NSObject*)anObserver forKeyPath: (NSString*)aPath {
    GSKVOInfo    *info;
    id            forwarder;
    /* * Get the observation information and remove this observation. */
    info = (GSKVOInfo*)[self observationInfo];
    forwarder = [info contextForObserver: anObserver ofKeyPath: aPath];
    [info removeObserver: anObserver forKeyPath: aPath];
    if ([info isUnobserved] == YES) {
        /* * The instance is no longer being observed ... so we can * turn off key-value-observing for it. * 实例再也不被观察。。。因此咱们能够关闭它的键值观测。 */
        //修改对象所属的类 为新建立的类
        object_setClass(self, [self class]);
        IF_NO_GC(AUTORELEASE(info);)
        [self setObservationInfo: nil];
    }
    if ([aPath rangeOfString:@"."].location != NSNotFound)
        [forwarder finalize];
}
复制代码

这里实际上就是添加观察者的反过程,不过多的说明。

另外,由于并无 **- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString )keyPath context:(nullable void )context 方法的实现,我猜想了一下可能的实现。

  • 对于infoTable可能设计的更加复杂,可使用context做为key来添加和删除相同的被观察者的实例,即便是同一个被观察者对象,也能够经过context来建立不一样的被观察实例。

题外话

有个老哥本身根据反汇编写了一个KVC、KVO的实现,代码地址在这里,在表现形式上已经和原生的KVO差很少了。不过做者使用的依然是Dictionary而非NSMapTable;锁使用的是pthread_mutex_t互斥锁以及OSSpinLockLock自旋锁,而非NSRecursiveLock递归锁。不过写到这个已经很不错了。

关于KVOController

KVO在使用上有各类各样的问题,有一种比较好的解决办法就是使用Facebook的KVOController。 咱们就能够写成这样。

[self.KVOController observe:clock keyPath:@"date" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew action:@selector(updateClockWithDateChange:)];
复制代码

而且带来了不少好处:

  1. 再也不关心释放的问题,其实是很是有效而且安全。
  2. 直接使用keypath来对应属性,就再也不须要屡次的if判断,即便是多个观察者;
  3. 使用 block 来提高使用 KVO 的体验;

它的实现其实蛮简单的。刨除头文件,主要有4个文件。

  • NSObject+FBKVOController.h
  • NSObject+FBKVOController.m
  • FBKVOController.h
  • FBKVOController.m

分别来看,NSObject+FBKVOController里的 KVOControllerNonRetaining 这个元素并不会持有被观察的对象,有效的防止循环引用;而KVOController仍是会形成循环引用。 而它们的区别在于初始化传入的retianObserved的不一样。

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

在这里,生成持有者信息的时候会有个判断,持有对象传入的是 NSPointerFunctionsStrongMemory ,不止有对象的是 NSPointerFunctionsWeakMemory 。

主要的代码都在FBKVOController.m中。

FBKVOController

这里,咱们能够发现,这里有一个NSMapTable类型的_objectInfosMap,和上面的相似的map起到了相似的做用--用来存储当前对象持有者的相关信息。 而为了线程安全,这里使用了pthread_mutex_t,一个互斥锁。

  • _objectInfosMap
  • _lock

仍是从观察开始看

- (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];
}
复制代码

这里有个数据结构:_FBKVOInfo在上面也有相似的实现,用于存储全部有关的信息。这里就很少说了。 接着看关键的一个私有方法。

- (void)_observe:(id)object info:(_FBKVOInfo *)info {
  // lock
  pthread_mutex_lock(&_lock);
  NSMutableSet *infos = [_objectInfosMap objectForKey:object];
  // check for info existence
  _FBKVOInfo *existingInfo = [infos member:info];
  if (nil != existingInfo) {
    // observation info already exists; do not observe it again
    // unlock and return
    pthread_mutex_unlock(&_lock);
    return;
  }
  // lazilly create set of infos
  if (nil == infos) {
    infos = [NSMutableSet set];
    [_objectInfosMap setObject:infos forKey:object];
  }
  // add info and oberve
  [infos addObject:info];
  // unlock prior to callout
  pthread_mutex_unlock(&_lock);
  [[_FBKVOSharedController sharedController] observe:object info:info];
}
复制代码

这里经过_objectInfosMap来判断当年的对象信息是否已经注册过。 而后处理一次InfosMap以后,会接着调用_FBKVOSharedController的单例方法。

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

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

  [object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];
  if (info->_state == _FBKVOInfoStateInitial) {
    info->_state = _FBKVOInfoStateObserving;
  } else if (info->_state == _FBKVOInfoStateNotObserving) {
    [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
  }
}
复制代码

而在整个流程中,只会有一个_FBKVOSharedController单例。 而这个方法才会调用原生的KVO方法。

- (void)observeValueForKeyPath:(nullable NSString *)keyPath
                      ofObject:(nullable id)object
                        change:(nullable NSDictionary<NSString *, id> *)change
                       context:(nullable void *)context {
    _FBKVOInfo *info;
    pthread_mutex_lock(&_mutex);
    info = [_infos member:(__bridge id)context];
    pthread_mutex_unlock(&_mutex);

    FBKVOController *controller = info->_controller;
    id observer = controller.observer;

    if (info->_block) {
        NSDictionary<NSString *, id> *changeWithKeyPath = 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];
    }
}
复制代码

这里咱们能够发现,最后其实是经过_KVOInfo里的context来判断不一样的KVO方法。

removeObserver

移除观察者的策略比较简单明了。

- (void)unobserve:(id)object infos:(nullable NSSet<_FBKVOInfo *> *)infos {
  pthread_mutex_lock(&_mutex);
  for (_FBKVOInfo *info in infos) {
    [_infos removeObject:info];
  }
  pthread_mutex_unlock(&_mutex);

  for (_FBKVOInfo *info in infos) {
    if (info->_state == _FBKVOInfoStateObserving) {
      [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
    }
    info->_state = _FBKVOInfoStateNotObserving;
  }
}
复制代码

遍历这里的_FBKVOInfo,从其中取出 keyPath 并将 _KVOSharedController 移除观察者。

KVOController总结

KVOController实际上是用本身的方法,在原生KVO上又包了一层,用于自动处理,并不须要咱们来处理移除观察者,大大下降了出错的状况。

结论

  1. 能别用KVO就别用了,notification难道很差吗?一样是一对多,并且notification并不局限于属性的变化,各类各样状态的变化也均可以监听。
  2. 实在要用直接用KVOController吧。

ps:看完KVO其实比较无趣,由于你会发现KVO其实有很多优秀的替代者,研究得出了不要用的结论确实有点沮丧,也显得研究并无啥意义。 可是确实有趣啊,哈哈。

引用

Key-Value Observing Programming Guide
Observers and Thread Safety

相关文章
相关标签/搜索