preface

FBKVOController is an encapsulation of the system’s KVO, which realizes self-destruction through rational use of the object’s declaration cycle, and reasonably preserves the relationship between observer and observed

Before exploring him once, we need to review some basic knowledge of KVO. The method of adding listener is described as follows:

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

In this method, a listener object (observer) can be added to the observer. A key value of the observer can be observed through the observer. Therefore, the relationship between the observer and the observed can be elicited

1. When observed by an observer, multiple key values and callbacks can be observed

2. Multiple observed objects can be observed by the same object observer, so each observer can listen to multiple properties of multiple observed objects

3. Each observer can add more than one observer. Therefore, when the observer is destroyed, it cannot remove all the listeners of the observer, but only the listeners of the observer

Its relationship is shown in the figure below:

FBKVOController inquiry

FBKVOController has three core classes _FBKVOInfo, _FBKVOSharedController, and FBKVOController

FBKVOController is usually used in the following way, that is, the class of the observer holds the KVOControler object. Therefore, when the observed object is released, the KVOController object will also be released and its dealloc callback will be called, in which the listener associated with it can be removed

// create KVO controller with observer
self.KVOController = = [FBKVOController controllerWithObserver:self];

// observe clock date property
[self.KVOController observe:clock keyPath:@"date" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew block:^(ClockView *clockView, Clock *clock, NSDictionary *change) {

}];
Copy the code

The following steps unravel the principles of FBKVO by introducing three core classes

_FBKVOInfo

As shown in the source code, _FBKVOInfo defines the following parameters:

_controller: KVOController held by the observer to check whether the object has been freed

_keyPath: view key value

_options, _context: Enumeration types and _context passed through addObserver

_action, _block: callback when KVO is triggered

_state: Enumeration state of _FBKVOInfo

We can guess that KVOInfo is the carrier of a listener. Refer to the top figure, which is the basic carrier of the many-to-many relationship between the observer and the observed trip, that is, it has the necessary callback method and the corresponding keyPath in it

{
@public

  __weak FBKVOController *_controller;
  NSString *_keyPath;
  NSKeyValueObservingOptions _options;
  SEL _action;
  void *_context;
  FBKVONotificationBlock _block;
  _FBKVOInfoState _state;
}
Copy the code

And it overrides the hash and isEqual methods, so that KVOInfo with the same keyPath is treated as the same in set.

- (NSUInteger)hash { return [_keyPath hash]; } - (BOOL)isEqual:(id)object { if (nil == object) { return NO; } if (self == object) { return YES; } if (! [object isKindOfClass:[self class]]) { return NO; } return [_keyPath isEqualToString:((_FBKVOInfo *)object)->_keyPath]; }Copy the code

FBKVOController and add listener

FBKVOController is the observer-held property object KVOController, which is the listener module object independent of the caller, and also the entry object of the class we use, through which we can actively observe objects, just like the use case mentioned earlier, It is released when the holder (the object that called the observer method) is released

Referring to the top figure, FBKVOController is actually a one-to-many relationship between one observer and multiple observed observers

The FBKVOController member variables look like this:

{// The observer will observe multiple objects (the observed object), NSMapTable<id, NSMutableSet<_FBKVOInfo *> *> *_objectInfosMap; NSMutableSet<_FBKVOInfo *> *> *. Pthread_mutex_t _lock; pthread_mutex_t lock; / /}Copy the code

The release method provided by the system for us to observe the callback as a block for example

Observers hold FBKVOController object, in order to facilitate the following the release of the observer and the release of the self. The KVOController = = [FBKVOController controllerWithObserver: self]. // Add listener [self.KVOController observe:clock keyPath:@"date" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew block:^(ClockView *clockView, Clock *clock, NSDictionary *change) { }];Copy the code

Taking the block callback method above as an example (sel has the same logic), let’s see what it does internally

- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block { NSAssert(0 ! = keyPath.length && NULL ! = block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPath, block); if (nil == object || 0 == keyPath.length || NULL == block) { return; } // create _FBKVOInfo, It saves the corresponding keyPath and callback methods _FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block]; // Enable observation [self _observe:object info:info]; }Copy the code

After the above method is called, the _observe: info method is called, which is still the object method of FBKVOController. It checks whether the observed basic carrier already exists (that is, whether the observed key is already monitored by the object), and the code is as follows

- (void)_observe:(id)object info:(_FBKVOInfo *)info {// lock lock synchronization to avoid data read/write errors caused by multi-threaded read/write pthread_mutex_lock(&_lock); //_objectInfosMap is stored in the set object whose key is the object. NSMutableSet *infos = [_objectInfosMap objectForKey:object]; NSMutableSet *infos = [_objectInfosMap objectForKey:object]; // check for info existence // check for info existence // Check for info existence // Check for info existence // Check for info existence _FBKVOInfo *existingInfo = [infos member:info]; // Already observed by the current object, end 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); unlock prior to callout pthread_mutex_unlock(&_lock); [[_FBKVOSharedController sharedController] observe:object info:info]; }Copy the code

Okay, so that’s where the _FBKVOSharedController comes in, and we’re not going to look at how it’s removed, but what does _FBKVOSharedController do

_FBKVOSharedController and add listener

_FBKVOSharedController is a singleton that holds all the basic vectors (a many-to-many set of basic vectors for observer and observed travel) through the NSHashMap object, as shown in the top figure

Note: It is a proxy for all FBKVOSharedController objects, through which the FBKVOSharedController calls the system watch method to implement listening and remove listening

The _FBKVOSharedController object is shown below

{/ / save all the basic carrier of hash table, and pay attention to its set to NSPointerFunctionsWeakMemory attribute, / / when the object is destroyed inside _FBKVOInfo monomer, Will remove the hashTable NSHashTable<_FBKVOInfo *> *_infos; pthread_mutex_t _mutex; } // singleton + (instanceType)sharedController {static _FBKVOSharedController *_controller = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _controller = [[_FBKVOSharedController alloc] init]; }); return _controller; }Copy the code

Through the add listener method of FBKVOController above, we get to the actual listener method of _FBKVOSharedController, which calls the system listener, meaning that all observations are actually added and called back in this class

Add listening methods:

- (void)observe:(id)object info:(nullable _FBKVOInfo *)info { if (nil == info) { return; } // register info to add info to hashTable to use pthread_mutex_lock(&_mutex) for call-back; [_infos addObject:info]; pthread_mutex_unlock(&_mutex); // add observer The callback object is _FBKVOSharedController singlecase object [object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void) *)info]; // Adjust the info state and remove the info when it is not being observed. A given enumeration values for NSKeyValueObservingOptionInitial, If (info->_state == _FBKVOInfoStateInitial) {info->_state = _FBKVOInfoStateObserving; } else if (info->_state == _FBKVOInfoStateNotObserving) { // this could happen when `NSKeyValueObservingOptionInitial` is one of the NSKeyValueObservingOptions, // and the observer is unregistered within the callback block. // at this time the object has been registered as an observer (in Foundation KVO), // so we can safely unobserve it. [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info]; }}Copy the code

The system’s default listening callback

- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context { NSAssert(context, @"missing context keyPath:%@ object:%@ change:%@", keyPath, object, change); // Check whether the corresponding info exists. If not, end _FBKVOInfo *info; { // lookup context in registered infos, taking out a strong reference only if it exists pthread_mutex_lock(&_mutex); info = [_infos member:(__bridge id)context]; pthread_mutex_unlock(&_mutex); } if (nil ! = info) { // take strong reference to controller FBKVOController *controller = info->_controller; // Check if KVOController exists, if it does not exist, it has been destroyed. = controller) { // take strong reference to observer id observer = controller.observer; // Call back block and action if (nil! = observer) { // dispatch custom block or action, fall back to default action if (info->_block) { NSDictionary<NSKeyValueChangeKey, id> *changeWithKeyPath = change; // add the keyPath to the change dictionary for clarity when mulitple keyPaths are being observed 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) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks"  [observer performSelector:info->_action withObject:change withObject:object]; #pragma clang diagnostic pop } else { [observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context]; } } } } }Copy the code

Remove the monitor

As mentioned earlier, the FBKVOController is destroyed along with the caller’s destruction, so when it is destroyed, its dealloc violation is called, which requires that the object actively remove all listeners to be observed

The dealloc method of FBKVOController actually calls the _unobserveAll method to remove all of the _FBKVOInfo information

Note: When _FBKVOInfo is destroyed, the _FBKVOInfo in _FBKVOController is also removed

Since _FBKVOController is actually a proxy listener for FBKVOController, the addition and removal of listeners are also done in this object

- (void)_unobserveAll { // lock pthread_mutex_lock(&_lock); NSMapTable *objectInfoMaps = [_objectInfosMap copy]; NSMapTable *objectInfoMaps = [_objectInfosMap copy]; // clear table and map // remove all base carriers from _objectInfosMap, At the same time, the corresponding carrier in _FBKVOSharedController is removed [_objectInfosMap removeAllObjects]; // unlock pthread_mutex_unlock(&_lock); _FBKVOSharedController *shareController = [_FBKVOSharedController sharedController]; // Inside _FBKVOSharedController, For (id object in objectInfoMaps) {// unobserve each registered object and infos NSSet *infos = [objectInfoMaps objectForKey:object]; [shareController unobserve:object infos:infos]; }}Copy the code

In _FBKVOSharedController, to avoid other problems, it actively removes all the basic carriers and adjust the observing state of the carriers to avoid security problems caused by multithreading. The implementation method is as follows

- (void)unobserve:(id)object infos:(nullable NSSet<_FBKVOInfo *> *)infos { if (0 == infos.count) { return; Pthread_mutex_lock (&_mutex); for (_FBKVOInfo *info in infos) { [_infos removeObject:info]; } pthread_mutex_unlock(&_mutex); // remove observer // For (_FBKVOInfo *info in infos) {if (info->_state == _FBKVOInfoStateObserving) {[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info]; } info->_state = _FBKVOInfoStateNotObserving; }}Copy the code

So far it has been introduced, the other methods are basically written in accordance with this method, not here

conclusion

1. The side effect of adding functionality to a class and actively removing it when the class is destroyed is that the functionality can be isolated as properties, making full use of the lifecycle to solve the problem

2. The proper use of NSHashTable and NSMapTable can avoid many problems

3. A full understanding of the logical relationship chain generated in functional modules (many-to-many relationship between observer and observed, one-to-many relationship between one observer and multiple observed) may encapsulate more widely used and better APIS

4. The use of lock here is to ensure the security of public data read and write (avoid read error information), unnecessary functions do not need to lock