preface

I have been engaged in iOS development for more than a year, and I spent most of my time writing business code, with little learning and summary of excellent open source code. Deep down, it’s time to start learning something. All things are difficult at the beginning, so I’m going to start with the relatively short open source code. The first article is about FBKVOController, an open source code of Facebook, a company that loves open source very much. Before you read this article, you should have some knowledge of KVO.

The body of the

Let’s talk about what this article is mainly about.

An overview of the

  • FBKVOController What did
  • FBKVOController Use the pose
  • FBKVOController The source code parsing
  • FBKVOController Summary of design ideas
  • FBKVOController Other harvest

What does FBKVOController do?

In short, the code, which Is open sourced by Facebook, is basically an additional layer of encapsulation of the KVO mechanism we use all the time. The most eye-catching feature is that it provides a block callback for us to handle, preventing the KVO code from being scattered around without using the following method:


- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
Copy the code

Use the pose

Using an open source framework, we do this, where the second method can be implemented in one line of code:

#import "ViewController.h"
#import "FBKVOController.h"
#import "NSObject+FBKVOController.h"@interface KVOModel : NSObject @property (nonatomic, copy) NSString *name; @property (nonatomic, assign) NSUInteger age; @end @implementation KVOModel @end NS_ASSUME_NONNULL_BEGIN @interface ViewController () @property (nonatomic, strong) KVOModel *model; @property (nonatomic, strong) FBKVOController *kvoController; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // Create the observed model class KVOModel *model = [[KVOModel alloc] init]; // Initialize the model member variable model.name = @"wo"; model.age = 5; self.model = model; // The first method: Create an FBKVOController object that is strongly referenced by VC, otherwise out of scope, FBKVOController *kvoController = [[FBKVOController alloc] initWithObserver:self] _kvoController = kvoController; // Add observations [kvoController observe:model keyPath:@"name" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSKeyValueChangeKey,id> * _Nonnull change) {
            NSLog(@"My old name was: %@", change[NSKeyValueChangeOldKey]);
            NSLog(@"My new name is %@", change[NSKeyValueChangeNewKey]); }]; // Second method: Instead of actively creating the FBKVOController object, Self.KVOController can execute KVO //------ on multiple member variables of an object.KVO ------ [self observe:model keyPaths:@[@"name"The @"age"] options:  NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew block:^(id observer, id object, NSDictionary *change) {
        
        NSString *changedKeyPath = change[FBKVONotificationKeyPathKey];
        

        if ([changedKeyPath isEqualToString:@"name"]) {
            NSLog(@"Changed the name.");
        } else if ([changedKeyPath isEqualToString:@"age"]) {
            NSLog(@"Modified age");
        }
        
        NSLog(@"Old value: %@", change[NSKeyValueChangeOldKey]);
        NSLog(@"New value is: %@", change[NSKeyValueChangeNewKey]); }]; // Modify the name member variable model.name = @"ni";
}

@end

NS_ASSUME_NONNULL_END
Copy the code

Advantages over native apis:

  • 1 you can takeAn arrayAnd at the same timemodelOf a number of different member variablesKVO.
  • 2 Use what is providedblockThat will beKVOThe related code is concentrated rather than scattered around. It’s very clear.
  • 3 doesn’t need to be indeallocIn the methodcancelObject observation, whenFBKVOControllerobjectdealloc, will automatically cancel the observation.

The source code parsing

H, FBKVOController. M, NSObject+FBKVOController. H, NSObject+FBKVOController. NSObject+FBKVOController is a simpler category. The main thing it does is create and associate an FBKVOController object with NSObject, in lazy load form, via objc_setAssociatedObject. Next, I will focus on the FBKVOController class, which is the main character of the day. The file also contains two other classes, _FBKVOInfo and _FBKVOSharedController. All of which will be covered below. FBKVOController specifies the initialization function:


- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved
{
  self = [super init];
  if(nil ! = self) {// In general, the observer will hold FBKVOController to avoid circular references. / / define NSMapTable key memory management strategy, by default, the incoming parameters retainObserved = YES NSPointerFunctionsOptions keyOptions = retainObserved? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality; // Create NSMapTable key with id type. The value for NSMutableSet _objectInfosMap < _FBKVOInfo * > type = [[NSMapTable alloc] initWithKeyOptions: keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0]; Pthread_mutex_init (&_lock, NULL); pthread_mutex_init(&_lock, NULL); }return self;
}
Copy the code

In the initialization code above, the comments are written more clearly. The only stranger is NSMapTable. In a nutshell, it is similar to NSDictionary. The difference is that NSMapTable can control the memory management policy for keys and values. NSDictionary’s memory policy is fixed to copy. When the key is object, the overhead of copy may be high! Therefore, you can only use the relatively flexible NSMapTable here.

Perform code parsing of KVO’s related methods

- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options Block :(FBKVONotificationBlock)block {// an assertion is generated when the keyPath string is 0 or the block is empty. = keyPath.length && NULL ! = block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPath, block); // If the observed object is nil, it will also return directlyif (nil == object || 0 == keyPath.length || NULL == block) {
    return; } // create info _FBKVOInfo _FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block]; // Observe the observed object with info. [self _observe:object info:info]; }Copy the code

In the code above, a _FBKVOInfo class mentioned earlier appears, which stores information including FBKVOController, keypath, options, and block.

Follow the last line of the code [self _observe:object info:info];


- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
  // lock 互斥锁加锁
  pthread_mutex_lock(&_lock);
  //还记得初始化 FBKVOController 时创建的 NSMapTable 么?
  //其结构是以 被观察者 object 为 key。并不像我们常用的 NSDictionary 那样是以 NSString 为 key
  NSMutableSet *infos = [_objectInfosMap objectForKey:object];

  // check forInfo existence // must be overwritten _FBKVOInfohashAnd the isEqual method, so you can use the member method of NSSet. _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 there is no information about this object, create an NSMutableSet and add it to the NSMapTableset of infos
  if (nil == infos) {
    infos = [NSMutableSet set];
    [_objectInfosMap setObject:infos forKey:object]; } // add info and oberve -- NSMutableSet add info [infos addObject:info]; // unlock prior to callout pthread_mutex_unlock(&_lock); // What is sharedController for? [[_FBKVOSharedController sharedController] Observe :object info:info] }Copy the code

Summarize the data structure of the previous paragraph. The FBKVOController has the member variable NSMapTable, which takes object as the key and NSMutableSet as the value. In NSMutableSet, different info is stored. Its relationship is shown as follows:

[[_FBKVOSharedController sharedController] observe:object info:info];
Copy the code

_FBKVOSharedController is a singleton that exists throughout the app life cycle and is responsible for receiving and forwarding KVO notifications. So all KVO notifications in the app are done by this singleton.


- (void)observe:(id)object info:(nullable _FBKVOInfo *)info
{
  if (nil == info) {
    return; } // register info to add info to NSHashTable NSMutableSet in _FBKVOController already strongly references info // NSHashTable is used for weak references to info, when info dealloc, Pthread_mutex_lock (&_mutex) is also removed from the container; [_infos addObject:info]; pthread_mutex_unlock(&_mutex); //_FBKVOSharedController is the actual observer! //context is a void * untyped pointer to info! [object addObserver:selfforKeyPath:info->_keyPath options:info->_options context:(void *)info]; // If state is the original state, change it to the observing state, indicating that the state is being observedif (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 context argument uses a pointer to (void *)info to ensure that the context is unique.

Receive KVO notification and act accordingly


- (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 ifPthread_mutex_lock (&_mutex); it exists // use context to find info, where void * is converted to an ID variable (__bridge ID) 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 actionif (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]; / / a dictionary to merge, and to copy a, / / contains information are as follows: 1, 2, which changed the value mChange original change dictionary [mChange addEntriesFromDictionary: change]; changeWithKeyPath = [mChange copy]; } info->_block(observer, object, changeWithKeyPath); }else if(info->_action) {ignore warnings!#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
          [observer performSelector:info->_action withObject:change withObject:object];
#pragma clang diagnostic pop
        } else{// The observer's native function is called by default!! [observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context]; } } } } }Copy the code

Summary of design ideas

  • 1 FBKVOControllerholdNSMapTableIn order toobjectkeyI get the correspondingNSMutableSet.NSMutableSetStored in_FBKVOInfo. The main purpose of this data structure is to prevent developers from adding the same data repeatedlyKVO. When it is checked that the same one already exists_FBKVOInfoObject, the following code is no longer executed.
  • 2 _FBKVOSharedControllerholdNSHashTable.NSHashTableHold different in the way of weak references_FBKVOInfo. The actual execution hereKVOThe code._FBKVOInfoThere is one important member variable_FBKVOInfoState, according to the enumerated value (_FBKVOInfoStateInitial,_FBKVOInfoStateObserving,_FBKVOInfoStateNotObserving) to add or removeKVO.

Harvest (after reading through and studying the source code)

  • 1 NSSet / NSHashTableNSDictionary/ NSMapTableThe study of
    • NSSetIs to filter out repetitionobjectCollection class of,NSHashTableNSSetThe upgraded version of the container, and only the mutable version, allows the holding of weak references to objects added to the container, whenNSHashTableWhen an object in is destroyed, it is also removed from the container.
    • NSMapTableNSDictionarySimilar, only with one more feature: you can set itkeyvalueNSPointerFunctionsOptionsFeatures!NSDictionarythekeyPolicy fixation iscopyIn consideration of overhead, simple numbers or strings are generally usedkey. But if you need itobjectAs akeyWhat about the application scenarios?NSMapTableCan come in handy! Can be achieved byNSFunctionsPointerLet’s define pairs separatelykeyvalueMemory management strategy, can be divided into simplestrong.weakAs well ascopy.
  • Some useful macros
    • NS_ASSUME_NONNULL_BEGIN,NS_ASSUME_NONNULL_ENDIf each attribute or method needs to be specifiednonnullnullableIs a very tedious thing. Apple has made these two macros available to ease our workload. In the code between the two macros, all simpler pointer objects are assumed to benonnull, so we only need to specify thosenullableThe pointer. If we force a non-null pointer to be null through dot syntax, the compiler reportswarning.
    • NS_UNAVAILABLEWhen we don’t want other developers to use the normal init method to initialize a class, we can say something like this in a.h file:- (instancetype)init NS_UNAVAILABLE;The compiler does not prompt for completioninitMethod, even if the developer forces the init message, the compiler will directly report an error.
    • NS_DESIGNATED_INITIALIZERSpecifies the initialization method. When a class provides multiple initializers, all initializers eventually call the specified initializer. The more common ones are:- (instancetype)initWithFrame:(CGRect)frame NS_DESIGNATED_INITIALIZER;
  • 3 Use of assertionsNSAssert(x,y);:xBOOLValue,yThe value is a string. whenx = YES, no assertion is generated. whenx = NO, generate an assertion, the app will crash, and print in the consoleyString content. The proper use of assertions can ensure the robustness of an APP.
  • 4 Use of mutex
    • pthread_mutex_init(&_lock, NULL);(Initialization)&_lockIs a pointer to the mutex, and the second argument is the mutex property.The default valueYes: After a thread locks, other threads that request the lock will form a waiting queue and obtain the lock according to the priority after unlocking. This locking strategy ensures the fairness of resource allocation.
    • pthread_mutex_destroy(&_lock);(destroyed)
    • pthread_mutex_lock(&_lock);(lock)
    • pthread_mutex_unlock(&_lock);(unlocked)
    • Locks are required for data read and write operations to avoid data contention.
    • As a refresherA deadlockIf thread A locks record 1 and waits for record 2, and thread B locks record 2 and waits for record 1, the two threads are deadlocked.

Small tail

Write source code analysis for the first time, feel the train of thought is still more confused, understanding is also relatively shallow, need to gradually explore. If you have any questions, please let me know!

Some NSHashTable features and use of links to related knowledge [mutex] (http://blog.csdn.net/yasi_xi/article/details/19112077)