KVO FBKVOController analysis of iOS underlying exploration

Posted by andybrooke on Tue, 04 Jan 2022 12:57:59 +0100

review

In the previous blogs, we have introduced the basic use of KVO and how to customize KVO. This blog will analyze FBKVOController, an excellent KVO third-party library.

KVO (I) - An Introduction to KVO
KVO (II) - KVO principle analysis of iOS underlying exploration
KVO for iOS bottom layer exploration (III) - Custom KVO
KVO for iOS bottom layer exploration (IV) - Custom KVO

FBKVOController is a functional programming implementation without removing the observer.

1. Brief introduction to fbkvocontroller

FBKVOController is an open source framework based on system KVO implementation of Facebook. Support Objective-C and Swift languages.
GitHub address

Key value observation is a particularly useful technique for communicating between layers in model view controller applications. KVOController is based on Cocoa's proven key value observation implementation. It provides a simple, modern API, which is also thread safe.

KVOController has the following advantages:

1.1 advantages of kvocontroller

  • Callback notifications using blocks, custom actions, or NSKeyValueObserving.
  • No additional observers need to be removed
  • The observer is implicitly removed when the controller dealloc.
  • It has a special thread safe protection mechanism to prevent the resurrection of observers
  • For more information about KVO, see Apple's Introduction to key value observation.

1.2 use of fbkvocontroller

FBKVOController is very simple to use with few codes. The following codes are used for FBKVOController:

  • FBKVOController use
 self.person = [[JPPerson alloc] init];
    self.person.name = @"RENO";
    self.person.age = 18;
    self.person.mArray = [NSMutableArray arrayWithObject:@"1"];

    [self.kvoCtrl observe:self.person keyPath:@"age" options:0 action:@selector(jp_observerAge)];
    [self.kvoCtrl observe:self.person keyPath:@"name" options:(NSKeyValueObservingOptionNew) block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
        NSLog(@"****%@****",change);
    }];
    [self.kvoCtrl observe:self.person keyPath:@"mArray" options:(NSKeyValueObservingOptionNew) block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
        NSLog(@"****%@****",change);
    }];

The code is very concise. We don't need to remove the observer in dealloc like the KVO of the system we used before. This wave of use is very cool!

  • Lazy load initialization
#pragma mark - lazy
- (FBKVOController *)kvoCtrl{
    if (!_kvoCtrl) {
        _kvoCtrl = [FBKVOController controllerWithObserver:self];
    }
    return _kvoCtrl;
}

2. KVOController implementation analysis

2.1 intermediary model

We usually find an intermediary when we buy or rent a house. Through the intermediary, we can find a suitable house faster and more efficiently. There are many things that the intermediary can help us do. We don't have to find houses ourselves.

KVOController mainly uses the intermediary mode. The trouble with official kvo use is that it requires a trilogy. The core of KVOController is to encapsulate the trilogy at the bottom, and the upper layer only needs to care about the business logic.

FBKVOController handles registration, removal and callback (callback includes block, action and observe callback of compatible system). It is an exposed interaction class. Using FBKVOController is divided into two steps:

  • Initialize the FBKVOController instance using controllerWithObserver.
  • Use observe: to register.

2.2 FBKVOController initialization

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

_ Observer is an observer and an attribute of FBKVOController. It is decorated with weak

@property (nullable, nonatomic, weak, readonly) id observer;

Because the FBKVOController itself is held by the observer, it is a weak type modification.

_ objectInfosMap performs memory management / initialization configuration of NSMapTable according to retainObserved, and the member variable of FBKVOController. Among them, one observer corresponds to multiple observers_ FBKVOInfo (that is, the observed object corresponds to multiple keypaths):

  NSMapTable<id, NSMutableSet<_FBKVOInfo *> *> *_objectInfosMap;

_ FBKVOInfo is placed in NSMutableSet, indicating that it is de duplicated.

2.3 FBKVOController registration

- (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 info
  _FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];

  // observe object with info
  [self _observe:object info:info];
}
  • The first step is to make some judgment, fault-tolerant judgment.

  • Structure_ FBKVOInfo, save FBKVOController, keyPath, options and block.

  • Call_ observe:(id)object info:(_FBKVOInfo *)info

  • _FBKVOInfo

@implementation _FBKVOInfo
{
@public
  __weak FBKVOController *_controller;
  NSString *_keyPath;
  NSKeyValueObservingOptions _options;
  SEL _action;
  void *_context;
  FBKVONotificationBlock _block;
  _FBKVOInfoState _state;
}

In_ Some relevant data information is saved in FBKVOInfo

  • Rewrite isEqual and hash methods
- (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];
}

As long as_ The same keyPath is considered the same object

  • _observe: info:
- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
  // lock
  pthread_mutex_lock(&_lock);

  //Get the set corresponding to the object from the TableMap
  NSMutableSet *infos = [_objectInfosMap objectForKey:object];

  // check for info existence
  //Determine whether the corresponding keypath info exists
  _FBKVOInfo *existingInfo = [infos member:info];
  if (nil != existingInfo) {
    //There is a direct return, which is equivalent to excluding the same keypath for the same observer
    // observation info already exists; do not observe it again

    // unlock and return
    pthread_mutex_unlock(&_lock);
    return;
  }

  // lazilly create set of infos
  //The TableMap data is empty for creation settings
  if (nil == infos) {
    infos = [NSMutableSet set];
    //< observed - keypaths info >
    [_objectInfosMap setObject:infos forKey:object];
  }

  // add info and oberve
  //keypaths info add keypath info
  [infos addObject:info];

  // unlock prior to callout
  pthread_mutex_unlock(&_lock);
  //register
  [[_FBKVOSharedController sharedController] observe:object info:info];
}
  • First, judge whether kayPath has been registered. After registration, it will be returned directly. Here is the de duplication process. This wave of operation is very detailed.
  • To be constructed_ FBKVOInfo information added to_ objectInfosMap.
  • Call_ FBKVOSharedController for real registration.
  • member: description
    member will call_ The hash and isEqual in FBKVOInfo determine whether the object exists, that is, whether the object corresponding to the keyPath exists.

Registered [[_fbkvosharedcontrollersharedcontroller] observe: object Info: Info] here is a singleton

Why use singleton here? Instead of using singletons during external call initialization?
If it is used in our VC, it will not be released and will be very bloated. If a single example is used in this method, our FBKVOController will be destroyed with the destruction of the VC. This wave of operation is very detailed again. It's great!

What is the object parameter passed in here? Is it self?

It's not self, it's self person๏ผŒwhy ๏ผŸ Children, do you have many question marks now?

In our impression, we use KVO to add observers, and the incoming is self! But pretty boy, we're not here!


What our VC needs is a callback to the block, and adding an observer is self The attribute of person changes, so it is passed in self Just person. I don't care how you operate internally. Just tell me the result after the change. If you lose a block callback and notify me, VC will be OK!

As shown in the figure, self here refers to the previous singleton for reuse, that is, as long as the observation of adding attributes uses this singleton, it is distinguished by keyPath, and different attributes are observed.

2.4 kvocontroller destruction

The destruction of KVOController is actually implemented internally, so we don't need to destroy it manually.

  • dealloc
  • unobserveAll
- (void)unobserveAll
{
  [self _unobserveAll];
}
  • _unobserveAll
  • _unobserve:(id)object info:
- (void)_unobserve:(id)object info:(_FBKVOInfo *)info
{
  // lock
  pthread_mutex_lock(&_lock);

  // get observation infos
  NSMutableSet *infos = [_objectInfosMap objectForKey:object];

  // lookup registered info instance
  _FBKVOInfo *registeredInfo = [infos member:info];

  if (nil != registeredInfo) {
    [infos removeObject:registeredInfo];

    // remove no longer used infos
    if (0 == infos.count) {
      [_objectInfosMap removeObjectForKey:object];
    }
  }

  // unlock
  pthread_mutex_unlock(&_lock);

  // unobserve
  [[_FBKVOSharedController sharedController] unobserve:object info:registeredInfo];
}

The singleton calls the internal destruction implementation method


The code in the red box in the figure is actually the system's method for removing observers removeObserver: forKeyPath:

Since the FBKVOController instance is held by the VC, the FBKVOController instance will be dealloc when our VC is dealloc destroyed. Calling here is equivalent to calling in dealloc in VC, and removing is the same.

3. Explore KVO through gnustep

kvo and kvc belong to the Foundation framework. Because the code related to Foundation is not open source, apple can explore them through gnustep Looking at the principle, gnustep has some early underlying implementations of apple.

That's all for FBKVOController analysis.

Through gnustep, we can see that there are only a lot of descriptions. The old fellow who is interested in it can download the source code of Foundation by himself, and see how it works.

4. Summary

  • FBKVOController uses the mediator mode. Through the idea of functional programming, FBKVOController uses block to notify the callback of the change of attributes
  • FBKVOController is registered. A singleton is used internally for reuse. It is distinguished by keyPath. Different attributes are observed.
  • When the controller dealloc implicitly removes the observer, in fact, the removal method of the system is called internally.

More content is constantly updated

๐ŸŒน Just like it ๐Ÿ‘๐ŸŒน

๐ŸŒน If you think you have something to gain, you can have a wave, collect + pay attention, comment + forward, so as not to find me next time ๐Ÿ˜๐ŸŒน

๐ŸŒน Welcome to exchange messages, criticize and correct each other ๐Ÿ˜๏ผŒ Self improvement ๐ŸŒน

Topics: iOS