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
- Create
SXStudent
classes andSXTeacher
classes
//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
- Implementation
SXStudent
class.
// SXStudent.m
#import "SXStudent.h"
@implementation SXStudent
@end
- Implement
SXTeacher
the class, overrideinit
the method, and add listeners for the propertiesSXTeacher
.student1
Implementdemo
methods to change the values ofstudent1
and respectively .student2
age
// 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
mian
SXTeacher
Create an instance object within the function and calldemo
the 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;
}
- View the running results.
1.3 Research on implementation principles
1.3.1 Changes in student1
Why student1
can setter
the method trigger monitoring? student1
What exactly does the method of adding monitoring do?
- We set a breakpoint after adding the listener.
- Try using lldb debugging to view
student1
andstudent2
pointersisa
.
The pointer we found student1
was changed to the (NSKVONotifying_ is the prefix, the original class name is the suffix) class.isa
NSKVONotifying_SXStudent
1.3.2 NSKVONotifying_XXX类
- About
NSKVONotifying_XXX
classes
NSKVONotifying_XXX
A class isRuntime
a dynamically created class, and a new class is generated during the running of the program.NSKVONotifying_XXX
A class is a subclass of the original class.NSKVONotifying_XXX
Classes have their ownsetAge:
,class
,dealloc
,isKVOA
... methods.
Try to verify NSKVONotifying_XXX
the method and parent class of the class, we can use the following code to print NSKVONotifying_SXStudent
the class and SXStudent
the 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:
You can see that NSKVONotifying_SXStudent
the class has its own setAge:
, class
, dealloc
, _isKVOA
methods.
- 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 student1
pointer isa
is NSKVONotifying_SXStudent
, and it NSKVONotifying_SXStudent
also contains setAge:
methods, so student1
the setAge:
method should be the method NSKVONotifying_SXStudent
in the class setAge:
.
We try to use the following code to print the addresses of the methods student1
before 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:
The method we found student1
actually setAge:
calls a function Foundation
of 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_XXX
The method insetage:
actually callsFundation
the C language function in the framework_NSsetXXXValueAndNotify
._NSsetXXXValueAndNotify
The internal operation is equivalent to callingwillChangeValueForKey
the method to be changed first, then calling the originalsetage
method to assign values to the member variables, and finally callingdidChangeValueForKey
the changed method.didChangeValueForKey
The listener's listening method will be called, and finally it will come to the listener'sobserveValueForKeyPath
method.
Foundation
Different methods will be called in the framework based on the type of the attribute. NSInteger
For example, the properties of the type we defined before age
, then we see the functions Foundation
called in the framework _NSsetLongLongValueAndNotify
. Then we age
change the attribute type to double
print again.
We found that the called function changed to _NSSetDoubleValueAndNotify
, then this shows that Foundation
there 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 SXStudent
of the class to verify the above statement.willChangeValueForKey
didChangeValueForKey
#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:
It can be known:
_NSSetXXXValueAndNotify
callwillChangeValueForKey:
;_NSSetXXXValueAndNotify
callsetter
implementation;_NSSetXXXValueAndNotify
calldidChangeValueForKey:
;didChangeValueForKey
observe
The r method will be called internallyobserveValueForKeyPath: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.
SetValue:ForKey:
andValueForKey:
methods
- (void)demoSetValueForKeyAndValueForKey {
[self.student1 setValue:@20 forKey:@"age"];
NSLog(@"点语法:%ld", self.student1.age);
NSNumber *value = [self.student1 valueForKey:@"age"];
NSLog(@"KVC:%@", value);
}
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);
}
- Call the above two functions and run.
2.2.4 The difference between KeyPath and Key:
keyPath
It is equivalent to looking for attributes according to the path, layer by layer.key
It 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
- 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
- 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 student1
the member variables in. Look who has been assigned a value.
You can see _age
that it is assigned first. We comment out the member variables SXStudent
in _age
to 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 key
does not correspond to the attribute when getting or assigning a value, rewrite - (void)setValue:(id)value forUndefinedKey:(NSString *)key
the 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 Runtime
generates a subclass of the class object of this instance object. The specific format is _NSKVONotifying_XXX
, and isa
the 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:set
set
Foundation
NSSetXXXValueAndNotify
_NSSetXXXValueAndNotify
- call
willChangeValueForKey:
method; - Call the method of the parent class
set
and reassign the value; - call
didChangeValueForKey:
method; didChangeValueForKey:
Internally the listener will be triggeredobserveValueForKeyPath: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:
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.