The full name of KVO is key-value Observing, namely, key-value observer. Is an event notification mechanism provided by Apple. Key-value observation provides a mechanism that allows an object to be notified of changes to specific properties of other objects. This is especially useful for communication between the model layer and the controller layer in an application. Controller objects typically observe the properties of model objects, while view objects observe the properties of model objects through controllers. In addition, however, model objects can observe other model objects (typically to determine when a dependent value changes) and even themselves (again to determine when a dependent value changes). You can look at properties, including simple properties, one-to-one relationships, and one-to-many relationships. The observer of a one-to-many relationship is informed of the type of change made and which objects are involved in the change. The biggest advantage of KVO is that it does not need to modify its internal code to achieve listening, but there are pros and cons, the biggest problem is also from here.

Based on using

  • This article only said in the case of automatic observation principle, KVO actually has manual observation state, but the principle and automatic observation, no more.

In general, we use KVO in the following three steps:

    1. through-(void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;Method to register an observer, who can receive changes to the keyPath property and add information using the context;
    1. implementation-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)contextMethod, a callback occurs when the corresponding keypath element changes;
    1. If listening is no longer needed, this parameter is required-(void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;Method to release.

Here a little mention of NSKeyValueObservingOptions categories:

NSKeyValueObservingOptionNew = 0x01To provide value NSKeyValueObservingOptionOld = before the change0x02To provide the changed value NSKeyValueObservingOptionInitial =0x04, observing the initial value (in the registered monitoring service called when a trigger method) NSKeyValueObservingOptionPrior =0x08Firing methods before and after value changes (that is, firing twice with one change)Copy the code

For example, I created a Fish class

#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
Copy the code

Then in the viewController.m file, add the observer like this

    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"])];
Copy the code

So here I’m putting a string in the context, which is another way of passing values in KVO. Next we implement listening:

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

And then we remove it

- (void)dealloc {
    // Remove the listener
    [self.saury removeObserver:self forKeyPath:@"price" context:(__bridge void * _Nullable)([NSString stringWithFormat:@"yellow"])];
}
Copy the code

That seems to be the way it’s usually used.

Well, here, it’s time to make fun of KVO’s many pit dad’s place.

    1. Each time the observer must be removed manually at a reliable and accurate point in time;
    1. Passing context is very awkward when you use context, because this is a void pointer, which requires some magic bridging; Let’s say I want to pass a string that I use when I add an observer(__bridge void * _Nullable)([NSString stringWithFormat:@"yellow"])And then need to be used when receiving(__bridge NSString *)Let’s switch over.
    1. If you have multiple observers, you need to identify the context when manually removing it.
    1. AddObserver and removeObserver need to be paired. If there are too many removals, crash will occur; if there are too few removals, crash will occur when the callback is received again.
    1. Once there are many objects and attributes to be observed, it is very ugly to use the if method to classify them.
    1. KVO is implemented through setter methods. With KVO, setters must be called. Direct access to property objects is useless.
    1. KVO is not secure in the case of multiple threads. KVO is notified on the thread of the setter, so we must be aware of thread issues when using it. Here’s the official interpretation, as well as other articles illustrating the fact.

Of course, this problem is actually very common and has been going on for a long time, dating back to the time of GUN, and there have been a lot of funny articles, like this one. These shortcomings are also the main reason that various KVO packages, such as KVOController, are born.

KVO implementation principle

There is a sentence in the official documentation.

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. Automatic key observation is implemented using ISA-Swizzling. The ISA pointer, as the name implies, points to the class of objects that hold a schedule. The schedule table essentially contains Pointers to the methods implemented by the class, as well as other data. When registering an observer for an object’s properties, the ISA pointer to the observed object is modified to point to the intermediate class instead of the real 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 isa Pointers to determine class members. Instead, you should use the class method to determine the class of an object instance.

With the demo code, the realization principle of KVO is illustrated:

  • When a class property object is observed, a derived class NSKVONotifying_xx is dynamically created at runtime. Override setter and Class methods, dealloc, _isKVO, and isa Pointers to the newly created Class in the derived Class. The Class method refers to the same Class name. Derived classes implement the true notification mechanism in the overridden setter methods, isolated from the original object.
  • The implementation of KVO at the upper level also relies on two methods of NSObject: willChangeValueForKey: and didChangeValueForKey:. Before an observed property changes, call willChangeValueForKey: record the old value. After attribute value change call didChangeValueForKey: to observeValueForKey: ofObject: change: context: will be invoked.

Of course, in the end is not, take a look at the source code will not know.

View the source code

Embarrassingly, kVO is nowhere to be found in the Runtime source code. So what to do? A little bit of history to start with.

Back in 1985, Steve Jobs left Apple to form the company NeXT. In 1988, he launched the NeXT computer, using NeXTStep as an operating system. This is also the origin of many NS class names in Cocoa today. At the time, NeXTStep was a fairly advanced system. Based on Unix (BSD), PostScript provides a high-quality graphical interface and a complete object-oriented environment in Objective-C. Despite its excellence in software, NeXT failed to sell hardware, and soon turned itself into a software company. In 1994, NeXT partnered with Sun(Sun Microsystem) to launch the OpenStep interface, which was intended to be a cross-platform object-oriented programming environment. NeXT followed with an OpenStep system using the OpenStep interface, which ran on Mach, Microsoft Windows NT, Sun Solaris, and HP/UX. In 1996, Apple bought NeXT as the basis for the NeXT generation of Apple’s operating system. The OPENSTEP system evolved into a Cocoa environment for MacOS X. In 1995, the Free Software Foundation started the GNUstep project to provide a complete programming environment for Linux/BSD systems using the OpenStep interface. And GNUstep began as an effort by GNU developers to replicate the technically ambitious programme-friendly features of NeXTSTEP. GNUstep predates the Cocoa implementation. We can from the implementation of GNUstep code, to reference KVO design ideas. You can click here to find the source code for GNUstep, or you can look directly at the file I downloaded, and you can be surprised to find that, at least in the NSKeyValueObserving. H file, many of the function names are the same.

  • Of course there are a lot of differences, for example there’s a lot less support for context, there’s no function for remove that supports context.

1. -addobServer: forKeyPath: options: context

This method NSObject in * * * * (NSKeyValueObserverRegistration).

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

    / / initialization
    setup();
    // Use recursive locks for thread safety --kvoLock is an NSRecursiveLock
    [kvoLock lock];
    // Use the original class
    // Get the KVO subclass of a Class from the global NSMapTable
    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. */
    // Get the observer information object of a class from the global NSMapTable and change the receiver by changing its class to start observing key values
    info = (GSKVOInfo*)[self observationInfo];
    // Create an observer information object instance if no information exists.
 
    if (info == nil) {
        info = [[GSKVOInfo alloc] initWithInstance: self];
        // Save to the global NSMapTable.
        [self setObservationInfo: info];
        // Change the isa of the observed object to a new KVO subclass
        object_setClass(self, [r replacement]);
    }
    /* * Now add the observer. */
    dot = [aPath rangeOfString:@"."];
    // There is no such thing in string.
    if(dot.location ! = NSNotFound) {// There is a member variable
        forwarder = [[NSKeyValueObservationForwarder alloc]initWithKeyPath: aPath
                                                                  ofObject: self
                                                                withTarget: anObserver
                                                                   context: aContext];
        [info addObserver: anObserver
               forKeyPath: aPath
                  options: options
                  context: forwarder];
    } else {
        // Find the corresponding setter method based on the key, and then get the corresponding setter method based on the type of GSKVOSetter class
        [r overrideSetterFor: aPath];
        * Save the keyPath information to paths in GSKVOInfo for later retrieval directly from memory. * /
         [info addObserver: anObserver
               forKeyPath: aPath
                  options: options
                  context: aContext];
    }
    // Recursive lock unlock
    [kvoLock unlock];
}
Copy the code

Let’s do it in sections.

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 If the key and value are weak references, after the key and value are released and destroyed, the corresponding data in NSMapTable will also be deleted.
static NSMapTable    *infoTable = 0;
static NSMapTable       *dependentKeyTable;
static Class        baseClass;
static id               null;

#pragma mark----- setup
static inline void
setup(a) {
    if (nil == kvoLock) {
        // This is a global recursive lock NSRecursiveLock
        [gnustep_global_lock lock];
        if (nil == kvoLock) {
            kvoLock = [NSRecursiveLock new];
            /* * NSCreateMapTable creates an NSMapTable, a weak reference key-value container, */
            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]; }}Copy the code

ClassTable, infoTable, and dependentKeyTable are created to store the class name, observer information, and dependent key.

[kvoLock lock];

To ensure thread-safety, recursive locking is used. Recursive locking allows the same thread to lock more than once without causing a deadlock. ** A recursive lock keeps track of how many times it is locked. Each successful lock must balance calls to unlock. ** Only when this balance is reached can the lock finally be released for use by other threads. This fits well with our understanding of KVO.

r = replacementForClass([self class]);

static GSKVOReplacement *replacementForClass(Class c) {
    GSKVOReplacement *r;
    / / create
    setup();
    / / recursive locking
    [kvoLock lock];
    // Get the GSKVOReplacement instance from the global classTable
    r = (GSKVOReplacement*)NSMapGet(classTable, (void*)c);
    // If no information exists, create one and store it in the global classTable
    if (r == nil) {
        r = [[GSKVOReplacement alloc] initWithClass: c];
        NSMapInsert(classTable, (void*)c, (void*)r);
    }
    // Recursive lock unlock
    [kvoLock unlock];
    return r;
}
Copy the code

Here we find r = [[GSKVOReplacement Alloc] initWithClass: c]; Method, which is a method in GSKVOReplacement. It has three member variables.

{
    Class         original;       /* The original class */
    Class         replacement;    /* The replacement class */
    NSMutableSet  *keys;          /* The setter keys of The observed */
}
Copy the code

And then we look down.

- (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);
    // This baseClass is GSKVOBase
    GSObjCAddClassBehavior(replacement, baseClass);
    /* * Create the setter methods overridden. * /
    keys = [NSMutableSet new];
    return self;
}
Copy the code

In the -(id)initWithClass:(Class)aClass function, the original Class passed in is original, and the original Class name becomes the name of the replacement Class after concatenating a “GSKVO” string. With the GSObjCAddClassBehavior method, the GSKVOBase method is copied into replacement. What are the methods in 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;
Copy the code

The most critical dealloc, class, superclass, and setter methods have been overridden. The class and superclass methods are added with a class_getSuperclass layer to avoid interference and still get the correct class name.

To end this, go back to -addobServer: forKeyPath: options: context:. Next we create the observer information and insert it into the infoTable. The class name is then changed using the object_setClass method, changing the ISA of the observed object to the new KVO subclass class.

if (dot.location ! = NSNotFound)

Now, this is interesting, because we need to see if keyPath has… If there is., it could be a member variable, and we need to recursively filter down. For example 🌰, let’s look at the brand attribute of the NoteBook member variable in Computer. The keyPath you need to look at is actually NoteBook. Brand. Let’s look at the NoteBook properties and then look at the brand properties.

keyForUpdate = [[keyPath substringToIndex: dot.location] copy];
remainingKeyPath = [keyPath substringFromIndex: dot.location + 1];
Copy the code

And if we don’t have a problem with the key, then we can just find the corresponding setter method, the -(void)overrideSetterFor function. Then get the setter method of the corresponding data type in the GSKVOSetter class according to the type. For example:

- (void) setter: (void *)val {
    NSString    *key;
    Class        c = [self class];//GSKVOSetter inherits NSObject, so it's still getting the parent class, not overwritten
    void        (*imp)(id,SEL,void*);
    // Get the real function address -- the original setter method
    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);
}
Copy the code

GSKVOInfo -addobServer: forKeyPath: options: context:

And then we find, eh? How is another add observer? This is actually a function in GSKVOInfo. This is where KVO information is created, stored, and handled in some detail:

In the above, I specially mentioned NSKeyValueObservingOptions species. Inside a NSKeyValueObservingOptionInitial attributes, when use it, need in registration service called when a trigger method. The -ObservevalueForkeypath: ofObject: change: Context method can be called directly after the evaluation.

2. -observeValueForKeyPath: ofObject: change: context:

This is a long piece of code

- (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
Copy the code

We find that the – (void) keyPathChanged: function is called anyway, so we can look directly over the observeValueForKeyPath – (void) keyPathChanged: function.

- (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]; }}Copy the code

This is a long piece of code that continuously fills the required data into the desired location: The four main parameters, This is actually the method **-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object Change :(NSDictionary

)change context:(void) data in context.
,id>

3. – removeObserver: forKeyPath: context:;

The implementation of this method is simpler, with only the base method and no method specifying an observer according to the context, which is a defect.

- (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. So we can turn off the key observation. * /
        // Change the class of the object to the newly created class
        object_setClass(self, [self class]);
        IF_NO_GC(AUTORELEASE(info);)
        [self setObservationInfo: nil];
    }
    if ([aPath rangeOfString:@"."].location ! = NSNotFound) [forwarder finalize]; }Copy the code

This is actually the reverse process of adding an observer, without much explanation.

In addition, **- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString)keyPath context:(nullable void) context I made a guess at the possible realization.

  • For infoTable, the design can be more complex. Context can be used as a key to add and remove instances of the same observed object. Context can also be used to create different instances of the same observed object.

digression

There is an old brother himself according to the disassembly to write a KVC, KVO implementation, code address here, in the form of expression and the original KVO almost. However, the author still uses Dictionary instead of NSMapTable; Locks use the pthread_mutex_T mutex and the OSSpinLockLock spin lock instead of the NSRecursiveLock recursive lock. But that’s good enough to write about.

About KVOController

KVO has various problems in use, and a better solution is to use Facebook’s KVOController. So we could write it like this.

[self.KVOController observe:clock keyPath:@"date" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew action:@selector(updateClockWithDateChange:)];
Copy the code

And has brought many benefits:

  1. It’s actually quite effective and safe to stop worrying about release.
  2. Using keypath directly for attributes eliminates the need for multiple if judgments, even for multiple observers;
  3. Use blocks to improve the KVO experience;

The implementation is actually quite simple. Excluding header files, there are mainly four files.

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

Respectively, in the NSObject + FBKVOController KVOControllerNonRetaining this element does not hold the observed object, effectively prevent the circular reference; KVOController still makes circular references. And the difference between them is the difference of initialization passed in 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;
}
Copy the code

Generate holders information in here, there will be a judge, hold the incoming object is NSPointerFunctionsStrongMemory, not only have object is NSPointerFunctionsWeakMemory.

The main code is in fbkvoController.m.

FBKVOController

Here, we can see that there is a _objectInfosMap of type NSMapTable that serves a similar purpose — storing information about the current owner of the object. For thread safety, pthread_mutex_t, a mutex, is used.

  • _objectInfosMap
  • _lock

Let’s start with observation

- (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]; }Copy the code

There is a data structure: _FBKVOInfo has a similar implementation above, which stores all the relevant information. I won’t go into that here. Now look at the key private method.

- (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];
}
Copy the code

The _objectInfosMap command is used to check whether the object information of the current year has been registered. After InfosMap is processed once, the singleton method of _FBKVOSharedController is called.

- (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]; }}Copy the code

There is only one _FBKVOSharedController singleton in the entire process. This method calls the native KVO method.

- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSString  *, id> *)change context:(nullablevoid *)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]; }}Copy the code

Here we can see that in the end the different KVO methods are actually determined by the context in _KVOInfo.

removeObserver

The strategy for removing observers is straightforward.

- (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; }}Copy the code

Iterate over _FBKVOInfo here, take keyPath from it and remove _KVOSharedController from the observer.

KVOController summary

KVOController takes its own approach and wraps a layer on top of the native KVO for automatic processing without requiring us to remove the observer, greatly reducing the error rate.

conclusion

  1. Don’t use KVO if you can. Isn’t notification good? It is also one-to-many, and notification is not limited to property changes. It can also listen for all kinds of state changes.
  2. Use KVOController if you need it.

Ps: After watching KVO, it is not interesting because you will find that there are many excellent alternatives to KVO. It is a bit depressing that the study concluded that it should not be used, which also makes the study meaningless. But it was fun, haha.

reference

Key-Value Observing Programming Guide

Observers and Thread Safety