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.
-
KVO
Is based onruntime
Implementation of theisa-swizzling
Technically realized
-
- When an object is registered as an observer, the system dynamically creates a derived class of that class at run time
NSKVONotifying_XX
Object,isa
The pointer is modified to point to a derived class instead ofThe real class
- When an object is registered as an observer, the system dynamically creates a derived class of that class at run time
-
- Overrides the observation property in this derived class
setter
Method, the derived class in the bai overriddensetter
Implement a true notification mechanism within the method
- Overrides the observation property in this derived class
-
KVO
The notification is mainly based onwillChangeValueForKey
anddidChangevlueForKey
These two methods.willChangeValueForKey
Call this method before the value is changed, recording itoldValue
After the change, calldidChangevlueForKey
Method, and then callobserveValueForKey:ofObject:change:context:
methods
-
KVO
This set of implementations has been rewrittenclass
To 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 = 1
You can see this by looking at the enumeration definitionNSKeyValueChangeSetting
Belong tosetter
Method 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 the
Person
- inBreakpoint 2When we printobjectThe class name of the
NSKVONotifying_Person
- Thus verify that after adding
KVO
The bottom layer will generate oneDerived classes.
3.3 validationwillChangeValueForKey
anddidChangevlueForKey
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,isa
Point 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
-
- Verify the current
keyPath
If there is asetter
methods
- Verify the current
- (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
-
- Dynamically generate derived subclasses
KVONotifying_xxx
- Dynamically generate derived subclasses
- (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
-
- Modify the
isa
Point, point subclass
- Modify the
object_setClass(self, newClass);
Copy the code
-
- 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
- in
setter
In the method, the systemobjc_msgSendSuper
Convert to custom message sending - through
block
informobserver
The 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!