KVO, full name for key-value observing, in Chinese, KVO is a mechanism that allows an object to be notified of changes to specified properties of other objects.

In the official document of key-Value Observing Programming Guide, there is another sentence like this: Before understanding KVO, one must first understand KVC (that is, KVO is based on KVC, and the bottom layer of KVO is realized by KVC).

In order to understand key-value observing, you must first understand key-value coding. KVC is the key-value coding, after the object is created, it can dynamically assign value to the object property. KVO is key value observation, which provides a listening mechanism. When the properties of the specified object are modified, the object will receive notification. Therefore, it can be seen that KVO is based on KVC to monitor the dynamic changes of the properties.

In iOS daily development, KVO is often used to monitor the change of object properties and make timely response. That is, when the properties of the specified observed object are modified, KVO will automatically notify the corresponding observer. So what is the difference between KVO and NSNotificatioCenter?

  • The same

    • 1, the realization principle of bothIt's all observer modeBoth are used forListening to the
    • 2, all canImplement one-to-manyThe operation of the
  • The difference between

    • 1.KVO can only be used to listen for changes in object properties, and attribute names are found by NSString, the compiler will not help you detect the error and completion, pure hand typing will be more prone to error
    • 2,NSNotificationtheSend to monitor(POST) we can control,kvobysystemControl.
    • 3,KVOcanRecord changes between old and new values

Precautions for using KVO

1. Basic use

The basic use of KVO can be divided into 3 steps:

  • Registered observeraddObserver:forKeyPath:options:context
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL]; // Context is of type nullable void *, should be NULL, not nilCopy the code
  • Implement the KVO callbackobserveValueForKeyPath:ofObject:change:context
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{ if ([keyPath isEqualToString:@"name"]) { NSLog(@"%@",change); }}Copy the code
  • Remove observerremoveObserver:forKeyPath:context
[self.person removeObserver:self forKeyPath:@"nick" context:NULL];
Copy the code

2. Use context

In the official documentation, forParameters of the contextThe instructions are as follows:It basically means:AddObserver: forKeyPath: options: context:In the methodContext contextPointers contain arbitrary data that will be passed back to the observer in the corresponding change notification.

Can be achieved bySpecify context as NULL, rely on thekeyPathnamelyKey path stringDetermine the source of the change notification, but this approach can cause problems when the object’s parent class also observes the same key path for different reasons. So you can create a different one for each observed keyPathcontextAnd thusNo string comparison is required at all, allowing for more efficient notification resolution.

Generally speaking, context is mainly used to distinguish the properties of different objects with the same name. Therefore, context can be directly used in KVO callback methods to distinguish, which can greatly improve performance and code readability.

Context uses summary

  • Instead of using context, use keyPath to separate notification sources
// Context is of type nullable void *, should be NULL, Rather than nil [self. The person addObserver: self forKeyPath: @ "Nick" options: NSKeyValueObservingOptionNew context: NULL];Copy the code
  • Use context to distinguish notification sources
Static void *PersonNickContext = &PersonNickContext; static void *PersonNameContext = &PersonNameContext; / / registered observers [self. The person addObserver: self forKeyPath: @ "Nick" options: NSKeyValueObservingOptionNew context:PersonNickContext]; [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext]; //KVO callback - (void) observeforkeypath :(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{ if (context == PersonNickContext) { NSLog(@"%@",change); }else if (context == PersonNameContext){ NSLog(@"%@",change); }}Copy the code

3. Remove the necessity of KVO notification

In the official documentation, forKVO removeThere are several caveatsWhen deleting an observer, keep the following points in mind:

  • Asking to be removed as an observer (if not already registered as an observer) causesNSRangeException. You can toRemoveObserver: forKeyPath: context:Make a call to the responseAddObserver: forKeyPath: options: context:Or, if not feasible in your application, willRemoveObserver: forKeyPath: context:Call inThe try/catch blockInternal handling of potential exceptions.
  • Once released, the observer does not automatically remove itself. The observed object continues to send notifications, ignoring the status of the observer. However, as with any other message sent to a freed object, a change notification triggers a memory access exception. Therefore, you canMake sure the observer deletes itself before it disappears from memory.
  • The protocol cannot ask whether an object is an observer or an observed. Construct code to avoid publishing-related errors. A typical pattern is that during observer initialization (for example,In init or viewDidLoad) register as an observerAnd during the release process (usuallyIn dealloc) logoutIn order toEnsure that messages are added and removed in pairs and in orderAnd indeedThe observer is unregistered before registration and released from memory.

Therefore, in general, KVO registered observers and removed observers need to appear in pairs. If only registered observers are not removed, a crash similar to wild Pointers will occur, as shown in the figure below:

The reason for the crash is becauseKVO observer is not removed after first registration, re-entering the interface will cause the second registration of KVO observer, resulting inDuplicate registrations of KVO observations, and the first notification object is still in memory, has not been released, at this time received a change in the attribute value notification, will appearThe original notification object cannot be found. Only existing notification objects can be foundThat is, the observer of the second KVO registration, so that causesWild pointer - like crash, that is, a wild notification is always kept and is always listening.

Note: The crash case here is implemented by singletons (crash is very likely, not always occur), because the singletons are resident in memory, for general class objects, it is possible not to remove, but in order to prevent online accidents, it is recommended to remove.

***** Terminating app due to uncaught exception ‘NSRangeException’, reason: ‘Cannot remove an observer

for the key path “monitor keyPath” from

because it is not registered as an observer.’**

So registration and removal must come in pairs.

4. Automatic triggering and manual triggering of KVO

KVO observation can be turned on and off in two ways, automatic and manual

/ / automatic switch + (BOOL) automaticallyNotifiesObserversForKey: (nsstrings *) key {/ / if ([key isEqualToString: @ "age"]) {/ / return NO;  // } return YES; }Copy the code
  • Auto switch, returnNO, you can’t listen, returnYES, indicates listening
    • If the returnYESAfter turning on the automatic switch, setName method will be automatically implemented, but users can not manually implement
    • In view of the development of a situation if the observation of a situation can be set up here macro condition differentiation processing
  • When the automatic switch is off, it can passManual switch monitoring
- (void)setName:(NSString *)name{// switch [self willChangeValueForKey:@"name"]; _name = name; [self didChangeValueForKey:@"name"]; }Copy the code

The advantage of using manual switch is that you want to listen to listen, do not want to listen to close, more convenient and flexible than automatic trigger.

5. KVO observation: one-to-many (nested)

One-to-many in KVO observation, which means you can listen for changes to multiple properties by registering a SINGLE KVO observer

Take the download progress as an example. For example, there is a requirement to calculate the current download progress currentProcess based on total download Data and current download currentData. There are two ways to achieve this

  • Respectively to observeTotal downloadstotalDataAnd current downloadscurrentDataTwo properties that calculate the current download progress when one of them changescurrentProcess
  • implementationkeyPathsForValuesAffectingValueForKeyMethod, combine two observations into one observation, i.eView currentProcess
/ / 1, the unity of observation method + (NSSet < > nsstrings * *) keyPathsForValuesAffectingValueForKey: (nsstrings *) key {NSSet * keyPaths = [super keyPathsForValuesAffectingValueForKey:key]; if ([key isEqualToString:@"currentProcess"]) { NSArray *affectingKeys = @[@"totalData", @"currentData"]; keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys]; } return keyPaths; } / / 2, registered KVO observations [self. The person addObserver: self forKeyPath: @ "currentProcess options: (NSKeyValueObservingOptionNew)" context:NULL]; // (void)touch began :(NSSet< uittouch *> *) view withEvent:(UIEvent *)event{self.person.currentdata += 10; self.person.totalData += 1; } //4, removeObserver - (void)dealloc{[self.person removeObserver:self forKeyPath:@"currentProcess"]; }Copy the code

6. KVO looks at mutable arrays

KVO is based on KVC, so if you add data directly to a mutable array, you don’t call a setter. All KVO observations on mutable arrays don’t work like this: [self.person.datearray addObject:@”1″]; Adding elements to an array does not trigger a KVO notification callback.

Self.person. dateArray = [NSMutableArray arrayWithCapacity:1]; [self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL]; //2, KVO callback - (void) observeforkeypath :(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{ NSLog(@"%@",change); } //3, removeObserver - (void)dealloc{[self.person removeObserver:self forKeyPath:@"dateArray"]; } // (void)touch began :(NSSet<UITouch *> *) view withEvent:(UIEvent *)event{[self.person.datearray addObject:@"1"]; // KVO notification callback will not be triggered}Copy the code

In the official KVC documentation, forA collection of mutable arraysType, as described below, to access a collection objectmutableArrayValueForKeyMethod, so thatAdds elements to a mutable arrayIn:

Modify the

Modify the code in 4 as follows:

- (void)touchesBegan:(NSSet< uittouch *> *)touches withEvent:(UIEvent *)event{// KVC set array [[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"]; }Copy the code

As you can see, the element is added to the mutable array:

Where kind represents the type of key value change, which is an enumeration, mainly including the following four types:

typedef NS_ENUM(NSUInteger, NSKeyValueChange) {NSKeyValueChangeSetting = 1, / / a value NSKeyValueChangeInsertion = 2, / / insert NSKeyValueChangeRemoval = 3, / / removed NSKeyValueChangeReplacement = 4, / / replace};Copy the code

General attributes differ from the KVO observations of collections in that their kind are different, for example, attribute name and mutable array

  • The kind of an attribute is usually set

  • A kind of mutable array is usually an insert

Explore the underlying principle of KVO

The test code

In the official KVO user guide, there are the following instructions:

  • KVO was implemented using isa-Swizzling’s technology.

  • As the name implies, the ISA pointer points to the class of objects that maintain the allocation table. The dispatch table essentially contains Pointers to the methods implemented by the class, as well as other data.

  • When an observer is registered for an object’s properties, the ISA pointer to the observed object is modified to point to the intermediate class instead of the real 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 isa Pointers to determine class membership. Instead, you should use the class method to determine the class of an object instance.

Code debugging exploration

1. KVO only observes attributes

In LGPerson, there is a member variable name and an attribute nickName. Register the KVO observation separately.

  • Respectively,Member variable nameAttribute nickNameSign up for KVO Watch:
self.person = [[LGPerson alloc] init];
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
Copy the code
  • KVO notification triggers the operation
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ NSLog (@ "reality: % @ - % @", the self. The person. The nickName, the self. The person - > name); self.person.nickName = @"KC"; self.person->name = @"Cooci"; }Copy the code

The running results are as follows:

conclusion: the KVOMember variables are not observed.Look at attributes onlyThe difference between attributes and member variables is thatProperty has one more setter methodAnd theKVO happens to look at setter methods.

2. Middle class

According to the official documentation, after registering a KVO observer, the isa pointer to the observed object changes

  • Before registering an observer: Instance objectpersontheisaPointer toLGPerson
  • After observer registration: Instance objectpersontheisaPointer toNSKVONotifying_LGPerson

To sum up, after the observer is registered, the ISA pointer of the instance object changes from the LGPerson class to the NSKVONotifying_LGPerson intermediate class, that is, the ISA pointer of the instance object changes.

2-1. Determine whether the intermediate class is a derived class or a subclass.

So what does this dynamically generated middle class NSKVONotifying_LGPerson have to do with the LGPerson class? The following is verified by code

We can get the associated LGPerson class using the following encapsulation method:

#pragma mark - (void)printClasses:(Class) CLS {int count = objc_getClassList(NULL, 0); // Create an array containing the given object NSMutableArray *mArray = [NSMutableArray arrayWithObject: CLS]; Class* classes = (Class*)malloc(sizeof(Class)*count); objc_getClassList(classes, count); for (int i = 0; i<count; i++) { if (cls == class_getSuperclass(classes[i])) { [mArray addObject:classes[i]]; } } free(classes); NSLog(@"classes = %@", mArray); } / call / * * * * * * * * * * * * * * * * [self printClasses: [LGPerson class]].Copy the code

The print result is as follows:

The result shows that NSKVONotifying_LGPerson is a subclass of LGPerson.

2-2. What is in the middle class?

You can get all the methods in the NSKVONotifying_LGPerson class by using the following method

#pragma mark -ivar-property - (void)printClassAllMethod:(Class) CLS {unsigned int count = 0; Method *methodList = class_copyMethodList(cls, &count); for (int i = 0; i<count; i++) { Method method = methodList[i]; SEL sel = method_getName(method); IMP imp = class_getMethodImplementation(cls, sel); NSLog(@"%@-%p",NSStringFromSelector(sel),imp); } free(methodList); } / call / * * * * * * * * * * * * * * * * [self printClassAllMethod: objc_getClass (" NSKVONotifying_LGPerson ")];Copy the code

The following output is displayed:

(setNickName, class, dealloc, _isKVOA) (setNickName, class, dealloc, _isKVOA)

  • Let’s create a new classLGStudentTo inherit fromLGPersonTo printLGStudentClass
  • inLGStudentThe rewritesetNickNameMethod, obtainLGStudentClass

The comparison with the intermediate class shows that only overridden methods are iterated through the list of methods in the subclass, and inherited methods are not iterated through the subclass.

  • To obtainLGPersonandNSKVONotifying_LGPersonList of methods for comparison

To sum up, it can be concluded that:

  • NSKVONotifying_LGPersonThe middle classrewritetheThe parent class LGPersonthesetNickNamemethods
  • NSKVONotifying_LGPersonThe middle classrewritetheThe base class NSObjecttheClass, dealloc, _isKVOAmethods
    • Among themdeallocIs the release method
    • _isKVOACheck whether the current class is kVO
2-3. Who is isa pointing to when the observer is removed from dealloc, and will the intermediate class be destroyed?
  • Before removing the observer: The ISA point of the instance object is stillNSKVONotifying_LGPersonThe middle class

  • After removing the observer: the ISA point of the instance object is changed toLGPersonclass

So, after removing the KVO observer, the isa pointer changes from NSKVONotifying_LGPerson to LGPerson.

Details: If registerednameIn thedeallocNot to removename,isaOr toLGPerson

Does the middle class NSKVONotifying_LGPerson exist after it was created, after it removed the observer from the dealloc method?

  • Print on the previous interfaceLGPersonIs used to determine whether the intermediate class is destroyed

As you can see from the printable results of the subclasses, once the intermediate classes are generated, they are not removed, they are not destroyed, and they are still in memory — mainly because of the idea of reuse, that is, the intermediate classes are registered in memory, so they are always there for future reuse.

conclusion

To sum up, the intermediate class is explained as follows:

  • Instance objectsisaThe point toAfter registering as a KVO observerBy theThe original classChange toPoint to the middle class
  • The middle classRewrote the observationSetter methods for property,class,dealloc,_isKVOAmethods
  • deallocMethod, removeKVO observerAfter that, the instance objectisaPoint to theThe middle classChange toThe original class
  • The middle classSince it was created, it has beenIt is stored in memory and will not be destroyed

Custom KVO

The complete code

Note: about the article prefix CJL or LG, CJL is a reference to the author’s article figure and code in this article, some of the points I added later are used lg does not need to tangle with this detail

The process of customized KVO is consistent with the system, but some optimization has been made for some parts on the basis of the system.

  • 1, will beRegistration and ResponseBy functional programming, i.eblockMethods are combined together
  • 2, get rid of the tedious trilogy system, achieveKVO automatic destruction mechanism
    Register observer [self.person addObserver:self forKeyPath:@"nickName"options:(NSKeyValueObservingOptionNew) context:NULL]; KVO callback - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
        NSLog(@"% @",change); } Remove observer - (void)dealloc{
        [self.person removeObserver:self forKeyPath:@"nickName"];
    }
    Copy the code

In the system, registered observer and KVO response belong to responsive programming, which is written separately. In the custom KVO, the logic of registration and callback is combined in the form of block for better code coordination, that is, functional programming is adopted, which is still divided into three parts:

  • Registered observer
    //********* define block********* typedef void(^LGKVOBlock)(id observer,NSString *keyPath,id oldValue,id newValue); / / * * * * * * * * * registered observers * * * * * * * * * - (void) lg_addObserver: (NSObject *) observer forKeyPath keyPath: (nsstrings *) handleBlock:(LGKVOBlock)block;Copy the code
  • KVO responseThis part is mainly done by rewritingsetterMethod, in the setter method of the intermediate class, passesblockIs passed to the outside for a response
  • Remove observer
    / / * * * * * * * * * remove observer * * * * * * * * * - (void) lg_removeObserver: (NSObject *) observer forKeyPath (keyPath nsstrings *);Copy the code

Create NSObject class LGKVO, mainly implementing custom KVO methods in this file.

Registered observer

In the registered observer method, there are the following operations:

1. Determine whether the setter method for the current observation value keyPath exists

# pragma mark - verify whether there is a setter method - (void) judgeSetterMethodFromKeyPath keyPath: (nsstrings *) {Class superClass = object_getClass(self); SEL setterSelector = NSSelectorFromString(setterForGetter(keyPath)); Method setterMethod = class_getInstanceMethod(superClass, setterSelector); if (! setterMethod) { @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString StringWithFormat :@"CJLKVO - no setter method for the current %@ ", keyPath] userInfo:nil]; }} #pragma mark - pragma mark ===>>> setKey: static NSString *setterForGetter(NSString *getter){ if (getter.length <= 0) { return nil; } NSString *firstString = [[getter substringToIndex:1] uppercaseString]; NSString *leaveString = [getter substringFromIndex:1]; return [NSString stringWithFormat:@"set%@%@:",firstString,leaveString]; }Copy the code

2,Dynamically subclassing, will need to be rewrittenclassMethod is added to the intermediate class

# pragma mark - dynamic subclassing - (Class) createChildClassWithKeyPath keyPath: (nsstrings *) {/ / access to the original Class name nsstrings * oldClassName = NSStringFromClass([self class]); / / stitching new class name nsstrings * newClassName = [nsstrings stringWithFormat: @ "% @ % @", kCJLKVOPrefix, oldClassName]; //kLGKVOPrefix = @"LGKVONotifying_"; // get newClass newClass = NSClassFromString(newClassName); If (newClass) return newClass; if (newClass) return newClass; //2.1 Application class /** * If memory does not exist, create and generate * Parameter 1: parent class * Parameter 2: name of new class * Parameter 3: */ newClass = objc_allocateClassPair([self class], newClassName.utf8String, 0); //2.2 Register class objc_registerClassPair(newClass); LGPerson SEL classSel = @selector(class); Method classMethod = class_getInstanceMethod([self class], classSel); const char *classType = method_getTypeEncoding(classMethod); class_addMethod(newClass, classSel, (IMP)cjl_class, classType); return newClass; } / / * * * * * * * * * class methods * * * * * * * * * # pragma mark - rewrite the class method, Class cjl_class(id self, SEL _cmd){return class_getSuperclass(object_getClass(self)); // Fetching from [self class] creates an infinite loop}Copy the code

3,Isa toChange from the original class toPoint to the middle class

object_setClass(self, newClass);
Copy the code

4,Save the informationTo demonstrate the array used here, you can also use a map, which needs to be created for informationmodelModel class

/ / * * * * * * * * * KVO information model class / * * * * * * * * * # pragma mark information model class @ interface CJLKVOInfo: NSObject @property(nonatomic, weak) NSObject *observer; @property(nonatomic, copy) NSString *keyPath; @property(nonatomic, copy) LGKVOBlock handleBlock; - (instancetype)initWithObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(LGKVOBlock)block; @end @implementation CJLKVOInfo - (instancetype)initWithObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(LGKVOBlock)block{ if (self = [super init]) { _observer = observer; _keyPath = keyPath; _handleBlock = block; } return self; } @ the end / / * * * * * * * * * to save information * * * * * * * * * / / - save multiple information CJLKVOInfo * info = [[CJLKVOInfo alloc] initWithObserver: the observer forKeyPath:keyPath handleBlock:block]; NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kCJLKVOAssociateKey)); if (! MArray) {// If mArray does not exist, create a new mArray = [NSMutableArray arrayWithCapacity:1]; objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kCJLKVOAssociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } [mArray addObject:info];Copy the code

The complete registered observer code is as follows:

#pragma mark - Register observer - functional programming - (void)cjl_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(LGKVOBlock)block{
    
    //1. Verify that setter methods exist
    [self judgeSetterMethodFromKeyPath:keyPath];
    
    // Save the information
    //- Save multiple messages
    CJLKVOInfo *info = [[CJLKVOInfo alloc] initWithObserver:observer forKeyPath:keyPath handleBlock:block];
    // Use array storage -- you can also use map
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kCJLKVOAssociateKey));
    if(! mArray) {// If mArray does not exist, create it again
        mArray = [NSMutableArray arrayWithCapacity:1];
        objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kCJLKVOAssociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    [mArray addObject:info];
    
    / / judgment automaticallyNotifiesObserversForKey method returns the Boolean value
    BOOL isAutomatically = [self cjl_performSelectorWithMethodName:@"automaticallyNotifiesObserversForKey:" keyPath:keyPath];
    if(! isAutomatically)return;
    
    // create a subclass.
    /* 2.1 Applying for classes 2.2 Registering 2.3 Adding methods (override setter methods) */
    Class newClass = [self createChildClassWithKeyPath:keyPath];
    //3
    object_setClass(self, newClass);
    
    // Override setter methods
    / / for sel
    SEL setterSel = NSSelectorFromString(setterForGetter(keyPath));
    // Get setter instance methods
    Method method = class_getInstanceMethod([self class].setterSel);
    // Method signature
    const char *type = method_getTypeEncoding(method);
    // Add a setter method
    class_addMethod(newClass, setterSel, (IMP)cjl_setter, type); 
}
Copy the code

Pay attention to the point

  • aboutobjc_msgSendCheck off:target -> Build Setting -> Enable Strict Checking of objc_msgSend CallsSet toNO

  • The class method must be overridden to be consistent with the system’s external classes.

    First add class and ‘ ‘source code implementation:

    - (Class)class {
        return object_getClass(self);
    }
    
    /*********************************************************************** * object_getClassName. * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
    const char *object_getClassName(id obj)
    {
        return class_getName(obj ? obj->getIsa() : nil);
    }
    
    
    /***********************************************************************
    * object_getClass.
    * Locking: None. If you add locking, tell gdb (rdar://7516456).
    **********************************************************************/
    Class object_getClass(id obj)
    {
        if (obj) return obj->getIsa();
        else return Nil;
    }
    Copy the code
    • In KVO of the system, the class of the instance object Person remains CJLPerson until and after the observer is added

      • throughclassPrint andobject_getClassNamePrint the following: System KVOWill automatically be in the middle classNSKVONotifying_LGPersonrewriteclassMethods usingobject_getClassNameIs the real class name obtained
    • Custom KVO

      • ifClass not overwrittenMethod, custom KVO before and after registration of the instance object PersonclassYou will see that it is inconsistent, after registrationCJLKVONotifying_LGPerson
      • The custom KVO method overrides the post-class method, and the display of the instance object class is consistent with the display of the system before and after registering the observer

KVO response

This is done by dynamically adding setter methods to subclasses in order to send messages to the parent class in the setter method informing it of changes in property values.

5. Add setter method overrides to subclasses (mainly in registered observer methods)

Sel setterSel = NSSelectorFromString(setterForGetter(keyPath)); Method Method = class_getInstanceMethod([self class], setterSel); // Method signature const char *type = method_getTypeEncoding(method); // add a setter method class_addMethod(newClass, setterSel, (IMP)cjl_setter, type);Copy the code

6, through the systemobjc_msgSendSuperCast custom message sendingcjl_msgSendSuper

Objc_msgSendSuper (*cjl_msgSendSuper)(void *, SEL, id) = (void *)objc_msgSendSuper; Struct objc_super superStruct = {.receiver = self, Super_class = class_getSuperclass(object_getClass(self))); // The receiver of the message is the current self.super_class = class_getSuperclass(object_getClass(self)). // Call the custom message sending function cjl_msgSendSuper(&superStruct, _cmd, newValue);Copy the code

7, tell VC to respond: get information, pass through the block

/*-- function programming */ NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd)); id oldValue = [self valueForKey:keyPath]; NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kCJLKVOAssociateKey)); for (CJLKVOInfo *info in mArray) { NSMutableDictionary<NSKeyValueChangeKey, id> *change = [NSMutableDictionary dictionaryWithCapacity:1]; if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) { info.handleBlock(info.observer, keyPath, oldValue, newValue); }}Copy the code

The complete setter method code is as follows:

Static void cjl_setter(id self, SEL _cmd, id newValue){NSLog(@" come :%@",newValue); Objc_msgSendSuper (*cjl_msgSendSuper)(void *, SEL, id) = (void *)objc_msgSendSuper; Struct objc_super superStruct = {.receiver = self, Super_class = class_getSuperclass(object_getClass(self))); // The receiver of the message is the current self.super_class = class_getSuperclass(object_getClass(self)). // Call the custom message sending function cjl_msgSendSuper(&superStruct, _cmd, newValue); NSString keyPath = getterForSetter(NSStringFromSelector(_cmd)); id oldValue = [self valueForKey:keyPath]; NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kCJLKVOAssociateKey)); for (CJLKVOInfo *info in mArray) { NSMutableDictionary<NSKeyValueChangeKey, id> *change = [NSMutableDictionary dictionaryWithCapacity:1]; if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) { info.handleBlock(info.observer, keyPath, oldValue, newValue); }}}Copy the code

Remove observer

To avoid constantly calling the removeObserver method from the outside, implement automatic observer removal in custom KVO

8, implementation,cjl_removeObserver:forKeyPath:Method, mainly to empty the array, and ISA pointing to changes

- (void)cjl_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{// empty the array NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kCJLKVOAssociateKey)); if (mArray.count <= 0) { return; } for (CJLKVOInfo *info in mArray) { if ([info.keyPath isEqualToString:keyPath]) { [mArray removeObject:info]; objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kCJLKVOAssociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC); }} if (marray. count <= 0) {//isa returns to Class superClass = [self Class]; object_setClass(self, superClass); }}Copy the code

9. Override the dealloc method in a subclass, which will be called automatically when the subclass is destroyeddeallocMethod (added in the method for dynamically subclassing)

# pragma mark - dynamic subclassing - (Class) createChildClassWithKeyPath keyPath: (nsstrings *) {/ /... // add dealloc method SEL deallocSel = NSSelectorFromString(@"dealloc"); Method deallocMethod = class_getInstanceMethod([self class], deallocSel); const char *deallocType = method_getTypeEncoding(deallocMethod); class_addMethod(newClass, deallocSel, (IMP)cjl_dealloc, deallocType); return newClass; } / / * * * * * * * * * * * * override dealloc methods * * * * * * * * * * * * * void cjl_dealloc (id self, SEL _cmd) {NSLog (@ "to"); Class superClass = [self class]; object_setClass(self, superClass); }Copy the code

Its principle is as follows: CJLPerson sends a message to dealloc, which will automatically go to the overwritten cjL_dealloc method. Cjl_dealloc overwrites CJLPerson’s dealloc method, so it will automatically remove the observer

conclusion

To sum up, custom KVO can be roughly divided into the following steps

  • Register observer & response

    • 1. Verify that setter methods exist
    • 2. Save information
    • 3, dynamic generation of subclasses, need to rewriteclass,settermethods
    • 4. Send messages to the parent class in setter methods of subclasses, that is, send custom messages
    • 5. Let the observer respond
  • Remove observer

    • 1, changeIsa toFor the original class
    • 2, override the subclassdeallocmethods

expand

The above customized logic is not perfect. It only describes the general logic of the KVO underlying implementation. For details, you can refer to FBKVOController of Facebook’s KVO tripartite framework

reference

This article learns and references iOS- Underlying principles 23: KVO underlying principles, thanks in this

The article lists