introduce

KVO, which stands for KeyValueObserving, is an event notification mechanism provided by Apple. Allows an object to listen for changes to specific properties of another object and receive events when they change. Properties work because of the implementation mechanism of KVO, which is generally supported by objects that inherit from NSObject.

Both KVO and NSNotificationCenter are implementations of the Observer pattern in iOS. The difference is that KVO is one-to-one, not one-to-many, relative to the relationship between observed and observer. KVO is non-intrusive to the monitored object and can be monitored without manual modification of its internal code.

KVO can listen for changes in individual properties as well as collection objects. The proxy object is obtained through methods such as mutableArrayValueForKey: of KVC. When the internal object of the proxy object changes, the method monitored by KVO is called back. The collection object contains NSArray and NSSet.

use

There are three steps to using KVO

  1. throughaddObserver:forKeyPath:options:context:Method to register the observer, which the observer can receivekeyPathProperty change event callback.
  2. Implemented in the observerobserveValueForKeyPath:ofObject:change:context:Method, whenkeyPathWhen the properties change,KVOThis method is called back to notify the observer.
  3. Can be called when the observer does not need to listenremoveObserver:forKeyPath:Method will beKVORemoved. Note that the callremoveObserverBefore the observer disappears, otherwise it will causeCrash.

registered

When registering an observer, you can pass in the options parameter, which is an enumerated type. If incoming NSKeyValueObservingOptionNew and NSKeyValueObservingOptionOld said receive new value and old, to only receive new value by default. If you want to be in registered observers immediately after receiving a callback, you can join NSKeyValueObservingOptionInitial enumeration.

You can also pass in an object of any type through the method Context, which can be received in code that receives message callbacks, a method of passing values in KVO.

KVO does not strongly reference the observer after calling the addObserver method. Therefore, it is necessary to pay attention to the life cycle of the observer, otherwise it will lead to Crash caused by the release of the observer.

Listening to the

Observers need to implement observeValueForKeyPath: ofObject: change: context: method, call this method when KVO events to come, if not implemented causes a Crash. The change dictionary holds the values associated with the KVO attribute, which are returned according to the enumeration passed in with options. Enumerations will use keys to retrieve values from the dictionary, such as the NSKeyValueChangeOldKey field, which stores old values before they change.

There is also the NSKeyValueChangeKindKey field in change, which is level with NSKeyValueChangeOldKey to provide information about this change, corresponding to the value of the NSKeyValueChange enumeration type. For example, when the observed property changes, the field is NSKeyValueChangeSetting.

If the object being observed is a collection object, In NSKeyValueChangeKindKey field contains the information of the NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, NSKeyValueChangeReplacement, Represents how a collection object operates.

Other triggering methods

The KVO attribute object can be called not only through point syntax and set syntax, KVO is compatible with many call methods.

// Call the set method directly, or indirectly through the property's point syntax
[account setName:@"Savings"];

// Use the KVC setValue:forKey: method
[account setValue:@"Savings" forKey:@"name"];

// Use the KVC setValue:forKeyPath: method
[document setValue:@"Savings" forKeyPath:@"account.name"];

// Obtain the proxy object using the mutableArrayValueForKey: method and operate with the proxy object
Transaction *newTransaction = <#Create a new transaction for the account#>;
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];
Copy the code

The practical application

KVO is mainly used for key-value observation operations. If you want to notify another object when a value changes, KVO is the most suitable implementation. There is a classic example of communicating between a Model and a Controller via KVO in the Stanford iOS tutorial.

The trigger

Take the initiative to trigger

KVO is automatically called when a property changes. If you want to manually control the timing of the call, or if you want to implement the KVO property call yourself, you can call the method provided by KVO.

- (void)setBalance:(double)theBalance {
    if(theBalance ! = _balance) { [self willChangeValueForKey:@"balance"];
        _balance = theBalance;
        [self didChangeValueForKey:@"balance"]; }}Copy the code

You can see that calling KVO relies primarily on two methods: the willChangeValueForKey: method is called before the property changes and the didChangeValueForKey: method is called after the change. However, calling didChangeValueForKey directly does not work without calling willChangeValueForKey; the two are sequential and need to be paired.

Disable KVO

KVO if you want to ban certain properties, such as the three of key information don’t want to be the SDK to obtain, by means of KVO can be banned by automaticallyNotifiesObserversForKey method returns NO other place for this property for KVO. Method returns YES if it can be called, and NO if it cannot be called. This method is a class method that can judge keyPath inside the method to select whether the property is allowed to be KVO.

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
    BOOL automatic = NO;
    if ([theKey isEqualToString:@"balance"]) {
        automatic = NO;
    }
    else {
        automatic = [super automaticallyNotifiesObserversForKey:theKey];
    }
    return automatic;
}
Copy the code

KVC trigger

KVC is specially compatible with KVO. When a non-attribute instance variable is called through KVC, a KVO callback is also triggered internally by KVC and is called up through NSKeyValueDidChange and NSKeyValueWillChange.

The system functions up from main are ignored below, leaving only the key stack. This is the KVO stack that is called back by calling the property setter method.

* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 38.1
* frame #0: 0x0000000101bc3a15 TestKVO`::-[ViewController observeValueForKeyPath:ofObject:change:context:](self=0x00007f8419705890, _cmd="observeValueForKeyPath:ofObject:change:context:", keyPath="object", object=0x0000604000015b00, change=0x0000608000265540, context=0x0000000000000000) at ViewController.mm:84
frame #1: 0x000000010327e820 Foundation`NSKeyValueNotifyObserver + 349
frame #2: 0x000000010327e0d7 Foundation`NSKeyValueDidChange + 483
frame #3: 0x000000010335f22b Foundation`-[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:usingBlock:] + 778
frame #4: 0x000000010324b1b4 Foundation`-[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:] + 61
frame #5: 0x00000001032a7b79 Foundation`_NSSetObjectValueAndNotify + 255
frame #6: 0x0000000101bc3937 TestKVO`::-[ViewController viewDidLoad](self=0x00007f8419705890, _cmd="viewDidLoad") at ViewController.mm:70
Copy the code

This is an upward callback triggered by KVC. As you can see, there is a difference between KVO triggered by modifying attributes and KVO triggered by KVC. Triggered by means of KVC KVO, didn’t even _NSSetObjectValueAndNotify calls.

* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 37.1
* frame #0: 0x0000000106be1a85 TestKVO`::-[ViewController observeValueForKeyPath:ofObject:change:context:](self=0x00007fe68ac07710, _cmd="observeValueForKeyPath:ofObject:change:context:", keyPath="object", object=0x0000600000010c80, change=0x000060c000262780, context=0x0000000000000000) at ViewController.mm:84
frame #1: 0x000000010886d820 Foundation`NSKeyValueNotifyObserver + 349
frame #2: 0x000000010886d0d7 Foundation`NSKeyValueDidChange + 483
frame #3: 0x000000010894d422 Foundation`NSKeyValueDidChangeWithPerThreadPendingNotifications + 148
frame #4: 0x0000000108879b47 Foundation`-[NSObject(NSKeyValueCoding) setValue:forKey:] + 292
frame #5: 0x0000000106be19aa TestKVO`::-[ViewController viewDidLoad](self=0x00007fe68ac07710, _cmd="viewDidLoad") at ViewController.mm:70
Copy the code

Realize the principle of

The core logic

KVO is implemented through isA-Swizzling technology, which is the focus of the entire KVO implementation. Create an intermediate class from the original class at run time, which isa subclass of the original class, and dynamically modify the isa of the current object to point to the intermediate class. And rewrite the class method to return the class of the original class. Apple rewrote the class method to block the existence of intermediate classes.

Instead of relying on isa Pointers, Apple recommends using the Class instance method to get object types to avoid being influenced by KVO or other Runtime methods.

_NSSetObjectValueAndNotify

The set method corresponding to the intermediate class is then modified and the willChangeValueForkey and didChangeValueForKey methods are inserted, calling the parent class’s set method between the two methods. This process, the system will be encapsulated into _NSSetObjectValueAndNotify function. By looking at the assembly code for this function, you can see internally wrapped calls to the willChangeValueForkey and didChangeValueForKey methods.

System is not only encapsulates _NSSetObjectValueAndNotify function, but according to the attribute types, different function calls. If it’s an Int it calls _NSSetIntValueAndNotify, and these implementations are defined in the Foundation framework. You can use Hopper to see the implementation of the Foundation framework.

Runtime will newly generated NSKVONotifying_KVOTest setObject method implementation, replacing _NSSetObjectValueAndNotify function, rather than rewriting setObject function. With the following test code, you can look at the IMP corresponding to the selector and print out the address of its implementation.

KVOTest *test = [[KVOTest alloc] init];
[test setObject:[[NSObject alloc] init]];
NSLog(@"%p", [test methodForSelector:@selector(setObject:)]);
[test addObserver:self forKeyPath:@"object" options:NSKeyValueObservingOptionNew context:nil];
[test setObject:[[NSObject alloc] init]];
NSLog(@"%p", [test methodForSelector:@selector(setObject:)]);

// Print the result, the first method address is 0x100c8e270, the second method address is 0x7ffF207a3203
(lldb) p (IMP)0x100c8e270
(IMP) $0 = 0x0000000100c8e270 (DemoProject`-[KVOTest setObject:] at KVOTest.h:11)
(lldb) p (IMP)0x7fff207a3203
(IMP) $1 = 0x00007fff207a3203 (Foundation`_NSSetObjectValueAndNotify)
Copy the code

_NSKVONotifyingCreateInfoWithOriginalClass

For the system to implement KVO, it is possible to interrupt the object_setClass or the objc_allocateClassPair method, both of which are required to create classes. Trace back through the assembly stack of these two methods. You can then get translated assembly code as follows.

You can see that there are some class name concatenation rules, and then new classes are created based on the class name. If newCls is empty, it has already been created, or it may be empty. If newCls is not null, then register the newly created class, and set some parameters of SDTestKVOClassIndexedIvars structure.

Class _NSKVONotifyingCreateInfoWithOriginalClass(Class originalClass) {
    const char *clsName = class_getName(originalClass);
    size_t len = strlen(clsName);
    len += 0x10;
    char *newClsName = malloc(len);
    const char *prefix = "NSKVONotifying_";
    __strlcpy_chk(newClsName, prefix, len);
    __strlcat_chk(newClsName, clsName, len, - 1);
    Class newCls = objc_allocateClassPair(originalClass, newClsName, 0x68);
    if (newCls) {
        objc_registerClassPair(newCls);
        SDTestKVOClassIndexedIvars *indexedIvars = object_getIndexedIvars(newCls);
        indexedIvars->originalClass = originalClass;
        indexedIvars->KVOClass = newCls;
        CFMutableSetRef mset = CFSetCreateMutable(nil.0, kCFCopyStringSetCallBacks);
        indexedIvars->mset = mset;
        CFMutableDictionaryRef mdict = CFDictionaryCreateMutable(nil.0.nil, kCFTypeDictionaryValueCallBacks);
        indexedIvars->mdict = mdict;
        pthread_mutex_init(indexedIvars->lock);
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            bool flag = true;
            IMP willChangeValueForKeyImp = class_getMethodImplementation(indexedIvars->originalClass, @selector(willChangeValueForKey:));
            IMP didChangeValueForKeyImp = class_getMethodImplementation(indexedIvars->originalClass, @selector(didChangeValueForKey:));
            if(willChangeValueForKeyImp == _NSKVONotifyingCreateInfoWithOriginalClass.NSObjectWillChange && didChangeValueForKeyImp ==  _NSKVONotifyingCreateInfoWithOriginalClass.NSObjectDidChange) { flag =false;
            }
            indexedIvars->flag = flag;
            NSKVONotifyingSetMethodImplementation(indexedIvars, @selector(_isKVOA), NSKVOIsAutonotifying.nil);
            NSKVONotifyingSetMethodImplementation(indexedIvars, @selector(dealloc), NSKVODeallocate.nil);
            NSKVONotifyingSetMethodImplementation(indexedIvars, @selector(class), NSKVOClass.nil);
        });
    } else {
        return nil;
    }
    return newCls;
}
Copy the code

validation

To verify how KVO is implemented, we add the following test code. First create a KVOObject class and add two properties to it, then override the Description method and print some key parameters internally.

Note that to verify what KVO does at runtime, I print out the object’s class method and get the object’s class and superclass via Runtime. Print both before and after adding KVO listening to see what the system does.

@interface KVOObject : NSObject
@property (nonatomic.copy  ) NSString *name;
@property (nonatomic.assign) NSInteger age;
@end

- (NSString *)description {
    IMP nameIMP = class_getMethodImplementation(object_getClass(self), @selector(setName:));
    IMP ageIMP = class_getMethodImplementation(object_getClass(self), @selector(setAge:));
    NSLog(@"object setName: IMP %p object setAge: IMP %p \n", nameIMP, ageIMP);
    
    Class objectMethodClass = [self class];
    Class objectRuntimeClass = object_getClass(self);
    Class superClass = class_getSuperclass(objectRuntimeClass);
    NSLog(@"objectMethodClass : %@, ObjectRuntimeClass : %@, superClass : %@ \n", objectMethodClass, objectRuntimeClass, superClass);
    
    NSLog(@"object method list \n");
    unsigned int count;
    Method *methodList = class_copyMethodList(objectRuntimeClass, &count);
    for (NSInteger i = 0; i < count; i++) {
        Method method = methodList[i];
        NSString *methodName = NSStringFromSelector(method_getName(method));
        NSLog(@"method Name = %@\n", methodName);
    }
    
    return @ "";
}
Copy the code

Create a KVOObject object and print the key information of the object before and after KVO to see what happens before and after KVO.

self.object = [[KVOObject alloc] init];
[self.object description];

[self.object addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];

[self.object description];
Copy the code

Below is the key information printed before and after KVO.

We found that when the object was KVO, its real type changed to NSKVONotifying_KVOObject, not the previous class. KVO dynamically creates a new class at run time, pointing the object’s ISA to the newly created class and superClass to the original class KVOObject. The new class is named in the NSKVONotifying_xxx format. To make it more like the previous class, KVO also overwrites the object’s class instance method to make it more like the original class.

After adding KVO, the IMPs of setName and setAge were modified, so the IMPs of these two methods are also a new address. The new implementation is in NSKVONotifying_KVOObject.

This implementation is non-intrusive to the business code and allows you to listen on a single object and modify its method implementation without affecting other KVOObject objects, triggering KVO callbacks when assigning values.

Also found in the code above is the _isKVOA method, which can be used as a flag for using KVO, and may be used by the system. If we want to determine whether the current class is dynamically generated by KVO, we can search the method from the list of methods.

/ / for the first time
object address : 0x604000239340
object setName: IMP 0x10ddc2770 object setAge: IMP 0x10ddc27d0
objectMethodClass : KVOObject, ObjectRuntimeClass : KVOObject, superClass : NSObject
object method list
method Name = .cxx_destruct
method Name = description
method Name = name
method Name = setName:
method Name = setAge:
method Name = age

/ / the second time
object address : 0x604000239340
object setName: IMP 0x10ea8defe object setAge: IMP 0x10ea94106
objectMethodClass : KVOObject, ObjectRuntimeClass : NSKVONotifying_KVOObject, superClass : KVOObject
object method list
method Name = setAge:
method Name = setName:
method Name = class
method Name = dealloc
method Name = _isKVOA
Copy the code

object_getClass

Why does the runtime object_getClass function get the actual class?

When you call object_getClass, it returns a Class type. Class isa typedef alias for the objc_class definition. By using objc_class, you can obtain the Class to which the isa pointer points, i.e. the object’s Class object.

As you can see, the object_getClass function internally returns an ISA pointer to the object.

typedef struct objc_class *Class;

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if! __OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif
}
Copy the code

Pay attention to the point

Crash

The addObserver and removeObserver of KVO need to be paired. If the remove is repeated, it will cause a Crash of the NSRangeException type. If the remove is forgotten, it will Crash when the KVO callback is received again after the observer is released.

It is recommended by Apple to use addObserver for init and removeObserver for dealloc. This ensures that add and remove are paired, which is an ideal way to use them.

Error checking

There is no error message if you pass in the wrong keyPath. A keyPath is passed in when KVO is called. Since keyPath is a string, it is easy to Crash if the property name changes and the string does not change. For this problem, we can use the system’s reflection mechanism to reflect keyPath so that the compiler can check for legitimacy in @selector().

NSString *keyPath = NSStringFromSelector(@selector(isFinished));
Copy the code

Cannot trigger a callback

Because of the KVO implementation mechanism, KVO is not triggered if a member variable is called for assignment.

@interface TestObject : NSObject {
    @public
    NSObject *object;
}
@end

// Wrong way to call
self.object = [[TestObject alloc] init];
[self.object addObserver:self forKeyPath:@"object" options:NSKeyValueObservingOptionNew context:nil];
self.object->object = [[NSObject alloc] init];
Copy the code

However, if the assignment operation is called through KVC, KVO’s callback method is triggered. This is because KVC has a separate compatibility with KVO, and within KVC’s assignment methods, the willChangeValueForKey: and didChangeValueForKey: methods are manually called.

// KVC
self.object = [[TestObject alloc] init];
[self.object addObserver:self forKeyPath:@"object" options:NSKeyValueObservingOptionNew context:nil];
[self.object setValue:[[NSObject alloc] init] forKey:@"object"];
Copy the code

Repeat to add

Repeating the KVO addObserver does not crash, but it does cause the KVO callback method to be repeated.

[self.testLabel addObserver:self forKeyPath:@"text" options:NSKeyValueObservingOptionNew context:nil];
self.testLabel.text = @"test";
[self.testLabel addObserver:self forKeyPath:@"text" options:NSKeyValueObservingOptionNew context:nil];
[self.testLabel addObserver:self forKeyPath:@"text" options:NSKeyValueObservingOptionNew context:nil];
[self.testLabel addObserver:self forKeyPath:@"text" options:NSKeyValueObservingOptionNew context:nil];
self.testLabel.text = @"test";

/ / output
2018- 08- 03 11:48:49.502450+0800 KVOTest[5846:412257] test
2018- 08- 03 11:48:52.975102+0800 KVOTest[5846:412257] test
2018- 08- 03 11:48:53.547145+0800 KVOTest[5846:412257] test
2018- 08- 03 11:48:54.087171+0800 KVOTest[5846:412257] test
2018- 08- 03 11:48:54.649244+0800 KVOTest[5846:412257] test
Copy the code

From the above test code, and by printing the corresponding Class of Object in the callback, you do not create subclasses repeatedly. It is always a Class. Although repeated AddobServer does not crash immediately, repeated additions will crash immediately the first time removeObserver is called. The crash stack, like the double removal problem, is an active exception thrown by the system.

Terminating app due to uncaught exception 'NSRangeException', reason: 'Cannot remove an observer <UILabel 0x7f859b547490> for the key path "text" from <UILabel 0x7f859b547490> because it is not registered as an observer.'
Copy the code

Repeat to remove

KVO does not allow repeated removal of a keyPath, which will result in a crash. For example, the following test code.

[self.testLabel addObserver:self forKeyPath:@"text" options:NSKeyValueObservingOptionNew context:nil];
self.testLabel.text = @"test";
[self.testLabel removeObserver:self forKeyPath:@"text"];
[self.testLabel removeObserver:self forKeyPath:@"text"];
[self.testLabel removeObserver:self forKeyPath:@"text"];
Copy the code

Executing the above test code results in the following crash message. From the collapse of KVO stack can be seen, the system in order to realize the addObserver KVO and removeObserver for NSObject added a Category called NSKeyValueObserverRegistration, KVO’s addObserver and removeObserver implementations are included.

When removing the KVO listener, the system will determine whether the current KVO keyPath has been removed. If so, it will actively throw an NSException exception.

2018- 08- 03 10:54:27.477379+0800 KVOTest[4939:286991] *** Terminating app due to uncaught exception 'NSRangeException', reason: 'Cannot remove an observer 
      
        for the key path "text" from 
       
         because  it is not registered as an observer.'
       
      
*** First throw call stack:
(
	0   CoreFoundation                      0x000000010db2312b __exceptionPreprocess + 171
	1   libobjc.A.dylib                     0x000000010cc6af41 objc_exception_throw + 48
	2   CoreFoundation                      0x000000010db98245+ [NSException raise:format:] + 197
	3   Foundation                          0x0000000108631f15- [NSObject(NSKeyValueObserverRegistration) _removeObserver:forProperty:] + 497
	4   Foundation                          0x0000000108631ccb- [NSObject(NSKeyValueObserverRegistration) removeObserver:forKeyPath:] + 84
	5   KVOTest                             0x0000000107959a55 -[ViewController viewDidAppear:] + 373
	/ /...
	20  UIKit                               0x000000010996d5d6 UIApplicationMain + 159
	21  KVOTest                             0x00000001079696cf main + 111
	22  libdyld.dylib                       0x000000010fb43d81 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
Copy the code

Trying to link

KVO is an implementation of an event binding mechanism that calls back to the corresponding method when the value corresponding to keyPath changes. This data binding mechanism, in the case of complex object relationships, can easily lead to bugs that are difficult to troubleshoot. For example, KVO is not recommended for a property corresponding to keyPath that is called in a complex relationship.

Implement KVO yourself

In addition to the drawbacks above, KVO does not support block syntax and requires a separate override of the parent class methods, so adding add and remove methods can lead to very fragmented code. So, I implemented a simple KVO via Runtime, and put the source code on my Github, called EasyKVO.

self.object = [[KVOObject alloc] init];
[self.object lxz_addObserver:self originalSelector:@selector(name) callback:^(id observedObject, NSString *observedKey, id oldValue, id newValue) {
    // Process the business logic
}];

self.object.name = @"lxz";

// Remove notification
[self.object lxz_removeObserver:self originalSelector:@selector(name)];
Copy the code

The calling code is very simple, direct through lxz_addObserver: originalSelector: callback: method can add KVO listening, can pass a callback block receiving properties change after the callback. And the method’s keyPath takes an SEL parameter, so you can check the method’s validity by passing it in with @selector(), and warn if it’s an unimplemented method.

Through lxz_removeObserver: originalSelector: method is introduced to observer and keyPath, all keyPath removed when the observer object is removed from the KVO observer.

If you duplicate addObserver and removeObserver, it’s fine. There’s internal logic. EasyKVO internally references the observer through weak, which does not affect the life cycle of the observer, and does not cause Crash after the observer is released. One add method call corresponds to one block, and if the observer is listening for multiple keyPath properties, there is no need to judge keyPath in the block callback.

KVOController

If you want to use KVO safely and conveniently in your project, I recommend KVOController, an open source third-party framework for KVO from Facebook. KVOController is essentially the encapsulation of system KVO, with all functions of native KVO, and avoids many problems of native KVO, compatible with block and Action callback methods.

Source code analysis

Source code is relatively simple, mainly divided into NSObject Category and FBKVOController two parts.

In Category provides KVOController and KVOControllerNonRetaining two attributes, just as its name implies the first can produce strong reference to the observer, the second is not. The internal code is the code that creates the FBKVOController object and assigns the created object to the Category property. From this Category, you can load and create the FBKVOController object.

- (FBKVOController *)KVOControllerNonRetaining
{
  id controller = objc_getAssociatedObject(self.NSObjectKVOControllerNonRetainingKey);
  
  if (nil == controller) {
    controller = [[FBKVOController alloc] initWithObserver:self retainObserved:NO];
    self.KVOControllerNonRetaining = controller;
  }
  
  return controller;
}
Copy the code

Realize the principle of

FBKVOController is divided into three parts, _FBKVOInfo is a private class, the function of this class is very simple, is to save the various objects required by FBKVOController in a structured form, similar to the function of the model class.

There is also a private class, _FBKVOSharedController, which is key to the implementation of the FBKVOController framework. As you can see from the name, this is a singleton, and all KVO implemented by FBKVOController, the observer is it. Every time by FBKVOController add a KVO, _FBKVOSharedController themselves will be set as observers, and in its internal implementation observeValueForKeyPath: ofObject: change: context: method, The received message is forwarded by block or Action.

The function is simple: add the KVO listener with the observe:info: method and store the _FBKVOInfo information with an NSHashTable. Remove the listener using the unobserve:info: method and remove the corresponding _FBKVOInfo from the NSHashTable. Both methods call the system’s KVO methods internally.

For external use, use the FBKVOController class, which internally implements initialization and adds and removes listeners. After calling the Add Listener method, an _FBKVOInfo object is created internally and held by an NSMapTable object, and then _FBKVOSharedController is called to register the listener.

With FBKVOController, there is no need to manually call the removeObserver method. When the listener disappears, the Remove method is called in dealloc. If the remove method can be called manually because of business requirements, it will not be a problem to call the remove method repeatedly.

- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
    NSMutableSet *infos = [_objectInfosMap objectForKey:object];

    _FBKVOInfo *existingInfo = [infos member:info];
    if (nil! = existingInfo) {return;
    }

    if (nil == infos) {
      infos = [NSMutableSet set];
      [_objectInfosMap setObject:infos forKey:object];
    }

    [infos addObject:info];

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

Because the implementation of FBKVOController is very simple, so here is a very simple talk, specific implementation can go to Github download source code carefully analyzed.