KVO principle analysis

introduce

KVO, fully known as KeyValueObserving, is a set of event notification mechanism provided by apple. Allows an object to listen for changes to another object's specific properties and receive events when they are changed. Because of the implementation mechanism of KVO, it works on attributes. Generally, objects inherited from NSObject support KVO by default.

KVO and NSNotificationCenter are both implementations of observer mode in iOS. The difference is that compared with the relationship between the observed and the observer, KVO is one-to-one, not one to many. KVO is non intrusive to the monitored object and can monitor without manually modifying its internal code.

KVO can monitor the changes of a single attribute or a collection object. Get the proxy object through the mutableArrayValueForKey: and other methods of KVC. When the internal object of the proxy object changes, it will call back the method monitored by KVO. The collection object contains NSArray and NSSet.

use

There are three steps to using KVO

  1. Register the observer through the addObserver:forKeyPath:options:context: method, and the observer can receive the change event callback of keyPath property.
  2. Implement the observeValueForKeyPath:ofObject:change:context: Method in the observer. When the keyPath property changes, KVO will call back this method to notify the observer.
  3. When the observer does not need to listen, you can call the removeObserver:forKeyPath: method to remove the KVO. It should be noted that calling removeObserver needs to be before the observer disappears, otherwise it will cause Crash.

register

When registering an observer, you can pass in the options parameter, which is an enumeration type. If nskeyvalueobservangoptionnew and nskeyvalueobservangoptionold are passed in, it means that new and old values are received. By default, only new values are received. If you want to receive a callback immediately after registering an observer, you can add nskeyvalueobservangoptioninitial enumeration.

You can also pass in any type of object through the method context, which can be received in the code receiving the message callback. It is a value transmission method in KVO.

After calling the addObserver method, KVO does not make a strong reference to the observer. Therefore, we need to pay attention to the life cycle of the observer, otherwise it will lead to the Crash caused by the release of the observer.

monitor

The observer needs to implement the observeValueForKeyPath:ofObject:change:context: method. This method will be called when the KVO event arrives. If it is not implemented, it will cause Crash. The change dictionary stores the value related to the KVO attribute, which is returned according to the enumeration passed in during options. Enumeration will retrieve the value from the dictionary corresponding to the corresponding key. For example, there is an NSKeyValueChangeOldKey field to store the old value before the change.

There is also NSKeyValueChangeKindKey field in change, which has a horizontal relationship with NSKeyValueChangeOldKey to provide the information of this change, corresponding to the value of NSKeyValueChange enumeration type. For example, when the observed attribute changes, the field is NSKeyValueChangeSetting.

If the observed object is a collection object, the NSKeyValueChangeKindKey field will contain information about NSKeyValueChangeInsertion, NSKeyValueChangeRemoval and NSKeyValueChangeReplacement, indicating the operation mode of the collection object.

Other trigger methods

When calling KVO attribute objects, you can not only call them through point syntax and set syntax, but KVO is compatible with many call methods.

// Call the set method directly or indirectly through the point syntax of the attribute
[account setName:@"Savings"];

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

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

// Obtain the proxy object through 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];

practical application

KVO is mainly used for key value observation. If you want to notify another object of a value change, KVO is the most appropriate implementation. There is a classic case in the iOS tutorial of Stanford University, which communicates between Model and Controller through KVO.

trigger

Active trigger

KVO calls automatically when the attribute changes. If you want to manually control the call timing or want to call the KVO attribute yourself, you can call it through the method provided by KVO.

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

You can see that calling KVO relies mainly on two methods, calling the willChangeValueForKey: method before the property changes, and calling the didChangeValueForKey: method after the change occurs. However, if willchangevalueforkey is not called, calling didchangevalueforkey directly will not take effect. They have sequence and need to appear in pairs.

Disable KVO

If you want to disable KVO of an attribute, for example, if you don't want the key information to be obtained by the third-party SDK through KVO, you can disable KVO of this attribute in other places by returning NO through the automatically notifiesobserversforkey method. If the method returns YES, it means it can be called. If it returns NO, it means it cannot be called. This method is a class method. You can judge the keyPath inside the method to select whether this 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;
}

KVC trigger

KVC is specially compatible with KVO. When calling non attribute instance variables through KVC, KVC will also trigger KVO callback internally, and callback upward through NSKeyValueDidChange and NSKeyValueWillChange.

Next, ignore the system function upward from the main function and keep only the key stack. This is the KVO stack recalled 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

This is an upward callback triggered by KVC. You can see that KVO is triggered normally by modifying attributes, which is still different from KVO triggered by KVC. KVO is triggered by KVC, and it is not even triggered_ Call to NSSetObjectValueAndNotify.

* 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

Implementation principle

Core logic

KVO is realized through isa swizzling technology, which is the focus of the whole KVO implementation. At runtime, create an intermediate class based on the original class, which is a subclass of the original class, and dynamically modify the ISA of the current object to point to the intermediate class. And override the class method to return the class of the original class. Apple rewrites the class method to shield the existence of intermediate classes.

Therefore, apple suggests that the development should not rely on isa pointers, but obtain object types through class instance methods to avoid being affected by KVO or other runtime methods.

_NSSetObjectValueAndNotify

Then, the set method corresponding to the intermediate class will be modified, the willChangeValueForkey method and the didChangeValueForKey method will be inserted, and the set method of the parent class will be called between the two methods. In this process, the system encapsulates it into_ In the NSSetObjectValueAndNotify function. By looking at the assembly code of this function, you can see the calls of the internally encapsulated willChangeValueForkey method and didChangeValueForKey method.

The system is not just encapsulated_ Instead of the NSSetObjectValueAndNotify function, different functions will be called according to the property type. If it is an Int type, it will be called_ NSSetIntValueAndNotify. These implementations are defined in the Foundation framework. Specifically, you can view the implementation of the Foundation framework through hopper.

The runtime will notify the newly generated nskvonotifying_ The implementation of the setObject method of kvotest is replaced by_ NSSetObjectValueAndNotify function instead of overriding setObject function. Through the following test code, you can view the IMP corresponding to the selector and print 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:)]);

// The method address of the first time is 0x100c8e270 and the method address of the second time 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)

_NSKVONotifyingCreateInfoWithOriginalClass

For the principle of system implementation of KVO, object_setClass break point, or objc_ The allocateclasspair method can also be used as a break point. Both methods are necessary for creating classes. Through the assembly stack of these two methods, backtrace forward. Then, you can get the assembly code translated as follows.

You can see that there are some class name splicing rules, and then create a new class according to the class name. If newCls is empty, it has been created or may be empty. If newCls is not empty, 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;
}

verification

In order to verify the implementation of KVO, we add the following test code. First create a KVOObject class and add two attributes to it, then override the description method and print some key parameters internally.

It should be noted that in order to verify what KVO does at runtime, I print the class method of the object and get the class and parent class of the object through runtime. Before and after adding KVO monitoring, print it once to observe what the system has done.

@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 @"";
}

Create a KVOObject object and print the key information of the object before and after KVO to see what changes are made 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];

The following is the key information printed before and after KVO.

We found that after the object is KVO, its real type changes to NSKVONotifying_KVOObject class is no longer the previous class. KVO will dynamically create a new class at runtime, point the isa of the object to the newly created class, and point the superClass to the original class KVOObject. The naming rule of the newly created class is NSKVONotifying_xxx format. In order to make it more like the previous class, KVO will also rewrite the class instance method of the object to make it more like the original class.

After adding KVO, because the IMP of setName method and setAge method are modified, printing the IMP of these two methods is also a new address. The new implementation is NSKVONotifying_KVOObject.

This implementation method is not intrusive to business code. It can monitor a single object and modify its method implementation without affecting other KVOObject objects, and trigger KVO callback during assignment.

It is also found in the above code_ isKVOA method. This method can be used as a mark that uses KVO. The system may also use this method. If we want to judge whether the current class is a class dynamically generated by KVO, we can search the method from the method list.

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

object_getClass

Why is the runtime object called above_ GetClass function, you can get the real class?

Call object_ After the getClass function, it returns a class type, and class is objc_class defines a typedef alias through objc_class can get the class pointed to by the isa pointer of the object, that is, the class object of the object.

From this, we can know that object_ The internal return of the getClass function is the isa pointer of 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
}

Attention

Crash

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

Apple officially recommends addObserver during init and removeObserver during dealloc, which can ensure that add and remove appear in pairs, which is an ideal way to use.

Error checking

If an incorrect keyPath is passed in, there will be no error prompt. When calling KVO, you need to pass in a keyPath. Because keyPath is in the form of string, if the string does not change after the property name is changed, it is easy to cause Crash. For this problem, we can use the reflection mechanism of the system to reflect the keyPath, so that the compiler can check the legitimacy in @ selector().

NSString *keyPath = NSStringFromSelector(@selector(isFinished));

Cannot trigger callback

Due to the implementation mechanism of KVO, KVO will not be triggered if member variables are called for assignment.

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

// Wrong calling method
self.object = [[TestObject alloc] init];
[self.object addObserver:self forKeyPath:@"object" options:NSKeyValueObservingOptionNew context:nil];
self.object->object = [[NSObject alloc] init];

However, if the assignment operation is called through KVC, the callback method of KVO will be triggered. This is because KVC is separately compatible with KVO. Inside the KVC assignment method, the willChangeValueForKey: and didChangeValueForKey: methods are manually called.

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

Repeat add

Repeating addObserver on KVO will not cause a crash, but there will be the problem of repeatedly executing KVO callback methods.

[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

Through the above test code and printing the Class corresponding to the object in the callback, the subclass will not be created repeatedly. It is always a Class. Although repeated addobserver will not crash immediately, it will crash immediately when removeObserver is called for the first time after repeated addition. From the perspective of the crash stack, like the problem of repeated removal, it is an exception thrown by the system on its own initiative.

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.'

Repeat remove

KVO does not allow repeated removal of a keyPath. Repeated removal will cause 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"];

After executing the above test code, the following crash information will be caused. It can be seen from the crash stack of KVO that the system adds a Category named nskeyvalueobserver registration to NSObject in order to implement KVO's addObserver and removeObserver. The implementations of KVO's addObserver and removeObserver are all in it.

When removing KVO listening, the system will judge whether the keyPath of the current KVO has been removed. If it has been removed, 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 <ViewController 0x7ff6aee31600> for the key path "text" from <UILabel 0x7ff6aee2e850> 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

Troubleshooting links

KVO is the implementation of an event binding mechanism. After the value corresponding to the keyPath changes, it will call back the corresponding method. This data binding mechanism can easily lead to bug s that are difficult to troubleshoot when the object relationship is very complex. For example, KVO is not recommended for the property corresponding to keyPath because the relationship between the property and the call is very complex.

Realize KVO by yourself

In addition to the above shortcomings, KVO does not support block syntax and needs to rewrite the parent method separately. In this way, adding the add and remove methods will lead to very scattered code. Therefore, I simply implemented a KVO through runtime, and the source code is placed 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) {
    // Processing business logic
}];

self.object.name = @"lxz";

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

The calling code is very simple, directly through lxz_addObserver:originalSelector:callback: the method can add KVO listening, and can receive callbacks with changed attributes through the callback block. Moreover, the keyPath of the method receives a SEL type parameter, so you can check the validity of the method when passing in the parameter through @ selector(). If it is an unimplemented method, it will give an alarm directly.

By lxz_removeObserver:originalSelector: the method passes in the observer and keyPath. When all keypaths of the observer are removed, the observer object is removed from KVO.

If addObserver and removeObserver are repeated, it is OK. There is internal judgment logic. EasyKVO internally refers to the observer through break, which will not affect the life cycle of the observer, and will not cause Crash after the observer is released. One add method call corresponds to one block. If the observer listens to multiple keyPath attributes, it is not necessary to judge the keyPath in the block callback.

KVOController

If you want to use KVO safely and conveniently in the project, recommend a KVO open-source third-party framework of Facebook KVOController . KVOController essentially encapsulates the system KVO, has all the functions of the native KVO, avoids many problems of the native KVO, and is compatible with two callback methods: block and action.

Source code analysis

From the perspective of source code, it is relatively simple. It is mainly divided into two parts: NSObject Category and FBKVOController.

KVOController and KVOControllerNonRetaining attributes are provided in the Category. As the name suggests, the first one will generate a strong reference to the observer, and the second one will not. Its internal code is the code to create the FBKVOController object, and assign the created object to the attribute of Category. You can lazy load and create the FBKVOController object directly through this Category.

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

Implementation principle

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

There is also a private class_ FBKVOSharedController, which is the key to the implementation of FBKVOController framework. From the naming, it can be seen that it is a single instance. All kvos implemented through FBKVOController are observers. Each time a KVO is added through the FBKVOController_ FBKVOSharedController will set itself as an observer and implement the observeValueForKeyPath:ofObject:change:context: Method internally to forward the received message through block or action.

Its function is very simple. KVO monitoring is added through the observe:info: method and saved with an NSHashTable_ FBKVOInfo information. Remove the listener through the unobserve:info: method and delete the corresponding listener from the NSHashTable_ FBKVOInfo removed. Both methods call the KVO method of the system internally.

The FBKVOController class is required for external use, which internally implements the initialization, adding and removing listening operations. After calling the add listener method, an_ FBKVOInfo object and hold it through an NSMapTable object, and then call_ FBKVOSharedController to register and listen.

Using FBKVOController, you do not need to manually invoke the removeObserver method. When the listening object is gone, the remove method will be called in dealloc. If you can manually call the remove method because of business requirements, there will be no problem calling 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];
}

Because the implementation of FBKVOController is very simple, it is very simple to talk about here. The specific implementation can be discussed Github Download the source code and analyze it carefully.

Tags: Swift iOS objective-c

Posted on Sat, 20 Nov 2021 22:10:38 -0500 by mickey9801