1. Use of KVO

KVO (key-value Observing), also known as key-value monitoring, can be used to monitor the change of the property Value of an object. KVO is easier to use. Define a Student class with two properties, declare an instance object, and add an observer to listen for a property. When the monitored property changes, the observeValueForKeyPath: ofObject calls the observer: Change: context: method. Remove the observer when it is not needed.

// Student.h文件
@interface Student : NSObject
@property (nonatomic , strong) NSString *name;
@property (nonatomic , strong) NSMutableArray booksArr;
@end
Copy the code
// Use Student files - (void)test{ self.stu1 = [[Student alloc] init]; // addObserver to listen for name changes [self.stu1 addObserver:selfforKeyPath:@"name"
                   options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                   context:NULL];

    NSLog(@"Before name change");
    self.stu1.name = @"Jack";
    NSLog(@"Name changed"); } // callback when the listener property changes - (void) observeforkeypath :(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{ NSLog(@"keyPath:%@,change-->%@",keyPath,change); } - (void)dealloc{// removeObserver [self.stu1 removeObserver:selfforKeyPath:@"name"]; } / / * * * * * * * * * * * * * * * * * * * * printing result * * * * * * * * * * * * * * * * * * * * 2020-01-05 09:42:32. 371008 + 0800 GCDDemo (13375-567451) before the name change KeyPath :name,change-->{kind = 1; keyPath:name,change-->{kind = 1; new = Jack; old ="<null>"; } 2020-01-05 09:42:32.371895+0800 GCDDemo[13375:567451] name after changeCopy the code

2. The underlying implementation principle of KVO

When an observer is added to an instance object (such as self.stu1), a class is dynamically added according to the class name of the instance object (NSKVONotifying_ is added before the name of the original class). This class inherits from the original class. The underlying implementation of the above example is as follows:

  • self.stu1When you add an observer, the bottom layer usesruntimeDynamically generate a calledNSKVONotifying_StudentClass, which inherits fromStudentClass, and rewrite the following instance methods:
    • rewriteclassMethod. If you call this method without overwriting it, it returnsNSKVONotifying_StudentThis class, when overwritten, returns the originalStudentClass. Apple is doing this to hideKVOImplementation details of.
    • rewritedeallocMethod, do some finishing touches in this method.
    • rewrite_isKVOAMethod, this is a private method, we don’t have to worry about it.
    • Overrides the listened propertyA setter methodThe above case was only monitorednameProperty, so it just needs to be overriddensetName:Methods. Overwriting the setter is the implementationKVOThe key, in the setter method is actually calledFoundationUnder the framework of_NSSet***ValueAndNotifyMethod (*** means not a fixed one, this depends on the type of the property being listened on, for example, __NSSetIntValueAndNotify if the property is of type int, and the contained type will be listed later).
  • thenself.stu1Of this instance objectisaTo point toNSKVONotifying_Student(Originally pointing toStudentClass).
  • When we set the value of the listened propertyself.stu1.name = @"Jack", is calledsetName:The method, as mentioned earliersetName:The method has been overridden, so what is actually called is_NSSetObjectValueAndNotifyThis method. The implementation of this method is not open source, apple can not know its specific implementation, but you can guess that the implementation process is roughly as follows:
    • First call[self willChangeValueForKey:@"name"];This method.
    • Then call the implementation of the original setter method (e.g_name = name;);
    • Call again[self didChangeValueForKey:@"name"];This method.
    • Finally, indidChangeValueForKey:The method that calls the observerobserveValueForKeyPath: ofObject: change: context:Method to notify the observer that the property value has changed.

The _NSSet***ValueAndNotify method list is as follows:

 _NSSetBoolValueAndNotify 
 _NSSetCharValueAndNotify 
 _NSSetDoubleValueAndNotify 
 _NSSetFloatValueAndNotify 
 _NSSetIntValueAndNotify 
 _NSSetLongLongValueAndNotify 
 _NSSetLongValueAndNotify 
 _NSSetObjectValueAndNotify 
 _NSSetPointValueAndNotify 
 _NSSetRangeValueAndNotify 
 _NSSetRectValueAndNotify 
 _NSSetShortValueAndNotify 
 _NSSetSizeValueAndNotify 
 _NSSetUnsignedCharValueAndNotify 
 _NSSetUnsignedIntValueAndNotify 
 _NSSetUnsignedLongLongValueAndNotify 
 _NSSetUnsignedLongValueAndNotify 
 _NSSetUnsignedShortValueAndNotify 
Copy the code

3. Verification of KVO underlying implementation

3.1 How do we know that a class was dynamically added when adding an observer?

All we need to do is print out the class that the instance object belongs to before and after adding the observer. As mentioned earlier, the dynamically added class overwrites the class method, so we can’t use this method to get an instance object’s class. Instead, we use the Runtime object_getClass() API to get it:

- (void)test1{
    self.stu1 = [[Student alloc] init];
    
    NSLog(@"[self.stu1 class] -->%@",[self.stu1 class]);
    NSLog(@"-object_getClass (self.stu1) -->%@",object_getClass(self.stu1));

    [self.stu1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];

    NSLog(@"[self.stu1 class] -->%@",[self.stu1 class]);
    NSLog(@"-object_getClass (self.stu1) -->%@",object_getClass(self.stu1)); } / / * * * * * * * * * * * * * * * * * * * * printing result * * * * * * * * * * * * * * * * * * * * 2020-01-05 10:51:00. 584299 + 0800 GCDDemo [14497-600230] observation before - [self. Stu1 Class] -->Student 2020-01-05 10:51:00.584690+0800 GCDDemo[14497:600230] -->Student 2020-01-05 10:51:00.584690+0800 GCDDemo[14497:600230 Stu1 class] -->Student 2020-01-05 10:51:00.593064+0800 -object_getClass (self.stu1) -->NSKVONotifying_StudentCopy the code

3.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.

- (void)test2{
    self.stu1 = [[Student alloc] init];

    NSLog(@"List of methods before observation -->%@",[self methodNamesOfClass:object_getClass(self.stu1)]);

    [self.stu1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];

    NSLog(@"List of observed methods -->%@",[self methodNamesOfClass:object_getClass(self.stu1)]); Return - (NSString *)methodNamesOfClass:(Class) CLS {unsigned int count; Method *methodList = class_copyMethodList(CLS, &count); NSString *methodNamesStr = @""; // Iterate over the list of methods to concatenate method names into stringsfor (int i = 0; i < count; i++) {
        Method method = methodList[i];
        NSString *methodName = NSStringFromSelector(method_getName(method));
        methodNamesStr = [methodNamesStr stringByAppendingFormat:@"%@ ,",methodName]; } // Free (methodList);returnmethodNamesStr; } / / * * * * * * * * * * * * * * * * * * * * printing result * * * * * * * * * * * * * * * * * * * * 2020-01-05 10:56:43. 077817 + 0800 GCDDemo (14606-603376) List of methods before observation -->.cxx_destruct,name,setName: ,age ,setAge:, 2020-01-05 10:56:43.078483+0800 GCDDemo[14606:603376]setName: ,class ,dealloc ,_isKVOA ,
Copy the code

3.3 How do I know which setter method is called to override?

Here we also need the Runtime API, first getting the Method of the setter Method by calling class_getInstanceMethod(), and then calling method_getImplementation() to get the IMP of the setter Method.

But we first print IMP address, want to see IMP specific information we need to hit a breakpoint to call up LLDB, and then use LLDB to print specific information. For example, if the IMP address before listening is 0x10967D4c0, you can enter P (IMP)0x10967d4c0 in LLDB to print specific information. Can be seen from the following setter method is normal before listening, becomes _NSSetObjectValueAndNotify after listening.

- (void)test1{
    self.stu1 = [[Student alloc] init];

    NSLog(@"Setmethod IMP before listening -->%p",[self IMPWithSelector:@selector(setName:)]);

    [self.stu1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];

    NSLog(@"Setmethod IMP-->%p",[self IMPWithSelector:@selector(setName:)]); } // get a method IMP - (IMP)IMPWithSelector: SEL selector{Class CLS = object_getClass(self.stu1); Method methon = class_getInstanceMethod(cls, selector); IMP imp = method_getImplementation(methon);returnimp; } / / * * * * * * * * * * * * * * * * * * * * printing result * * * * * * * * * * * * * * * * * * * * 2020-01-05 11:25:40. 485792 + 0800 GCDDemo (15032-617260) Setmethod IMP-->0x10967d4c0 2020-01-05 11:25:40.489656+0800 GCDDemo[15032:617260] --> 0x7FFF25701C8A (lldb) p (IMP)0x10967d4c0 (IMP)$0 = 0x000000010967d4c0 (GCDDemo`-[Student setName:] at Student.h:15)
(lldb) p (IMP)0x7fff25701c8a
(IMP) The $1 = 0x00007fff25701c8a (Foundation`_NSSetObjectValueAndNotify)
Copy the code

4. KVO summary

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.stu1.name = @”Jack”; 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.stu1->_name = @"Jack";This method will not trigger if the member variable _name is exposed before it can be accessed externallyKVOBecause it doesn’t call the setter method.
  • For collection types, updates to the data in the collection are not triggeredKVO. Such as[self.stu1.booksArr addObject:@"book1"]Again, it doesn’t call that operationsetBooksArr:Method, so it does not fireKVO.
  • If the property being listened on is a custom OC object, such as aDogThere’s one in the classageProperties,StudentThere’s one in the classDogType attributesdogIf we listen indogThis property, whendogtheageDoes not trigger when changes occurKVOBecause it doesn’t callsetDog:Methods.

In each of these cases, if we want to trigger KVO as well, we can do it manually by adding willChangeValueForKey and didChangeValueForKey before and after the original methods. For example, in this last example, we could write:

[self.stu1 willChangeValueForKey:@"dog"];
self.stu1.dog.age = 3;
[self.stu1 didChangeValueForKey:@"dog"];
Copy the code

Finally, setting property values via KVC also triggers KVO. For example [self.stu1 setValue:@”Jack” forKey:@”name”]; To trigger KVO, apple calls willChangeValueForKey and didChangeValueForKey in the KVC implementation.