【iOS】KVO&KVC principle

1 KVO key value monitoring

1.1 Introduction to KVO

The full name of KVO is Key-Value Observing, commonly known as "key-value monitoring", which can be used to monitor changes in the properties of an object.

KVO is generally used through the following three steps:

// 1. 添加监听
[self.student1 addObserver:self forKeyPath:@"age" options:options context:nil];


// 2. 重写- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context方法

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    
    
    NSLog(@"%@的%@被改变:%@", object, keyPath, change);
}

// 3. 适当时机移除监听
[self.student1 removeObserver:self forKeyPath:@"age"];

1.2 KVO is easy to use

  1. Create SXStudentclasses and SXTeacherclasses
//SXStudent.h 

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface SXStudent : NSObject
@property (nonatomic, assign) NSInteger age;
@end

NS_ASSUME_NONNULL_END


// SXTeacher.h
#import <Foundation/Foundation.h>
#import "SXStudent.h"

NS_ASSUME_NONNULL_BEGIN

@interface SXTeacher : NSObject
@property (nonatomic, strong) SXStudent *student1;
@property (nonatomic, strong) SXStudent *student2;
- (void)demo;
@end

NS_ASSUME_NONNULL_END
  1. Implementation SXStudentclass.
// SXStudent.m

#import "SXStudent.h"
@implementation SXStudent
@end
  1. Implement SXTeacherthe class, override initthe method, and add listeners for the properties SXTeacher. student1Implement demomethods to change the values ​​of student1and respectively .student2age
// SXTeacher.m

#import "SXTeacher.h"
#import <objc/runtime.h>

@implementation SXTeacher

- (id)init {
    
    
    if (self = [super init]) {
    
    
        self.student1 = [[SXStudent alloc] init];
        self.student2 = [[SXStudent alloc] init];
        
        self.student1.age = 1;
        self.student2.age = 2;
        
        // 添加监听
        NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
        [self.student1 addObserver:self forKeyPath:@"age" options:options context:nil];
    }
    return self;
}

- (void)demo {
    
    
    self.student1.age = 20;
    self.student2.age = 30;
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    
    
    NSLog(@"%@的%@被改变:%@", object, keyPath, change);
}

- (void)dealloc {
    
    
    // 移除监听
    [self.student1 removeObserver:self forKeyPath:@"age"];
}

@end
  1. mianSXTeacherCreate an instance object within the function and call demothe method test.
#import <Foundation/Foundation.h>
#import "SXTeacher.h"

int main(int argc, const char * argv[]) {
    
    
    @autoreleasepool {
    
    
        SXTeacher *teacher = [[SXTeacher alloc] init];
        [teacher demo];
    }
    return 0;
}
  1. View the running results.
    Insert image description here

1.3 Research on implementation principles

1.3.1 Changes in student1

Why student1can setterthe method trigger monitoring? student1What exactly does the method of adding monitoring do?

  1. We set a breakpoint after adding the listener.
    Insert image description here
  2. Try using lldb debugging to view student1and student2pointers isa.
    Insert image description here

The pointer we found student1was changed to the (NSKVONotifying_ is the prefix, the original class name is the suffix) class.isaNSKVONotifying_SXStudent

1.3.2 NSKVONotifying_XXX类

  1. About NSKVONotifying_XXXclasses
  • NSKVONotifying_XXXA class is Runtimea dynamically created class, and a new class is generated during the running of the program.
  • NSKVONotifying_XXXA class is a subclass of the original class.
  • NSKVONotifying_XXXClasses have their own setAge:, class, dealloc, isKVOA... methods.

Try to verify NSKVONotifying_XXXthe method and parent class of the class, we can use the following code to print NSKVONotifying_SXStudentthe class and SXStudentthe method list of the class and the parent class type.

- (void)demo2 {
    
    
    [self printMethods:object_getClass(self.student1)];
    [self printMethods:object_getClass(self.student2)];
}

- (void) printMethods:(Class)cls {
    
    
    unsigned int count;
    Method *methods = class_copyMethodList(cls, &count);
    NSMutableString *methodNames = [NSMutableString string];
    [methodNames appendFormat:@"%@ - ", cls];
    NSLog(@"%@ superClass ----> %@", NSStringFromClass(cls), NSStringFromClass(class_getSuperclass(cls)));
    
    for (int i = 0; i < count; i++) {
    
    
        Method method = methods[i];
        NSString *methodName = NSStringFromSelector(method_getName(method));
        
        [methodNames appendFormat:@"%@ ", methodName];
    }
    
    NSLog(@"%@", methodNames);
    free(methods);
}

Print the result:
Insert image description here

You can see that NSKVONotifying_SXStudentthe class has its own setAge:, class, dealloc, _isKVOAmethods.

  • The class method is overridden to hide the existence of the NSKVONotifying_XXX class. The overridden class method returns the type of its parent class (original class), making users think that the class has not changed.
  • _isKVOA is used to identify whether the current class is a class object dynamically generated through runtime. If so, it returns YES, if not, it returns NO.
  • After the object is destroyed, dealloc does some finishing work.

1.3.3 Method call exploration

It can be analyzed from the above that the class object pointed to by our student1pointer isais NSKVONotifying_SXStudent, and it NSKVONotifying_SXStudentalso contains setAge: methods, so student1the setAge:method should be the method NSKVONotifying_SXStudentin the class setAge:.

We try to use the following code to print the addresses of the methods student1before and after being monitored setAge:, and use lldb to debug to find out.

- (id)init {
    
    
    if (self = [super init]) {
    
    
        self.student1 = [[SXStudent alloc] init];
        self.student2 = [[SXStudent alloc] init];
        
        self.student1.age = 1;
        self.student2.age = 2;
        
        NSLog(@"添加监听之前 - p1 = %p, p2 = %p", [self.student1 methodForSelector:@selector(setAge:)], [self.student2 methodForSelector:@selector(setAge:)]);
        
        // 添加监听
        NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
        [self.student1 addObserver:self forKeyPath:@"age" options:options context:nil];
        
        NSLog(@"添加监听之后 - p1 = %p, p2 = %p", [self.student1 methodForSelector:@selector(setAge:)], [self.student2 methodForSelector:@selector(setAge:)]);
    }
    return self;
}

Use lldb to print the method name corresponding to the method address at the break point:
Insert image description here

The method we found student1actually setAge:calls a function Foundationof the framework _NSSetLongLongValueAndNotify. What's going on? Let's first understand this function.

1.3.3 _NSSetXXXValueAndNotify

We can find out by checking the information.
NSKVONotifyin_XXXThe method in setage:actually calls Fundationthe C language function in the framework _NSsetXXXValueAndNotify. _NSsetXXXValueAndNotifyThe internal operation is equivalent to calling willChangeValueForKeythe method to be changed first, then calling the original setagemethod to assign values ​​to the member variables, and finally calling didChangeValueForKeythe changed method. didChangeValueForKeyThe listener's listening method will be called, and finally it will come to the listener's observeValueForKeyPathmethod.

FoundationDifferent methods will be called in the framework based on the type of the attribute. NSIntegerFor example, the properties of the type we defined before age, then we see the functions Foundationcalled in the framework _NSsetLongLongValueAndNotify. Then we agechange the attribute type to doubleprint again.

Insert image description here

We found that the called function changed to _NSSetDoubleValueAndNotify, then this shows that Foundationthere are many functions of this type in the framework, and different functions are called through different types of attributes.

We can override the methods and methods SXStudentof the class to verify the above statement.willChangeValueForKeydidChangeValueForKey

#import "SXStudent.h"

@implementation SXStudent
- (void)setAge:(NSInteger)age {
    
    
    NSLog(@"setAge");
    _age = age;
}
- (void)willChangeValueForKey:(NSString *)key {
    
    
    NSLog(@"willChangeValueForKey begin");
    [super willChangeValueForKey:key];
    NSLog(@"willChangeValueForKey end");
}

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

Print the result:
Insert image description here

It can be known:

  • _NSSetXXXValueAndNotifycall willChangeValueForKey:;
  • _NSSetXXXValueAndNotifycall setterimplementation;
  • _NSSetXXXValueAndNotifycall didChangeValueForKey:;
  • didChangeValueForKeyobserveThe r method will be called internally observeValueForKeyPath:ofObject:change:context:.

1.3.4 Pseudocode

According to the above, the pseudocode of the NSKVONotifying_SXStudent class can be written:

///> NSKVONotifying_SXStudent.m 文件

#import "NSKVONotifying_SXStudent.h"

@implementation NSKVONotifying_SXStudent

- (void)setAge:(int)age{
    
    
  _NSSetLongLongValueAndNotify();  ///> 文章末尾 知识点补充小结有此方法来源
}

void _NSSetLongLongValueAndNotify(){
    
    
  [self willChangeValueForKey:@"age"];
  [super setAge:age];
  [self didChangeValueForKey:@"age"];
}

- (void)didChangeValueForKey:(NSString *)key{
    
    
  ///> 通知监听器 key发生了改变
  [observe observeValueForKeyPath:key ofObject:self change:nil context:nil];
}

@end

2 KVC key-value coding

2.1 Introduction to KVC

The full name of KVC is key-value-coding, commonly known as "key-value coding". You can access an attribute through key.

Common APIs include:

- (void)setValue:(id)value forKey:(NSString *)key;
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;

- (id)valueForKey:(NSString *)key 
- (id)valueForKeyPath:(NSString *)keyPath;

2.2 KVC is easy to use

2.2.1 Customize the SXDog class, SXStudent class and SXTeacher class.

// SXDog.h
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface SXDog : NSObject
@property (nonatomic, assign) CGFloat weight;
@end

NS_ASSUME_NONNULL_END


// SXStudent.h
#import <Foundation/Foundation.h>
#import "SXDog.h"

NS_ASSUME_NONNULL_BEGIN

@interface SXStudent : NSObject
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, strong) SXDog *dog;
@end

NS_ASSUME_NONNULL_END


// SXTeacher.h
#import <Foundation/Foundation.h>
#import "SXStudent.h"

NS_ASSUME_NONNULL_BEGIN

@interface SXTeacher : NSObject
@property (nonatomic, strong) SXStudent *student1;
@end

NS_ASSUME_NONNULL_END

2.2.2 Implement these three classes. For ease of use, rewrite the initialization methods of SXStudent and SXTeacher, and initialize the properties in the initialization method.

SXDog.m

#import "SXDog.h"

@implementation SXDog
@end

SXStudent.m

#import "SXStudent.h"
#import <objc/runtime.h>

@implementation SXStudent
- (id)init {
    
    
    if (self = [super init]) {
    
    
        self.dog = [[SXDog alloc] init];
    }
    return self;
}
@end

SXTeacher.m

#import "SXTeacher.h"
#import <objc/runtime.h>

@implementation SXTeacher
- (id)init {
    
    
    if (self = [super init]) {
    
    
        self.student1 = [[SXStudent alloc] init];
    }
    return self;
}
@end

2.2.3 Add two methods to SXTeacher and try to call KVC in these two methods.

  1. SetValue:ForKey:and ValueForKey:methods
- (void)demoSetValueForKeyAndValueForKey {
    
    
    [self.student1 setValue:@20 forKey:@"age"];
    NSLog(@"点语法:%ld", self.student1.age);
    NSNumber *value = [self.student1 valueForKey:@"age"];
    NSLog(@"KVC:%@", value);
}
  1. SetValue:ForKeyPath:andValueForKeyPath:
- (void)demoSetValueForKeyPathAndValueForKeyPath {
    
    
    [self.student1 setValue:@16 forKeyPath:@"dog.weight"];
    NSLog(@"点语法:%lf", self.student1.dog.weight);
    NSNumber *value = [self.student1 valueForKeyPath:@"dog.weight"];
    NSLog(@"KVC:%@", value);
}
  1. Call the above two functions and run.
    Insert image description here

2.2.4 The difference between KeyPath and Key:

  • keyPathIt is equivalent to looking for attributes according to the path, layer by layer.
  • keyIt is the name of the attribute that is directly accessed. If you search by path, an error will be reported.

2.3 KVC process

2.3.1 setValue:forkey: assignment process

Insert image description here

  • First, these two methods are searched in the method list of the object in the order of setKey: and _setKey:. If the method is found, the parameters are passed and the method is called.
  • If the method is not found, the return value of the accessInstanceVariablesDirectly method is used to determine whether the member variable can be found. If accessInstanceVariablesDirectly returns YES, the corresponding member variable will be found in the member variable list in the following order:
    • _key
    • _isKey
    • key
    • isKey
  • If accessInstanceVariablesDirectly returns NO, an NSUnknownKeyException exception is thrown directly.
    If the corresponding attribute value is found in the member variable list, the value will be assigned directly. If it cannot be found, an NSUnknownKeyException will be thrown.

accessInstanceVariablesDirectly function

+ (BOOL)accessInstanceVariablesDirectly{
    
    
      return YES;   ///> 可以直接访问成员变量
  //    return NO;  ///>  不可以直接访问成员变量,  
  ///> 直接访问会报NSUnkonwKeyException错误  
}

2.3.2 valueForKey: value acquisition process

Insert image description here

  • A list of methods is first searched in the following order:
    • getKey
    • key
    • isKey
    • _key
  • If found, pass the parameters directly and call the method. If not found, check the return value of the accessInstanceVariablesDirectly method. If NO is returned, an NSUnknownKeyException exception is thrown directly.
  • If the accessInstanceVariablesDirectly method returns YES, the member variable list is looked up in the following order:
    • _key
    • _isKey
    • key
    • isKey
  • If the corresponding member variable can be found, the value of the member variable is obtained directly. If not found, an NSUnknownKeyException exception is thrown.

2.3.3 Trial verification setValue:forkey: assignment process

Small modification to the above example:
SXStudent.h

@interface SXStudent : NSObject {
    
    
    @public
    int _age;
    int _isAge;
    int age;
    int isAge;
}
@end

SXTeacher.m

- (id)init {
    
    
    if (self = [super init]) {
    
    
        self.student1 = [[SXStudent alloc] init];
        
        NSKeyValueObservingOptions option = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
        [self.student1 addObserver:self forKeyPath:@"age" options:option context:nil];
        
        [self.student1 setValue:@20 forKey:@"age"];
        
        NSLog(@"-----");
    }
    return self;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    
    
    NSLog(@"%@的%@被改变:%@", object, keyPath, change);
}

NSLog(@"-----");Set a breakpoint at , run, and view student1the member variables in. Look who has been assigned a value.
Insert image description here

You can see _agethat it is assigned first. We comment out the member variables SXStudentin _ageto see who is assigned next. Repeat this to get setValue:forkey:the assignment process. The result is correct as above, so I won't continue.

Through this example, we can also know that KVC can also trigger KVO monitoring.

2.4 Batch storage and retrieval of KVC values

@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *sex;
@property (nonatomic, strong) NSString *age;
@end
// KVC批量赋值
- (void)kvc_setKeys {
    
    
    NSDictionary *kvc_dict = @{
    
    @"name": @"clearlove", @"sex": @"male", @"pr_age": @"21"};
    Person *pr = [[Person alloc] init];
    [pr setValuesForKeysWithDictionary:kvc_dict];
    NSLog(@"%@", pr.age);
}

// KVC批量取值
- (void)kvc_getValues {
    
    
    Person *pr = [[Person alloc] init];
    [pr setValue:@"mekio" forKey:@"name"];
    [pr setValue:@"male" forKey:@"sex"];
    [pr setValue:@"120" forKey:@"pr_age"];
    NSDictionary *pr_dict = [pr dictionaryWithValuesForKeys:@[@"name", @"age", @"sex"]];
    NSLog(@"%@", pr_dict);
}

If there is a value that keydoes not correspond to the attribute when getting or assigning a value, rewrite - (void)setValue:(id)value forUndefinedKey:(NSString *)keythe method
(the above code is the part that has been rewritten)

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    
    
    if ([key isEqualToString:@"pr_age"]) {
    
    
        self.age = (NSString *)value;
    }
}

3 some questions

3.1 How does iOS implement KVO on an object? (What is the essence of KVO?)

To add KVO to an instance object, the system dynamically Runtimegenerates a subclass of the class object of this instance object. The specific format is _NSKVONotifying_XXX, and isathe pointer of the instance object points to the newly generated class. Methods
that override attributes . When the method is called, the framework's function is called and the following steps are performed:setsetFoundationNSSetXXXValueAndNotify
_NSSetXXXValueAndNotify

  • call willChangeValueForKey:method;
  • Call the method of the parent class setand reassign the value;
  • call didChangeValueForKey:method;
  • didChangeValueForKey:Internally the listener will be triggered observeValueForKeyPath:ofObject:change:context:.

3.2 How to trigger KVO manually?

Manually call willChangeValueForKey:and didChangeValueForKey:.

example:

- (id)init {
    
    
    if (self = [super init]) {
    
    
        self.student1 = [[SXStudent alloc] init];
        
        NSKeyValueObservingOptions option = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
        [self.student1 addObserver:self forKeyPath:@"age" options:option context:nil];
        [self.student1 willChangeValueForKey:@"age"];
        [self.student1 didChangeValueForKey:@"age"];
        
    }
    return self;
}

operation result:

Insert image description here

Although it is a method didChangeValueForKey:that triggers the listener internally observeValueForKeyPath:ofObject:change:context:, willChangeValueForKey:the listener cannot be triggered without calling the method. The two must be used together.

3.3 Will directly modifying the value of a member variable trigger KVO?

Directly modifying the value of a member variable will not trigger KVO because the setter method is not triggered.

Guess you like

Origin blog.csdn.net/m0_63852285/article/details/131920372