1. What is KVO

KVO Apple official documentation

KVO(key-value Observing): a kind of objective-C realization of the design mode of observer. Key-value observation is a mechanism that allows an object to be notified when a specified property of another object changes.

2. The underlying implementation principle of KVO

Key-value Observing Implementation Details(official documentation)

Automatic key-value observing is implemented using a technique called isa-swizzling. The isa pointer, as the name suggests, points to the object’s class which maintains a dispatch table. This dispatch t able essentially contains pointers to the methods the class implements, among other data. When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance. You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

    1. KVOIs based onruntimeImplementation of theisa-swizzlingTechnically realized
    1. When an object is registered as an observer, the system dynamically creates a derived class of that class at run timeNSKVONotifying_XXObject,isaThe pointer is modified to point to a derived class instead ofThe real class
    1. Overrides the observation property in this derived classsetterMethod, the derived class in the bai overriddensetterImplement a true notification mechanism within the method
    1. KVOThe notification is mainly based onwillChangeValueForKeyanddidChangevlueForKeyThese two methods.willChangeValueForKeyCall this method before the value is changed, recording itoldValueAfter the change, calldidChangevlueForKeyMethod, and then callobserveValueForKey:ofObject:change:context:methods
    1. KVOThis set of implementations has been rewrittenclassTo hide derived classes

3. Demonstration of KVO underlying implementation

3.1 Preparing Code

- (void)viewDidLoad { [super viewDidLoad]; self.person = [Person new]; // Context :(nullable void *) void * NULL [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL]; self.person.name = @"+"; } - (IBAction)modifiedValue:(UIButton *)sender { self.person.name = [NSString stringWithFormat:@"%@+", self.person.name]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {NSLog(@" listening method :%@", change); } - (IBAction)removeObserve:(UIButton *)sender { [self.person removeObserver:self forKeyPath:@"name"]; NSLog(@" Observer removed successfully "); }Copy the code

Execution Result:

Listener method :{kind = 1; new = "+"; old = "<null>"; } kind = 1; new = "++"; old = "+"; } kind = 1; new = "+++"; old = "++"; }Copy the code
  • kind = 1You can see this by looking at the enumeration definitionNSKeyValueChangeSettingBelong tosetterMethod to modify the value
typedef NS_ENUM(NSUInteger, NSKeyValueChange) { NSKeyValueChangeSetting = 1, / / attribute setter methods NSKeyValueChangeInsertion = 2, / / collection types add methods NSKeyValueChangeRemoval = 3, / / set the remove operation NSKeyValueChangeReplacement = 4, / / variable collection type element to replace};Copy the code

3.2 Verifying derived classesNSKVONotifying_XX

  • inBreakpoint 1When we printobjectThe class name of thePerson
  • inBreakpoint 2When we printobjectThe class name of theNSKVONotifying_Person
  • Thus verify that after addingKVOThe bottom layer will generate oneDerived classes.

3.3 validationwillChangeValueForKeyanddidChangevlueForKey

Prepare the code:

+ / / shut down automatically build value observer (BOOL) accessInstanceVariablesDirectly: (nsstrings *) key {return NO; } - (void)setName:(NSString *)name { [self willChangeValueForKey:name]; NSLog(@" setmethod: willChangeValueForKey -%@", _name); _name = [name copy]; [self didChangeValueForKey:name]; NSLog(@" setmethod: didChangeValueForKey -%@", _name); }Copy the code

Execution Result:

Setmethod: willChangeValueForKey -(null) Setmethod: didChangeValueForKey -+ listener :{kind = 1; new = "+"; old = "<null>"; Setmethod: willChangeValueForKey -+ setmethod: didChangeValueForKey -++ listener :{kind = 1; new = "++"; old = "+"; }Copy the code

This perfectly validates our argument above

3.4 After notification Is Removed,isaPoint to the original class

As shown in the figure above,isa points to the original class after KVO is removed

4. Use of KVO

4.1 KVO observation mutable array

Apple Documents – mutable arrays

In the official KVC documentation, collection types for mutable arrays require the mutableArrayValueForKey method in order to add elements to a mutable array

self.person.dataArray = [NSMutableArray array];
[[self.person mutableArrayValueForKey:@"dataArray"] addObject:@"1"];
Copy the code

4.2 KVO: One-to-many observation

- (NSString *)downloadProgress {return [NSString stringWithFormat:@"%f", 1.0f * self.writeData/self.totalData]; } / / path processing + (NSSet < > nsstrings * *) keyPathsForValuesAffectingValueForKey: (nsstrings *) key {NSMutableSet * keyPaths = [[super keyPathsForValuesAffectingValueForKey:key] mutableCopy]; if ([key isEqualToString:@"downloadProgress"]) { NSArray *affectingKey = @[@"totalData", @"writeData"]; [keyPaths setByAddingObjectsFromArray:affectingKey]; } return keyPaths; } / / 2, registered KVO observations [self. The person addObserver: self forKeyPath: @ "downloadProgress" options: NSKeyValueObservingOptionNew context:NULL]; Self.person. totalData += 1; self.person.writeData += 1; //4, removeObserver - (void)dealloc{[self.person removeObserver:self forKeyPath:@"downloadProgress"]; }Copy the code

5. Custom implementation of KVO

5.1 Registered Observer

    1. Verify the currentkeyPathIf there is asettermethods
- (void)judgeSetterMethodFromKeyPath:(NSString *)keyPath { Class superClass = object_getClass(self); NSString *setterMethodName = setterForGetter(keyPath); SEL setterSEL = NSSelectorFromString(setterMethodName); Method setterMethod = class_getInstanceMethod(superClass, setterSEL); if (! setterMethod) { @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString StringWithFormat :@" no %@ set method ", keyPath] userInfo:nil]; }}Copy the code
    1. Dynamically generate derived subclassesKVONotifying_xxx
- (Class)createChildClassWithKeyPath:(NSString *)keyPath { NSString *oldClassName = NSStringFromClass([self class]); NSString *newClassName = [NSString stringWithFormat:@"%@%@", kKVOPrefix, oldClassName]; Class newClass = NSClassFromString(newClassName); if (newClass) return newClass; NewClass = objc_allocateClassPair([self class], newClassName.utf8String, 0); / / 2.2 registered objc_registerClassPair (newClass); Personsel classSEL = NSSelectorFromString(@"class"); Method classMethod = class_getInstanceMethod([self class], classSEL); const char *classTypes = method_getTypeEncoding(classMethod); class_addMethod(newClass, classSEL, (IMP)kvo_class, classTypes); // 2.3.2: Add setsel setterSEL = NSSelectorFromString(setterForGetter(keyPath)); Method setterMethod = class_getInstanceMethod([self class], setterSEL); const char *setterTypes = method_getTypeEncoding(setterMethod); class_addMethod(newClass, setterSEL, (IMP)kvo_setter, setterTypes); return newClass; }Copy the code
    1. Modify theisaPoint, point subclass
object_setClass(self, newClass);

Copy the code
    1. Save the information
KVOInfo *info = [[KVOInfo alloc] initWitObserver:observer forKeyPath:keyPath handleBlock:block]; //4. NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void *_Nonnull)(kKVOAssiociateKey)); if (! mArray) { mArray = [NSMutableArray arrayWithCapacity:1]; objc_setAssociatedObject(self, (__bridge const void *_Nonnull)(kKVOAssiociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } [mArray addObject:info];Copy the code
  • Complete registration method code
- (void)kvo_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(KVOBlock)block { //1. Verify setter if there is a [self judgeSetterMethodFromKeyPath: keyPath]; / / 2. The dynamically generated Class such as newClass = [self createChildClassWithKeyPath: keyPath]; //3. Alter isa to KVONotifying_XX object_setClass(self, newClass); KVOInfo *info = [[KVOInfo alloc] initWitObserver:observer forKeyPath:keyPath handleBlock:block]; //4. NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void *_Nonnull)(kKVOAssiociateKey)); if (! mArray) { mArray = [NSMutableArray arrayWithCapacity:1]; objc_setAssociatedObject(self, (__bridge const void *_Nonnull)(kKVOAssiociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } [mArray addObject:info]; }Copy the code

5.2 the responder

  • insetterIn the method, the systemobjc_msgSendSuperConvert to custom message sending
  • throughblockinformobserverThe response
static void kvo_setter(id self, SEL _cmd, id newValue) { NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd)); id oldValue = [self valueForKey:keyPath]; Void (*kvo_msgSendSuper)(void *, SEL, id) = (void *)objc_msgSendSuper; // void /* struct objc_super *super, SEL op, ... */ struct objc_super superStruct = { .receiver = self, .super_class = class_getSuperclass(object_getClass(self)), }; //objc_msgSendSuper(&superStruct,_cmd,newValue) kvo_msgSendSuper(&superStruct, _cmd, newValue); NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void *_Nonnull)(kKVOAssiociateKey)); for (KVOInfo *info in mArray) { if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) { info.handleBlock(info.observer, keyPath, oldValue, newValue); }}}Copy the code

5.3 Removing the Observer

- (void)kvo_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath { NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void *_Nonnull)(kKVOAssiociateKey)); if (observerArr.count <= 0) { return; } for (KVOInfo *info in observerArr) { if ([info.keyPath isEqualToString:keyPath]) { [observerArr removeObject:info]; objc_setAssociatedObject(self, (__bridge const void *_Nonnull)(kKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC); break; }} if (observerarr. count <= 0) {// Call the parent Class superClass = [self Class]; object_setClass(self, superClass); }}Copy the code

See GitHub->KVOExplore for the full code

References:

FBKVOController Apple document


If there are shortcomings, welcome to correct, if you feel good writing, remember to give a thumbs up!