First, the underlying implementation principle of KVO
Sample code:
@interface LGViewController () @property (nonatomic, strong) LGPerson *person; @end @implementation LGViewController - (void)viewDidLoad { [super viewDidLoad]; self.person = [[LGPerson alloc] init]; [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL]; } - (void) Touch began :(NSSet< touch *> *)touches :(UIEvent *) view {NSLog(@@@- % %@",self.person.name); self.person.name = @"KC"; } #pragma mark - KVO callback - (void) observeforkeypath :(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{ NSLog(@"%@",change); } @endCopy the code
The implementation of KVO actually takes advantage of the OC Runtime mechanism. When an instance object (such as self.person above) adds an observer, The underlying class is dynamically added based on the class to which the instance object belongs (the dynamically added class name is preceded by the NSKVONotifying_ prefix), which is inherited from the original class. The underlying implementation of the above example is as follows:
When self.person adds an observer, the runtime dynamically generates a class called NSKVONotifying_LGPerson. This class inherits from LGPerson and overwrites the following instance methods: If you override the class method, if you don’t override it, you’re going to get NSKVONotifying_LGPerson, and you’re going to get the same LGPerson class. Apple did this to hide the implementation details of KVO. Rewrite the dealloc method and put some finishing touches in it. Overwrite the _isKVOA method, which is a private method and we don’t have to worry about it. Override setter methods for the listened property. The example above only listens for the name property, so just override setName:. Override setter is the key to implementing KVO. In setter methods, the _NSSet***ValueAndNotify method is called from the Foundation framework. For example, __NSSetIntValueAndNotify is an int property, and the contained type is listed later. Then change the isa of the self.person instance object to point to NSKVONotifying_LGPerson (originally to the LGPerson class). When we set is to monitor the value of the attribute. Self person. Name = @ “KC”, is to call the elegantly-named setName: method of front said elegantly-named setName: method was changed, so call is actually _NSSetObjectValueAndNotify this method. This method implementation is not open source, we can not know the specific implementation, but we can guess the implementation process is as follows: first call [self willChangeValueForKey:@”name”]; This method. Then call the implementation of the original setter method (such as _name = name;). ; Call [self didChangeValueForKey:@”name”]; This method. Finally, in the didChangeValueForKey: method, the observer’s observeValueForKeyPath: ofObject: change: Context: method is called to notify the observer that the property value has changed.
Second, the verification of KVO underlying implementation
2.1 How do we know that a class was dynamically added when adding an observer?
Official documents:
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.
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. The value of the ISA pointer does not necessarily reflect the actual class of the instance
First, let’s verify this intermediate class, and we can see in the SettingsKVO
After that, the object’s class is already pointed toNSKVONotifying_LGPerson
- If the original class is
A
, then the generated derived class is namedNSKVONotifying_A
- If we create a new one called
"NSKVONotifying_A"
Class, will find the systemKVO
(Keywatch) does not work because the system dynamically creates a listener name when it registers the listenerNSKVONotifying_A
The middle class is already created, so the class that looks at the key doesn’t work.
KVO failed to allocate class pair for name NSKVONotifying_Person, automatic key-value observing will not work for this class
KVO failed to allocate space for the class named NSKVONotifying_Person. Automatic key observation will not work for this class
2.2 How do I know which methods are overridden?
Here we need to use some runtime API to retrieve the list of methods stored in a class object. We will encapsulate a method to retrieve this information, and then print out the list of methods before and after listening.
#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(@"%@",NSStringFromSelector(sel)); } free(methodList); }Copy the code
Did not set KVO before
self.person = [[LGPerson alloc] init];
[self printClassAllMethod:object_getClass(self.person)];
Copy the code
The print result is as follows:
CopyWithZone [74222:1238967] copyWithZone [74222:1238967] Cxx_destruct [74222:1238967]. Cxx_destruct 2020-10-27 20:14:43.273091+0800 003-- KVO[74222:1238967] name 2020-10-27 20:14:43.273197+0800 003-- KVO[74222:1238967] setName:Copy the code
After setting KVO
[self.person lg_addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
[self printClassAllMethod:object_getClass(self.person)];
Copy the code
The print result is as follows:
2020-10-27 20:14:49.319536+0800 003-- custom KVO[74222:1238967] setName:Copy the code
The core of KVO is to dynamically generate a class that inherits from the original class, and then point the isa of the instance object to that class. The setter method that listens to the property is then overridden, calling willChangeValueForKey before and didChangeValueForKey after the original setter method.
So the key to determining whether an operation triggers KVO is whether it calls setter methods that listen for properties. For example, self.person.name = @”KC”; This way is to call setter methods, so it fires KVO. However, KVO is not triggered in the following ways:
- By assigning values to member variables,
self.person->_name = @"KC";
(The premise is that you need to change the member variable_name
To expose to access outside), this method is not triggeredKVO
Because it’s not calledsetter
Methods. - For collection types, updates to the data in the collection are not triggered
KVO
. Such as[self.person.dateArray addObject:@"1"]
Again, it doesn’t call that operationsetDateArray:
Method, so it does not fireKVO
。 - If the listening property is a custom
OC
Object, let’s say there’s aLGDog
There’s one in the classage
Properties,LGPerson
There’s one in the classLGDog
Type attributesdog
If we listen indog
This property, whendog
的age
Does not trigger when changes occurKVO
Because it doesn’t callsetDog:
Methods.