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 both
It's all observer mode
Both are used forListening to the
- 2, all can
Implement one-to-many
The operation of the
- 1, the realization principle of both
-
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,
NSNotification
theSend to monitor
(POST) we can control,kvo
bysystem
Control. - 3,
KVO
canRecord changes between old and new values
- 1.
Precautions for using KVO
1. Basic use
The basic use of KVO can be divided into 3 steps:
- Registered observer
addObserver: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 callback
observeValueForKeyPath: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 observer
removeObserver:forKeyPath:context
[self.person removeObserver:self forKeyPath:@"nick" context:NULL];
Copy the code
2. Use context
In the official documentation, forParameters of the context
The instructions are as follows:It basically means:AddObserver: forKeyPath: options: context:
In the methodContext context
Pointers 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 thekeyPath
namelyKey path string
Determine 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 keyPathcontext
And 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 remove
There 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) causes
NSRangeException
. 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 block
Internal 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 observer
And during the release process (usuallyIn dealloc) logout
In order toEnsure that messages are added and removed in pairs and in order
And 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 found
That 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, return
NO
, you can’t listen, returnYES
, indicates listening- If the return
YES
After 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
- If the return
- When the automatic switch is off, it can pass
Manual 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 observe
Total downloadstotalData
And current downloadscurrentData
Two properties that calculate the current download progress when one of them changescurrentProcess
- implementation
keyPathsForValuesAffectingValueForKey
Method, 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 arrays
Type, as described below, to access a collection objectmutableArrayValueForKey
Method, so thatAdds elements to a mutable array
In:
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 name
和Attribute nickName
Sign 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 only
The difference between attributes and member variables is thatProperty has one more setter method
And 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 object
person
theisa
Pointer toLGPerson
- After observer registration: Instance object
person
theisa
Pointer 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 class
LGStudent
To inherit fromLGPerson
To printLGStudent
Class - in
LGStudent
The rewritesetNickName
Method, obtainLGStudent
Class
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 obtain
LGPerson
andNSKVONotifying_LGPerson
List of methods for comparison
To sum up, it can be concluded that:
NSKVONotifying_LGPerson
The middle classrewrite
theThe parent class LGPerson
thesetNickName
methodsNSKVONotifying_LGPerson
The middle classrewrite
theThe base class NSObject
theClass, dealloc, _isKVOA
methods- Among them
dealloc
Is the release method _isKVOA
Check whether the current class is kVO
- Among them
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 still
NSKVONotifying_LGPerson
The middle class -
After removing the observer: the ISA point of the instance object is changed to
LGPerson
class
So, after removing the KVO observer, the isa pointer changes from NSKVONotifying_LGPerson to LGPerson.
Details: If registered
name
In thedealloc
Not to removename
,isa
Or 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 interface
LGPerson
Is 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 objects
isa
The point toAfter registering as a KVO observer
By theThe original class
Change toPoint to the middle class
The middle class
Rewrote the observationSetter methods for property
,class
,dealloc
,_isKVOA
methodsdealloc
Method, removeKVO observer
After that, the instance objectisa
Point to theThe middle class
Change toThe original class
The middle class
Since 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 be
Registration and Response
By functional programming, i.eblock
Methods are combined together - 2, get rid of the tedious trilogy system, achieve
KVO 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 response
This part is mainly done by rewritingsetter
Method, in the setter method of the intermediate class, passesblock
Is passed to the outside for a responseRemove 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 rewrittenclass
Method 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 to
Change from the original class toPoint to the middle class
object_setClass(self, newClass);
Copy the code
4,Save the information
To demonstrate the array used here, you can also use a map, which needs to be created for informationmodel
Model 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
-
about
objc_msgSend
Check off:target -> Build Setting -> Enable Strict Checking of objc_msgSend Calls
Set 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
- through
class
Print andobject_getClassName
Print the following:System KVO
Will automatically be in the middle classNSKVONotifying_LGPerson
rewriteclass
Methods usingobject_getClassName
Is the real class name obtained
- through
-
Custom KVO
- if
Class not overwritten
Method, custom KVO before and after registration of the instance object Personclass
You 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
- if
-
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_msgSendSuper
Cast 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 destroyeddealloc
Method (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 rewrite
class
,setter
methods - 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, change
Isa to
For the original class - 2, override the subclass
dealloc
methods
- 1, change
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