This post was originally posted on my blog

preface

  • KVO, commonly known as KeyValueObserving, is an event notification mechanism provided by Apple. Allows an object to listen for changes to specific properties of another object and receive events when they change. Properties work because of the implementation mechanism of KVO, which is generally supported by objects that inherit from NSObject.
  • Both KVC and KVO are key-value programming and the underlying implementation mechanism is ISA-Swizzing.
  • Both KVO and NSNotificationCenter are implementations of the Observer pattern in iOS. KVO is non-intrusive to the monitored object and can be monitored without modifying its internal code.
  • KVO can listen for changes in individual properties as well as collection objects. The proxy object is obtained through methods such as mutableArrayValueForKey: of KVC. When the internal object of the proxy object changes, the method monitored by KVO is called back. The collection object contains NSArray and NSSet.

Realize the principle of

  • KVO is implemented using isA-Swizzling technology (this sentence is the main point of the entire KVO implementation).
  • Create an intermediate class from the original class at run time, which isa subclass of the original class, and dynamically modify the isa of the current object to point to the intermediate class. When modifying an instance object’s properties, the Foundation framework’s _NSSetXXXValueAndNotify function is called, which calls willChangeValueForKey: Then call the original setter method of the parent class to modify the value, and finally didChangeValueForKey:. DidChangeValueForKey internal will trigger the listener (Oberser) surveillance method observeValueForKeyPath: ofObject: change: context:
  • And rewrite the class method to return the class of the original class.

The use of the KVO

Method of use

  1. Method through the addObserver: forKeyPath: options: context: registered observers, the observer can receive keyPath property change events.
  2. Implementation in the observer observeValueForKeyPath: ofObject: change: context: method, after the keyPath attributes change, KVO callback this method to notify the observer.
  3. When the observer don’t need to monitor, you can call removeObserver: forKeyPath: method removes KVO. Note that removeObserver needs to be called before the observer disappears, otherwise it will Crash.

For example, we define a YZPerson class that inherits from NSObject and has the name and age properties in it

@interface YZPerson : NSObject
@property (nonatomic ,assign) int age;
@property (nonatomic,strong) NSString  *name;
@end

Copy the code

And then in the ViewController, write the following code

- (void)viewDidLoad { [super viewDidLoad]; // Call the method [selfsetNameKVO];
}

-(void)setNameKVO{ self.person = [[YZPerson alloc] init]; / / registered observers NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; [self.person addObserver:selfforKeyPath:@"name" options:options context:@"1111"]; } // When the listener's property value changes, This calls - (void) observeforkeypath :(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { NSLog(@"Listen to %@ change %@ - %@ - %@", object, keyPath, change, context);
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
  self.person.name = @"ccc"; } -(void)dealloc {// Remove listener [self.person removeObserver:selfforKeyPath:@"name"];
}

Copy the code

The result after execution is

KVOdemo[11482:141804] listens for <YZPerson: 0x6000004e8400> name property value changed - {kind = 1; new = ccc; old ="<null>"; } - 1111-1111Copy the code

Pay attention to the point

Note that we have removed the listening from the code above, and if we remove it again, it will crash

For example,

- (void)viewDidLoad { [super viewDidLoad]; // Call the method [selfsetNameKVO];
}
-(void)setNameKVO{ self.person = [[YZPerson alloc] init]; / / registered observers NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; [self.person addObserver:selfforKeyPath:@"name" options:options context:@"1111"]; Person removeObserver:selfforKeyPath:@"name"]; // Remove [person removeObserver:self againforKeyPath:@"name"];

}
Copy the code

Removing multiple times will cause an error

KVOdemo[9261:2171323] *** Terminating app due to uncaught exception 'NSRangeException', 
reason: 'Cannot remove an observer 
      
        for the key path "name" from 
       
         because it is not registered as an observer.'
       
      
Copy the code

If you forget to remove it, you may receive a carsh the next time you receive a change to the property

So, we want to make sure that add and remove come in pairs

Throw doubt

Join us with two more YZPerson objects and just listen on one of them


- (void)viewDidLoad {
    [super viewDidLoad];
    [self setNameKVO];
}

-(void)setNameKVO{ self.person = [[YZPerson alloc] init]; self.person2 = [[YZPerson alloc] init]; / / registered observers NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; [self.person addObserver:selfforKeyPath:@"name" options:options context:@"1111"];
  

}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person.name = @"ccc";
    self.person2.name = @"ddd"; } // When the listener's property value changes, This calls - (void) observeforkeypath :(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { NSLog(@"Listen to %@ change %@ - %@ - %@", object, keyPath, change, context); } - (void)dealloc {// Remove listener [self.person removeObserver:selfforKeyPath:@"name"];
}
Copy the code

When you click on the screen, print the following

Listen to <YZPerson: 0x600001AFa740 > change the name property value - {kind = 1; new = ccc; old ="<null>"; } - 1111.Copy the code

But as we know,

 self.person.name = @"ccc";
 self.person2.name = @"ddd";
Copy the code

The above two lines of code call setName

@implementation YZPerson
- (void)setName:(NSString *)name{
    _name = name;
}
Copy the code

In other words, both objects call the setName method, which, according to iOS mechanisms, should be based on the ISA pointer to YZPerson, to find the method in the class object. Self.person. name can listen on self.person2.name can’t listen on self.person2.name

Nature analysis

The isa Pointers for person and person2 are different, resulting in different methods being executed.

Interrupt point and print both isa

(lldb) po self.person->isa
NSKVONotifying_YZPerson

(lldb) po self.person2->isa
YZPerson

Copy the code

Isa pointer is different, since ISA pointer is different. Does that mean they have different class objects? The answer is yes. Since oc is looking for class objects according to ISA, the next step is validation

validation

Validates class objects

Import the Runtime and print the classes for both


  NSLog(@"Person before adding KVO listener - %@ %@", object_getClass(self.person), object_getClass(self.person2)); / / registered observers NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; [self.person addObserver:selfforKeyPath:@"name" options:options context:@"1111"];
    
    NSLog(@"Person added KVO listener - %@ %@",
          object_getClass(self.person),
          object_getClass(self.person2));


Copy the code

The printed result is

KVOdemo[13302:171740] person before adding KVO listening - YZPerson YZPerson KVOdemo[13302:171740] person after adding KVO listening - NSKVONotifying_YZPerson YZPersonCopy the code

So, after adding KVO listener, it is true that the self.person class object is NSKVONotifying_YZPerson and the self.person2 class object is still YZPerson

Note: The real class cannot be retrieved using [self.person class]

For example, after adding the KVO listener, we get the class object this way


 NSLog(@"Person added KVO listener - %@ %@",
              [self.person class],
              [self.person2 class]);

Copy the code

So the printed result is zero

KVOdemo[17839:239214] person After adding KVO listening - YZPerson YZPersonCopy the code

This is because Apple generated the middle class NSKVONotifying_YZPerson for us, but he didn’t want us to know that it existed, so he overwrote the class method of NSKVONotifying_YZPerson, so we got inaccurate results.

Verify the method IMP

SetName: = setName: = setName: = setName: = setName


  NSLog(@"Person before adding KVO listener - %p %p",
          [self.person methodForSelector:@selector(setName:)],
          [self.person2 methodForSelector:@selector(setName:)]); / / registered observers NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; [self.person addObserver:selfforKeyPath:@"name" options:options context:@"1111"];
    
    NSLog(@"Person added KVO listener - %p %p",
          [self.person methodForSelector:@selector(setName:)],
          [self.person2 methodForSelector:@selector(setName:)]);

Copy the code

The results for

KVOdemo[13655:177448] person Before KVO listening is added -0x10CCFA630 0x10CCFA630 KVOdemo[13655:177448] person after KVO listening is added -0x10D056d1A 0x10ccfa630Copy the code

Self. person’s setName address has changed since the listener was added. Continue viewing through the LLDB

KVOdemo[13655:177448] person Before KVO listening is added -0x10CCFA630 0x10CCFA630 KVOdemo[13655:177448] person after KVO listening is added -0x10D056d1A 0x10ccfa630 (lldb) p (IMP)0x10ccfa630 (IMP)$0 = 0x000000010ccfa630 (KVOdemo`-[YZPerson setName:] at YZPerson.m:12)
(lldb) p (IMP)0x10d056d1a
(IMP) The $1 = 0x000000010d056d1a (Foundation`_NSSetObjectValueAndNotify)

Copy the code

Method, add the KVO after listening, elegantly-named setName: point to the _NSSetObjectValueAndNotify in the Foundation framework

Metaclass object validation

Since the class object is not the same after adding the KVO listener, what about the metaclass object? The following validation

/ / registered observers NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; [self.person addObserver:selfforKeyPath:@"name" options:options context:@"1111"];
    
    
NSLog(@"Class object - %@ %@",
          object_getClass(self.person),  // self.person.isa
          object_getClass(self.person2)); // self.person2.isa
          
 NSLog(@"Metaclass object - %@ %@", object_getClass(object_getClass(self.person)), // self.person.isa.isa object_getClass(object_getClass(self.person2)));  // self.person2.isa.isaCopy the code

The results for

KVOdemo[13655:177448] class object - NSKVONotifying_YZPerson YZPerson KVOdemo[13655:177448] meta-object - NSKVONotifying_YZPerson YZPersonCopy the code

The metaclass object becomes NSKVONotifying_YZPerson

Internal call flow

What is the flow of the internal call once the KVO listener is set up? Let’s add the following code to Person


#import "YZPerson.h"

@implementation YZPerson
- (void)setName:(NSString *)name{
     _name = name;   
}

- (void)willChangeValueForKey:(NSString *)key
{
    [super willChangeValueForKey:key];
    
    NSLog(@"willChangeValueForKey");
}

- (void)didChangeValueForKey:(NSString *)key
{
    NSLog(@"didChangeValueForKey - begin");
    
    [super didChangeValueForKey:key];
    
    NSLog(@"didChangeValueForKey - end");
}

Copy the code

After clicking on the screen, print as follows

KVOdemo[17486:233248] willChangeValueForKey KVOdemo[17486:233248] didChangeValueForKey - begin KVOdemo[17486:233248] <YZPerson: 0x600000889CA0 > <YZPerson: 0x600000889CA0 > < name = {kind = 1; new = ccc; old ="<null>";
} - 1111
KVOdemo[17486:233248] didChangeValueForKey - end

Copy the code

That is, call [super didChangeValueForKey:key]; Listen for changes to the listening object, and then process the listening logic

Snooping on the NSKVONotifying_YZPerson method

- (void)printMethodNamesOfClass:(Class)cls { unsigned int count; Method *methodList = class_copyMethodList(CLS, &count); // Store method name NSMutableString *methodNames = [NSMutableString String]; // Iterate over all the methodsfor(int i = 0; i < count; Method Method = methodList[I]; NSString *methodName = NSStringFromSelector(method_getName(method)); [methodNames appendString:methodName]; [methodNames appendString:@","]; } // Free (methodList); // Prints the method name NSLog(@"% @ % @", cls, methodNames);
}

-(void)setNameKVO{ self.person = [[YZPerson alloc] init]; / / registered observers NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; [self.person addObserver:selfforKeyPath:@"name" options:options context:@"1111"];
    NSLog(@"After adding KVO listener to person - self. Person class is: %@,object_getClass(self.person));
    [self printMethodNamesOfClass:object_getClass(self.person)];
}

Copy the code

The result of the above code execution is

KVOdemo[19286:259546] person add KVO listener - self. Person class NSKVONotifying_YZPerson  KVOdemo[19286:259546] NSKVONotifying_YZPersonsetName:, class, dealloc, _isKVOA,
Copy the code

To verify this, the system overrides the setName, class, dealloc of the new subclass NSKVONotifying_YZPerson and adds the _isKVOA method

Call KVO manually

The KVO listener (willChangeValueForKey and didChangeValueForKey) can only trigger a listener if the listener property changes. Just call these two methods manually yourself. eg:

-(void)setNameKVO{ self.person = [[YZPerson alloc] init]; / / registered observers NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; [self.person addObserver:selfforKeyPath:@"name" options:options context:@"1111"];
    NSLog(@"After adding KVO listener to person - self. Person class is: %@,object_getClass(self.person));
    [self printMethodNamesOfClass:object_getClass(self.person)];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    //    self.person.name = @"ccc"; // Manually call KVO [self.person willChangeValueForKey:@"name"];
    
    [self.person didChangeValueForKey:@"name"]; } // When the listener's property value changes, This calls - (void) observeforkeypath :(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { NSLog(@"Listen to %@ change %@ - %@ - %@", object, keyPath, change, context); } - (void)dealloc {// Remove listener [self.person removeObserver:selfforKeyPath:@"name"];
}

Copy the code

Each time you click on the screen, print the following

Listen to <YZPerson: 0x600003e5B020 > change the name property value - {kind = 1; new ="<null>";
    old = "<null>"; } - 1111.Copy the code

You can see that even though new and old are both null, the value of name hasn’t changed, but because we called it manually,

 [self.person willChangeValueForKey:@"name"];
    
 [self.person didChangeValueForKey:@"name"];
Copy the code

So it triggers KVO

Expand further

How does iOS implement KVO for an object? (What is the nature of KVO?)

  • Use the RuntimeAPI to dynamically generate a subclass and have instance object’s ISA point to the new subclass. When modifying an instance object’s properties, It calls Foundation’s _NSSetXXXValueAndNotify function willChangeValueForKey, the original setter for the parent class, didChangeValueForKey, Internal will trigger the listener (Oberser) surveillance methods (observeValueForKeyPath: ofObject: change: context:)

How do I trigger KVO manually?

  • Manually call willChangeValueForKey and didChangeValueForKey:

Does modifying a member variable directly trigger KVO?

  • KVO will not trigger

Because, KVO is triggered because, when I do set, I call willChangeValueForKey didChangeValueForKey but modifying a member variable directly doesn’t call set

Eg: If we set the name member variable to the following form, the set and GET methods will not be generated automatically

@interface YZPerson : NSObject
{
    @public
    NSString *_name;
    
}
@property (nonatomic ,assign) int age;

@end
Copy the code

In the listener controller, modify the member variables directly as follows


-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    
    self.person->_name = @"abc";

}

Copy the code

This will not trigger KVO, if we want it to trigger KVO, we call it manually, as follows

@implementation ViewController

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    
    [self.person willChangeValueForKey:@"name"];
    self.person->_name = @"abc";
	 [self.person didChangeValueForKey:@"name"];
}

@end
Copy the code

This will trigger KVO.

Does modifying properties via KVC trigger KVO?

  • Will trigger the KVO
  • Break down the KVC thing

How is KVC different from KVO?

KVC(key-value Coding), an informal Protocol, uses strings (keys) to access an object instance variable. Instead of calling Setter, Getter, and other explicit access methods. KVO, or key-value Observing, provides a mechanism for receiving notifications when the properties of a specified object are modified, provided that setter methods are executed or KVC assignments are used.

What is the difference between KVO and Notification?

Notification takes one more step than KVO to send notifications. Both are one-to-many, but for direct interaction between objects, notification is much more obvious and notificationCenter is required as an intermediate interaction. And KVO as we introduced, set observer -> handle attribute changes, as for the middle notification loop, is much more secret, only leave a sentence “by the system notification”, specific can refer to the above implementation process analysis.

The advantage of Notification is that the monitoring is not limited to the change of attributes, but can also monitor a variety of state changes with a wide range of monitoring. For example, the use of system notifications such as keyboard, front and background is more flexible and convenient. (Refer to Section 5 of notification Mechanism)

What’s the difference between KVO and Delegate?

Like a delegate, both KVO and NSNotification serve to communicate between classes. But unlike a delegate, both send and receive notifications, and the system handles the rest, so no value is returned. A delegate needs objects to communicate with each other through variables (proxies); Delegate is usually one to one, and these two can be one to many.

Github address github

References for this article:

The Runtime source

Basic principles of iOS

KVO principle analysis and use of advanced

Facebook’s KVOController

IOS development — implementation principle and specific application of KVO

For more information, please pay attention to the personal public number