FBKVOController source

FBKVOController

advantages

  • Avoid repeated add, repeated remove mismatch problem
  • Support Block,SEL, write code more convenient and fast
  • The observer is released early, causing flashback problems

disadvantages

  • Specification requirements need to be made in the project, otherwise there will be two ways: KVO and FBKVOController
  • Bring extra memory consumption, NSMapTable*objectInfosMap, _FBKVOSharedController *sharedController, these consumption is acceptable, better than the online flash back, of course, can also Hook system method to solve the flash back problem.

structure

  • NSObject+FBKVOController
  • _FBKVOInfo
  • FBKVOController
  • _FBKVOSharedController

Logical diagram

NSObject+FBKVOController

  1. This is a class of NSObject, and any object that inherits from NSObject can use it.
  2. callself.KVOControllerorself.KVOControllerNonRetainingWill generate aFBKVOControllerInstance objects. The difference between instance objects isFBKVOControllerThe inside of theNSMapTable*objectInfosMapAt initializationkeyOptionsThe different. If YES, the agent will hold objects internally, equivalent to Strong; if NO, equivalent to Weak.
  3. Will generate part 2self.KVOControllerObject, which is saved by association operations.
@property (nonatomic, strong) FBKVOController *KVOController;
@property (nonatomic, strong) FBKVOController *KVOControllerNonRetaining;
Copy the code
  FBKVOController * controller = [FBKVOController controllerWithObserver:self retainObserved:YES/NO];
  objc_setAssociatedObject(self, Key, KVOController, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
Copy the code

_FBKVOInfo

Wrap the parameters for FBKVOController *KVOController to call the observe method, including the following:

  • __weak FBKVOController *_controller;
  • _keyPath
  • _options
  • _FBKVOInfoState _state, default is _FBKVOInfoStateInitial, When in the state = _FBKVOInfoStateObserving, remove when the observer state = _FBKVOInfoStateNotObserving
  • _block,_action, void *_context, either all empty, or only one of them has a value.
@interface _FBKVOInfo : NSObject
{
@public
  __weak FBKVOController *_controller;
  NSString *_keyPath;
  NSKeyValueObservingOptions _options;
  SEL _action;
  void *_context;
  FBKVONotificationBlock _block;
  _FBKVOInfoState _state;
}
Copy the code

@end

_FBKVOInfo initialization

- (instancetype)initWithController:(FBKVOController *)controller keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(nullable FBKVONotificationBlock)block action:(nullable SEL)action context:(nullable void *)context { self = [super init]; if (nil ! = self) { _controller = controller; _block = [block copy]; _keyPath = [keyPath copy]; _options = options; _action = action; _context = context; } return self; }Copy the code

Determine if the two _FBKVOInfo are equal, overridehash.isEqual:(id)objectmethods

- (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

The Class structure

// Observers can sometimes be holders of FBKVOController * Object, as weak does not cause strong references. __weak id observer; // Key is the observed address, and value is a set of _FBKVOInfo, where isEqual is overridden. NSMapTable<id, NSMutableSet<_FBKVOInfo *> *> *_objectInfosMap; Pthread_mutex_t _lock;Copy the code

Initialize the

  • _observer assignment
  • _objectInfosMap initialization, according to the value of retainObserved, decides whether the operation is Strong or weak when storing.
  • Pthread_mutex_t _lock is initialized
- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved
{
    _observer = observer;
    NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
    _objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];
    pthread_mutex_init(&_lock, NULL);
}
Copy the code

The method call

  • Parameter verification, invalid return, and then the parameter wrapped into a _FBKVOInfo *infoInstance object that will be generated_FBKVOInfo *infoPass to step 2.
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
{  
  if (nil == object || 0 == keyPath.length || NULL == block) return;
  _FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];
  [self _observe:object info:info];
}
Copy the code
  • Check whether the object is registered, and if soNSMutableSet *infos = [_objectInfosMap objectForKey:object];Will not be empty, not registered will create a new one and store it in_objectInfosMapIn the. Key is the memory address of object
  • Check whether the new keyPath already exists and return directly to avoid repeated addition. [This is the key step]
  • Pass Object and info to the singleton_FBKVOSharedController
- (void)_observe:(id)object info:(_FBKVOInfo *)info { // lock pthread_mutex_lock(&_lock); NSMutableSet *infos = NSMutableSet *infos; The _observe method NSMutableSet *infos = [_objectInfosMap objectForKey:object] has been executed. [infos member:info], if info is stored, return nil, if not, return nil _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; If (nil == infos) {infos = [NSMutableSet set]; [_objectInfosMap setObject:infos forKey:object]; } // Put the newly registered info into NSMutableSet infos [infos addObject:info]; // unlock prior to callout pthread_mutex_unlock(&_lock); // Pass the registered object info to _FBKVOSharedController [[_FBKVOSharedController sharedController] observe:object info:info]; }Copy the code
  • Remove the observation of the single property keyPath in object
  • First check whether the object has added an attribute observation
  • Then check whether the keyPath to be removed exists. If it does, remove keyPath from infOS. After removing keyPath, infOS is empty, indicating that the object has no properties.
  • Pass data to a singleton_FBKVOSharedController
- (void)_unobserve:(id)object info:(_FBKVOInfo *)info { // lock pthread_mutex_lock(&_lock); NSMutableSet *infos = [_objectInfosMap objectForKey:object]; NSMutableSet *infos = [_objectInfosMap objectForKey:object]; RegisteredInfo _FBKVOInfo *registeredInfo = [infos member:info]; registeredInfo = [infos member:info]; if (nil ! = registeredInfo) {/ / will keyPath lift [infos removeObject: registeredInfo]; // remove no longer used infos if (0 == infos.count) { Remove the object corresponding to a collection of [_objectInfosMap removeObjectForKey: object]; } } // unlock pthread_mutex_unlock(&_lock); // unobserve [[_FBKVOSharedController sharedController] unobserve:object info:registeredInfo]; }Copy the code

_FBKVOSharedController

  • Singleton design pattern
  • Implementation of KVO – related methods
  • Methods of listening and removing properties are provided externally, notifying the observer of receiving the observed property change method.
  • Thread safety

Class structure

{
  NSHashTable<_FBKVOInfo *> *_infos;
  pthread_mutex_t _mutex;
}
Copy the code
NSHashTable *infos NSHashTable *infos = [[NSHashTable alloc] initWithOptions:]; pthread_mutex_init(&_mutex, NULL); return self; }Copy the code

Core method

  • Stores the info registered to listen into the _infOS collection
  • Add a listen to the property keypath of obejCT and pass info back as context, which will be used when property changes are received.
  • Change the info – > _state state for _FBKVOInfoStateNotObserving when removing the observers to monitor directly
- (void)observe:(id)object info:(nullable _FBKVOInfo *)info { if (nil == info) { return; } // register info pthread_mutex_lock(&_mutex); [_infos addObject:info]; pthread_mutex_unlock(&_mutex); // The singlet _FBKVOSharedController adds a view of the object's property keyPath, where we pass info to the context [object addObserver:self] forKeyPath:info->_keyPath options:info->_options context:(void *)info]; If (info->_state == _FBKVOInfoStateInitial) {info->_state = _FBKVOInfoStateObserving; } else if (info - > _state = = _FBKVOInfoStateNotObserving) {/ / if the state is not in the observation, [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info]; }}Copy the code
  • According to the info, the corresponding property observer, and change the info – > _state status to _FBKVOInfoStateNotObserving
- (void)unobserve:(id)object info:(nullable _FBKVOInfo *)info { if (nil == info) { return; } // unregister info pthread_mutex_lock(&_mutex); [_infos removeObject:info]; pthread_mutex_unlock(&_mutex); If (info->_state == _FBKVOInfoStateObserving) {[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info]; } info->_state = _FBKVOInfoStateNotObserving; }Copy the code
  • Notify the observer of the singleton _FBKVOSharedController when the properties of the observed object change
  • _FBKVOInfo *info = _FBKVOInfo *info
  • According to _FBKVOInfo *info, get controller, Observer, etc
  • Execute a block, Sel, or tell the observer that the property information has changed. Note that the observer can be implemented directlyobserveValueForKeyPath:ofObject:change:contextThe difference is that the change message is received by _FBKVOSharedController and then passed to the observer.
- (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); _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; if (nil ! = controller) { // take strong reference to observer id observer = controller.observer; 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

Making a link

Github.com/facebookarc…