KVO

The full name of KVO is key-value Observing, commonly known as “key-value monitoring”, which can be used to monitor the change of an object’s attribute Value

The use of the KVO

You can initiate a listener for the property through the addObserver: forKeyPath: method and then through the observeValueForKeyPath: ofObject: change: method, as shown in the sample code below

@interface Person: NSObject @property (assign, nonatomic) int age; @property (assign, nonatomic) int height; @end @implementation Person @end @interface ViewController () @property (strong, nonatomic) Person *person1; @property (strong, nonatomic) Person *person2; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.person1 = [[Person alloc] init]; self.person1.age = 1; self.person2 = [[Person alloc] init]; self.person2.age = 2; Object_getClass (self.person1), object_getClass(self.person1), object_getClass(self.person1), object_getClass(self.person1), object_getClass(self.person2)); // Print the result: NSLog(@"person1 before KVO listener -% p %p", [self.person1 methodForSelector:@selector(setAge:)], [self.person2 methodForSelector:@selector(setAge:)]); / / 0 x10b60c4b0 0 x10b60c4b0 / / to add KVO monitoring objects person1 NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"]; Object_getClass (self.person1), object_getClass(self.person1), object_getClass(self.person1), object_getClass(self.person2)); // Print the result: NSKVONotifying_Person Person // Prints whether the setAge methods for person1 and person2 have changed since the listener was added NSLog(@" before the KVO listener was added for person1 - %p %p", [self.person1 methodForSelector:@selector(setAge:)], [self.person2 methodForSelector:@selector(setAge:)]); // 0x7fff207b62b7 0x10b60c4b0 } - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { self.person1.age = 20; } - (void)dealloc { [self.person1 removeObserver:self forKeyPath:@"age"]; } // When the listener's property value changes, - (void) observeforkeypath :(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {NSLog(@" monitoring %@ - %@ - %@", object, keyPath, change, context); } @endCopy the code

Note: removeObserver: forKeyPath:

The implementation nature of KVO

AddObserver = addObserver (); After the forKeyPath: method, the ISA pointer to the person1 instance object points to a new type, NSKVONotifying_Person, while the ISA pointer to person2 without the listener points to Person

2. We found that the class object and metaclass object that print person1 via object_getClass are the newly derived NSKVONotifying_Person type

NSLog(@" class object - %@ %@", object_getClass(self.person1), object_getClass(self.person2)); // NSKVONotifying_Person Person NSLog(@" metaclassobject - %@ %@", object_getClass(object_getClass(self.person1)), object_getClass(object_getClass(self.person2))); // NSKVONotifying_Person PersonCopy the code

NSKVONotifying_Person is a subclass of Person

NSLog(@" superclass - %@ %@", object_getClass(self.person1). Superclass, object_getClass(self.person2).superclass); // Person NSObjectCopy the code

4. By printing, we find that the memory address of the setAge method called by person1 has changed. By printing the details of the address in LLDB, we find that the implementation of the setAge method is actually _NSSetIntValueAndNotify in Foundation framework

(lldb) p (IMP)0x7fff207b62b7
(IMP) $2 = 0x00007fff207b62b7 (Foundation`_NSSetIntValueAndNotify)
(lldb) p (IMP) 0x108801480
(IMP) $3 = 0x0000000108801480 (Interview01`-[Person setAge:] at Person.m:13)
Copy the code

5. We manually create the derived type NSKVONotifying_Person and override setAge:, willChangeValueForKey:, didChangeValueForKey: in Person, running the program and observing the calls

@interface NSKVONotifying_Person : Person

@end

@implementation NSKVONotifying_Person

@end


@interface Person : NSObject

@property (assign, nonatomic) int age;
@property (assign, nonatomic) int height;
@end

@implementation Person

- (void)setAge:(int)age
{
    _age = age;
    
    NSLog(@"setAge:");
}

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

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

@end
Copy the code

NSKVONotifying_Person calls willChangeValueForKey: setAge: didChangeValueForKey: willChangeValueForKey: setAge: didChangeValueForKey: And in didChangeValueForKey: call the observer’s observeValueForKeyPath: ofObject: change: to notify the value property of the change in value

// Print 2021-01-19 13:42:02.071987+0800 Interview01[37119:19609444] willChangeValueForKey 2021-01-19 13:42:02. 072192 + 0800 Interview01 [37119-19609444] setAge: 2021-01-19 13:42:02.072332+0800 Interview01[37119:19609444] didChangeValueForKey - begin 2021-01-19 13:42:02.072662+0800 Interview01[37119:19609444] listen on <Person: 0x6000036AC2C0 > age value changed - {kind = 1; new = 21; old = 1; } -123 2021-01-19 13:42:02.072817+0800 Interview01[37119:19609444] didChangeValueForKey - endCopy the code

NSKVONotifying_Person overrides the class method inside the derived class and returns the Person type. So the only way to get the real type is through object_getClass

NSLog(@"%@ %@",
          [self.person1 class], 
          [self.person2 class]); 
// Person Person

NSLog(@"%@ %@",
          object_getClass(self.person1), 
          object_getClass(self.person2)); 
// NSKVONotifying_Person Person
Copy the code

7. NSKVONotifying_Person also generates dealloc and _isKVOA dynamically by using class_copyMethodList

- (void)printMethodNamesOfClass:(Class)cls { unsigned int count; Method *methodList = class_copyMethodList(CLS, &count); // Store method name NSMutableString *methodNames = [NSMutableString String]; For (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); } [self printMethodNamesOfClass:object_getClass(self.person1)]; [self printMethodNamesOfClass:object_getClass(self.person2)]; 2021-01-19 15:38:13.552990+0800 Interview01[41940:19730538] NSKVONotifying_MJPerson setAge:, class, dealloc, _isKVOA, 2021-01-19 15:38:13.553166+0800 Interview01[41940:19730538] MJPerson setAge:, age,Copy the code

The above operations can be summarized as follows:

  • usingRuntimeAPIDynamically generate a subclass and letThe instance objectstheisaPoint to this brand new subclass
  • The new subclass will be overriddenclassThis function returns the parent type
  • When modifyingThe instance objectsProperty is calledFoundationthe_NSSetXXXValueAndNotify function
  • callwillChangeValueForKey:
  • Call the original parent classsetter
  • calldidChangeValueForKey:
  • The listener method (Oberser) is triggered internallyobserveValueForKeyPath:ofObject:change:context:

Application scenarios of KVO

1. Listen to the offset of ScrollView and change the background color of the navigation bar

2. Add placeHolder to TextView, and use KVO to monitor whether the text input corresponds to the hidden display placeHolder

KVC

KVC stands for key-value Coding, commonly known as “key-value Coding”, which allows access to an attribute through a Key

The use of KVC

You can assign values to properties by setValue: forKeyPath: and setValue: forKey:, and obtain values by valueForKeyPath: and valueForKey:.

SetValue: forKeyPath: Further attributes can be assigned based on keyPath. SetValue: forKey: can only find attributes of the current object, as shown in the example code below

@interface Cat: NSObject @property (assign, nonatomic) int weight; @end @interface Person : NSObject @property (assign, nonatomic) int age; @property (strong, nonatomic) Cat *cat; @end @implementation Cat @end @implementation Person @end Person *person = [[Person alloc] init]; [person setValue:@10 forKey:@"age"]; person.cat = [[Cat alloc] init]; [person setValue:@80 forKeyPath:@"cat.weight"]; // NSLog(@"%d, %d", person.age, person.cat.weight); NSLog(@"%@", [person valueForKey:@"age"]); NSLog(@"%@", [person valueForKeyPath:@"cat.weight"]); // output: 10,80Copy the code

Note:

  • ifperson.catNo object is created, thensetValue: forKeyPath:Can’t givecat.weightAttribute assignment
  • If you usesetValue: forKey:Method to assign a value to the cat.weight property, which throws an exception[<Person 0x100510ec0> setValue:forUndefinedKey:]

The implementation nature of KVC

The implementation nature of setValue: forKey

1. Add and comment setAge: and _setAge: to Person, and then run the program to see if each method exists in order

@interface Person : nsobject@end@implementation Person - (void)setAge:(int)age {// NSLog(@"setAge: - %d", age); //} - (void)_setAge:(int)age { NSLog(@"_setAge: - %d", age); } @endCopy the code

2. Comment out of the two methods above, rewrite accessInstanceVariablesDirectly method and corresponding returns YES and NO, run the program found that returns NO throws an exception, that won’t go to find whether there is a corresponding attributes.

AccessInstanceVariablesDirectly the default return value is YES

/ / the default return value is YES + (BOOL) accessInstanceVariablesDirectly {/ / return YES; return NO; }Copy the code

3. Finally, we add and comment the member variables _age, _isAge, age and isAge respectively to the Person object. When the program runs, it will find whether each member variable exists in order, and throw an exception if none is found

// Open and comment each of the following member variables @interface Person: NSObject {@public // int age; // int isAge; // int _isAge; int _age; } @endCopy the code

The above operations can be summarized as follows:

ValueForKey: implementation nature

1. Add and comment getAge, age, isAge, _age to Person, and then run the program to see if each method exists in order

@interface Person: nsobjjject @end@implementation MJPerson - (int)getAge {return 11; } //- (int)age //{ // return 12; //} //- (int)isAge //{ // return 13; //} //- (int)_age //{ // return 14; //} @endCopy the code

2. As with setValue: forKey: Part 2, throw an exception if the value returned is NO [ valueForUndefinedKey:]

libc++abi.dylib: terminating with uncaught exception of type NSException
*** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<Person 0x105820160> valueForUndefinedKey:]: this class is not key value coding-compliant for the key age.'
Copy the code

3. The same operation as setValue: forKey: the last step, except that the corresponding member variable can be directly evaluated if it cannot be found, the above exception will be thrown

The above series of operations can also be summarized as follows:

Application scenarios of KVC

1. YesKVCGets the private member variable and modifies the value of the private member variable

After iOS13, apple will not allow private members of the system API to be accessed through KVC

It is still possible to access private members of custom types through KVC

2. Dictionary mode

The interview questions

1. How do I manually trigger KVO?

Manually call willChangeValueForKey and didChangeValueForKey:

2. Does directly modifying a member variable trigger KVO

KVO will not trigger

3. Does modifying attributes via KVC trigger KVO?

Will trigger the KVO

As shown in the sample code, we add a member variable age and a read-only attribute weight to Person, and then assign each of them to KVC. KV0 listening is triggered. The willChangeValueForKey and didChangeValueForKey methods are called

// Person.h @interface Person : NSObject { @public int age; } @property (assign, nonatomic, readonly) int weight; @end // Person.m @implementation Person - (void)willChangeValueForKey:(NSString *)key { [super willChangeValueForKey:key]; NSLog(@"willChangeValueForKey"); } - (void)didChangeValueForKey:(NSString *)key { NSLog(@"didChangeValueForKey - begin"); [super didChangeValueForKey:key]; NSLog(@"didChangeValueForKey - end"); } @end // ViewController.m @interface ViewController () @property (strong, nonatomic) Person *person; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.person = [[Person alloc] init]; // Add KVO listener [self.person addObserver: self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL]; [self.person addObserver: self forKeyPath:@"weight" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL]; [self.person setValue:@10 forKey:@"age"]; [self.person setValue:@20 forKey:@"weight"]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{ NSLog(@"observeValueForKeyPath: %@",change); } - (void)dealloc { [self.person removeObserver:self forKeyPath:@"age"]; [self.person removeObserver:self forKeyPath:@"weight"]; } @ the end / / / / output results willChangeValueForKey / / didChangeValueForKey - begin / / observeValueForKeyPath: {/ / kind = 1; // new = 10; // old = 0; //} //didChangeValueForKey - end // // //willChangeValueForKey //didChangeValueForKey - begin //observeValueForKeyPath: { // kind = 1; // new = 20; // old = 0; //} //didChangeValueForKey - endCopy the code

4. How to use KVO to listen for array element changes?

We can add elements to an array using KVC, which calls a KVO trigger listener to listen for array elements

@interface ViewController () @property (nonatomic, strong) NSMutableArray *lines; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.lines = [NSMutableArray array]; [self addObserver: self forKeyPath:@"lines" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL]; [[self mutableArrayValueForKey:@"lines"] addObject:@"1"]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{ NSLog(@"observeValueForKeyPath: %@",change); } - (void)dealloc { [self removeObserver:self forKeyPath:@"lines"]; } @end // Print: observeForkeypath: {indexes = "<_NSCachedIndexSet: 0x6000030AFe60 > 1 (in 1 ranges), indexes: (0)]"; kind = 2; new = ( 1 ); }Copy the code