KVO
Article directory
KVO concept
KVO is a development mode. Its full name is Key-Value Observing (observer mode). It is a development mechanism provided under the Apple Fundation framework. Using KVO, you can easily observe a certain attribute of a specified object. When the attribute When a change occurs, notify the developer of the corresponding content between the old value and the new value of the property
Steps to use KVO
Register KVO monitoring
Register KVO through [addObserver:forKeyPath:options:context:]
the method, so that you can receive the change event of the keyPath attribute;
observer
: Observer, an object that monitors property changes. The object must implementobserveValueForKeyPath:ofObject:change:context:
the method.keyPath
: The property name to observe. Be consistent with the name of the attribute declaration.options
: In the callback method, the old value or new value of the attribute of the observed object is received, and the KVO mechanism is configured to modify the timing and content of the KVO notificationcontext
: Pass in any type of object, which can be received in the code of "receive message callback", which is a value-passing method in KVO.
KVO monitoring implementation
[observeValueForKeyPath:ofObject:change:context:]
Realize KVO monitoring through methods ;
keyPath
: property of the observed objectobject
: the object being observedchange
: Dictionary, store related values, return new value and old value according to the enumeration passed in by optionscontext
: When registering the observer, the value passed by the context
Remove KVO monitoring
When there is no need to monitor, use the method [removeObserver:forKeyPath:]
to remove the monitor;
Basic usage of KVO
We monitor the background color of the Button, click the button to change the background color of the button, and when the background color changes, print the background color before and after the change.
//KVO最基本的使用
self.view.backgroundColor = [UIColor whiteColor];
self.kvoButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
self.kvoButton.frame = CGRectMake(100, 100, 100, 100);
self.kvoButton.backgroundColor = [UIColor yellowColor];
[self.view addSubview:self.kvoButton];
[self.kvoButton addTarget:self action:@selector(press) forControlEvents:UIControlEventTouchUpInside];
//给所要监听的对象注册监听
[self.kvoButton addObserver:self forKeyPath:@"backgroundColor" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
- (void)press {
//改变被监听对象的值
[self.kvoButton setValue:[UIColor colorWithRed:arc4random() % 255 / 255.0 green:arc4random() % 255 / 255.0 blue:arc4random() % 250 / 250.0 alpha:1] forKey:@"backgroundColor"];
}
//当属性变化时会激发该监听方法
- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
//打印监听结果
if ([keyPath isEqual:@"backgroundColor"]) {
NSLog(@"old value is: %@", [change objectForKey:@"old"]);
NSLog(@"new value is: %@", [change objectForKey:@"new"]);
}
}
We click the button once:
KVO pass value
KVO value transfer is also very simple. It can be understood that we monitor a certain property of the second viewController. When we jump to the first viewController, we can monitor the value change.
//FirstViewController
- (void)pressChuanZhi {
SecondViewController *secondViewController = [[SecondViewController alloc] init];
secondViewController.modalPresentationStyle = UIModalPresentationFullScreen;
//为试图中的属性注册一个监听事件
[secondViewController addObserver:self forKeyPath:@"content" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
[self presentViewController:secondViewController animated:YES completion:nil];
}
//当属性变化时会激发该监听方法
- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqual:@"content"]) {
id value = [change objectForKey:@"new"];
self.chuanzhiLabel.text = value;
}
}
//SecondViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor orangeColor];
self.backButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
self.backButton.frame = CGRectMake(100, 100, 100, 100);
self.backButton.backgroundColor = [UIColor blueColor];
[self.backButton addTarget:self action:@selector(pressBack) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:self.backButton];
self.textField = [[UITextField alloc] initWithFrame:CGRectMake(100, 250, 200, 50)];
self.textField.keyboardType = UIKeyboardTypeDefault;
self.textField.borderStyle = UITextBorderStyleRoundedRect;
[self.view addSubview:self.textField];
}
- (void)pressBack {
self.content = self.textField.text;
[self dismissViewControllerAnimated:YES completion:nil];
}
Ways to ban KVO
//返回NO禁止KVO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"content"]) {
return NO;
} else {
return [super automaticallyNotifiesObserversForKey:key];
}
}
Precautions for use
- The call
[removeObserver:forKeyPath:]
needs to be made before the observer disappears, otherwise it will resultCrash
. - After calling
addObserver
the method, KVO does not make a strong reference to the observer, so you need to pay attention to the life cycle of the observer, otherwise it will cause the observer to be releasedCrash
. - The observer needs to implement
observeValueForKeyPath:ofObject:change:context:
the method, which will be called when the KVO event arrives, and will result if it is not implementedCrash
. addObserver
The sum of KVOremoveObserver
is paired, if it is repeated,remove
it will causeNSRangeException
the typeCrash
, if it is forgotten,remove
it will receive the KVO callback again after the observer is releasedCrash
.- When calling KVO, one needs to be passed in
keyPath
. SincekeyPath
it is in the form of a string, it is easy to cause that the string does not change after the corresponding attribute changesCrash
. We can use the system's reflection mechanism tokeyPath
reflect it, so that the compiler can@selector()
perform legality checks in it.
Principle of KVO
Before analyzing the internal implementation of KVO, let’s analyze the storage structure of KVO, which mainly uses the following classes:
GSKVOInfo
GSKVOPathInfo
GSKVOObservation
GSKVOInfo
KVO is an informal protocol implemented based on the NSObject category, so all classes inherited from NSObject can use KVO, and all KVO information associated with the object can be obtained through the - (void*) observationInfo method, and the return value is GSKVOInfo. The source code is as follows
@interface GSKVOInfo : NSObject
{
NSObject *instance; // Not retained.
GSLazyRecursiveLock *iLock;
NSMapTable *paths;
}
- It saves an instance of an object. The key point is
Not retained
that is not heldweak
, so when it is released, the call will crash, and all observers need to be removed before the object is destroyed. paths
Used to hold the mappingkeyPath
fromGSKVOPathInfo
to :
GSKVOPathInfo
@interface GSKVOPathInfo : NSObject
{
@public
unsigned recursion;
unsigned allOptions;
NSMutableArray *observations;
NSMutableDictionary *change;
}
- It holds
keypath
all observers corresponding to a observations
holds all observers (GSKVOObservation
type)allOptions
Save the observer's options collectionchange
Save the content to be delivered when KVO triggers
GSKVOObservation
@interface GSKVOObservation : NSObject
{
@public
NSObject *observer; // Not retained (zeroing weak pointer)
void *context;
int options;
}
@end
It holds all the information for a single observation
observer
save watcher note here tooNot retained
context options
Both are parameters passed in when adding observers
KVO is implemented through isa-swizzling technology (this sentence is the focus of the entire KVO implementation). Create an intermediate class based on the original class at runtime, this intermediate class is a subclass of the original class, and dynamically modify the isa of the current object to point to the intermediate class. And rewrite the class method to return the Class of the original class. Therefore, Apple suggests that you should not rely on the isa pointer during development, but use the class instance method to obtain the object type.
Why rewrite the class method?
If there is no rewriting class
method, when the object calls class
the method, it will search for the method in its own method cache list, method list, parent class cache, and method list, because class
the method is NSObject
a method in the method, if it is not rewritten, it may eventually will return NSKVONotifying_Apple
, and the class will be exposed.
To implement the isa-swizzling technology, it is mainly implemented through the following classes:
GSKVOReplacement
GSKVOBase
GSKVOSetter
GSKVOReplacement
@interface GSKVOReplacement : NSObject
{
Class original; /* The original class */
Class replacement; /* The replacement class */
NSMutableSet *keys; /* The observed setter keys */
}
- (id) initWithClass: (Class)aClass;
- (void) overrideSetterFor: (NSString*)aKey;
- (Class) replacement;
@end
// 创建
- (id) initWithClass: (Class)aClass
{
NSValue *template;
NSString *superName;
NSString *name;
original = aClass;
/*
* Create subclass of the original, and override some methods
* with implementations from our abstract base class.
*/
superName = NSStringFromClass(original); // original == Temp
name = [@"GSKVO" stringByAppendingString: superName]; // name = GSKVOTemp
template = GSObjCMakeClass(name, superName, nil); // template = GSKVOTemp
GSObjCAddClasses([NSArray arrayWithObject: template]);
replacement = NSClassFromString(name);
GSObjCAddClassBehavior(replacement, baseClass);
/* Create the set of setter methods overridden.
*/
keys = [NSMutableSet new];
return self;
}
- This class saves the original class information of the observed object
original
- Create a subclass of the original class and name it
GSKVO<原类名>
, the iOS system uses the naming rules forNSKVONotifying_
the original class name - Copy
GSKVOBase
a method from a class to a new class - The follow-up will pass
object_setClass
, and the observed object willisa
point to this new class, that is,isa-swizzling
technology, and isa saves the information of the class, that is to say, the observed object becomes an instance of the new class, and this new class is used to implement KVO notification mechanism
GSKVOBase
This class provides several methods by default, all of which are rewrites of NSObject methods, and from the above, these methods must be copied to the newly created replacement class. That is, the observed object will have the implementation of these methods
- (void) dealloc
{
// Turn off KVO for self ... then call the real dealloc implementation.
[self setObservationInfo: nil];
object_setClass(self, [self class]);
[self dealloc];
GSNOSUPERDEALLOC;
}
After the object is released, remove the KVO data and point the object back to the original class
- (Class) class
{
return class_getSuperclass(object_getClass(self));
}
This method is used to hide the information of the replacement class, and the information obtained by the application layer is still the information of the original class. Therefore, Apple recommends that you should not rely on the isa pointer during development, but use the class instance method to obtain the object type.
- (Class) superclass
{
return class_getSuperclass(class_getSuperclass(object_getClass(self)));
}
This method is the same as the class method
- (void) setValue: (id)anObject forKey: (NSString*)aKey
{
Class c = [self class];
void (*imp)(id,SEL,id,id);
imp = (void (*)(id,SEL,id,id))[c instanceMethodForSelector: _cmd];
if ([[self class] automaticallyNotifiesObserversForKey: aKey])
{
[self willChangeValueForKey: aKey];
imp(self,_cmd,anObject,aKey);
[self didChangeValueForKey: aKey];
}
else
{
imp(self,_cmd,anObject,aKey);
}
}
This method belongs to KVC. Rewrite this method to add [self willChangeValueForKey: aKey]
and before and after the original class KVC call [self didChangeValueForKey: aKey]
, and these two methods are the key to triggering KVO notifications.
So KVO is based on KVC, and KVC is the entrance triggered by KVO.
GSKVOBase
@interface GSKVOSetter : NSObject
- (void) setter: (void*)val;
- (void) setterChar: (unsigned char)val;
- (void) setterDouble: (double)val;
- (void) setterFloat: (float)val;
- (void) setterInt: (unsigned int)val;
- (void) setterLong: (unsigned long)val;
#ifdef _C_LNG_LNG
- (void) setterLongLong: (unsigned long long)val;
#endif
- (void) setterShort: (unsigned short)val;
- (void) setterRange: (NSRange)val;
- (void) setterPoint: (NSPoint)val;
- (void) setterSize: (NSSize)val;
- (void) setterRect: (NSRect)rect;
@end
This class has the same principle as the rewriting KVC method above, and will replace the method implementation keypath
of the observer in the future setter
. will setter
append [self willChangeValueForKey: aKey]
and[self didChangeValueForKey: aKey]
summary
So here, I have a general understanding of the implementation of KVO. Through isa-swizzling
technology, replace the observed class information, and hook the observed method, add and keyPath setter
before and after the original method call , so as to achieve the function of monitoring attribute changes[self willChangeValueForKey: aKey]
[self didChangeValueForKey: aKey]
Source code implementation
Next, view all the processes of KVO from the source code
- (void) addObserver: (NSObject*)anObserver
forKeyPath: (NSString*)aPath
options: (NSKeyValueObservingOptions)options
context: (void*)aContext
{
GSKVOInfo *info;
GSKVOReplacement *r;
NSKeyValueObservationForwarder *forwarder;
NSRange dot;
setup();
[kvoLock lock];
// Use the original class
r = replacementForClass([self class]);
/*
* Get the existing observation information, creating it (and changing
* the receiver to start key-value-observing by switching its class)
* if necessary.
*/
info = (GSKVOInfo*)[self observationInfo];
if (info == nil)
{
info = [[GSKVOInfo alloc] initWithInstance: self];
[self setObservationInfo: info];
object_setClass(self, [r replacement]);
}
/*
* Now add the observer.
*/
dot = [aPath rangeOfString:@"."];
if (dot.location != NSNotFound)
{
forwarder = [[NSKeyValueObservationForwarder alloc]
initWithKeyPath: aPath
ofObject: self
withTarget: anObserver
context: aContext];
[info addObserver: anObserver
forKeyPath: aPath
options: options
context: forwarder];
}
else
{
[r overrideSetterFor: aPath];
[info addObserver: anObserver
forKeyPath: aPath
options: options
context: aContext];
}
[kvoLock unlock];
}
The entry of KVO, adding the observer method, mainly does the following things:
- 1.
replacementForClass
Create a replacement class and add it to the globalclassTable
for future use - 2. Get the listener data of the object
GSKVOInfo
, if not, create a new one - 3. Next, there are two cases
-
- If the dot syntax ( ) is used , sub-object monitoring
self.keyPath.keyPath
will be created using recursion, and the sub-object will forward the monitored changes to the upper layer for specific analysis laterNSKeyValueObservationForwarder
- If the dot syntax ( ) is used , sub-object monitoring
-
- By default (keyPath) directly listens to a property of the object,
overrideSetterFor
the method will be called, the setter method of the hook property, and the implementation of the setter method will be replaced by theGSKVOSetter
method implementation in the corresponding parameter type
- By default (keyPath) directly listens to a property of the object,
- 4. Then call
[info addObserver: anObserver forKeyPath: aPath options: options context: aContext]
; method to save the new listener.
Add method in GSKVOInfo
- (void) addObserver: (NSObject*)anObserver
forKeyPath: (NSString*)aPath
options: (NSKeyValueObservingOptions)options
context: (void*)aContext
{
GSKVOPathInfo *pathInfo;
GSKVOObservation *observation;
unsigned count;
if ([anObserver respondsToSelector:
@selector(observeValueForKeyPath:ofObject:change:context:)] == NO)
{
return;
}
[iLock lock];
pathInfo = (GSKVOPathInfo*)NSMapGet(paths, (void*)aPath);
if (pathInfo == nil)
{
pathInfo = [GSKVOPathInfo new];
// use immutable object for map key
aPath = [aPath copy];
NSMapInsert(paths, (void*)aPath, (void*)pathInfo);
[pathInfo release];
[aPath release];
}
observation = nil;
pathInfo->allOptions = 0;
count = [pathInfo->observations count];
while (count-- > 0)
{
GSKVOObservation *o;
o = [pathInfo->observations objectAtIndex: count];
if (o->observer == anObserver)
{
o->context = aContext;
o->options = options;
observation = o;
}
pathInfo->allOptions |= o->options;
}
if (observation == nil)
{
observation = [GSKVOObservation new];
GSAssignZeroingWeakPointer((void**)&observation->observer,
(void*)anObserver);
observation->context = aContext;
observation->options = options;
[pathInfo->observations addObject: observation];
[observation release];
pathInfo->allOptions |= options;
}
if (options & NSKeyValueObservingOptionInitial)
{
/* If the NSKeyValueObservingOptionInitial option is set,
* we must send an immediate notification containing the
* existing value in the NSKeyValueChangeNewKey
*/
[pathInfo->change setObject: [NSNumber numberWithInt: 1]
forKey: NSKeyValueChangeKindKey];
if (options & NSKeyValueObservingOptionNew)
{
id value;
value = [instance valueForKeyPath: aPath];
if (value == nil)
{
value = null;
}
[pathInfo->change setObject: value
forKey: NSKeyValueChangeNewKey];
}
[anObserver observeValueForKeyPath: aPath
ofObject: instance
change: pathInfo->change
context: aContext];
}
[iLock unlock];
}
This method is mainly to save observer information
1. Query the corresponding GSKVOPathInfo
– GSKVOObservation
if there is, update it, if not, create a new one and save it
2. If options
it is included NSKeyValueObservingOptionInitial
, call it immediately [anObserver observeValueForKeyPath: aPath ofObject: instance change: pathInfo->change context: aContext]
; send a message to the observer
2. Get the current value through KVC [instance valueForKeyPath: aPath]
; get
willChangeValueForKey
didChangeValueForKey
These two methods are added before setter
and KVC
after assignment to save changes in property values and send messages to observers
willChangeValueForKey :
The main record isoldValue
to savepathInfo->change
in , ifoptions
it is includedNSKeyValueObservingOptionPrior
, it will traverse all observers, and immediately send a message to the observer,
NSKeyValueObservingOptionPrior
indicating that they will receive notifications before and after the attribute value is modifieddidChangeValueForKey
Save the old and new values of attributes according to options, traverse all observers, and send messages
remove observer
/*
* removes the observer
*/
- (void) removeObserver: (NSObject*)anObserver forKeyPath: (NSString*)aPath
{
GSKVOPathInfo *pathInfo;
[iLock lock];
pathInfo = (GSKVOPathInfo*)NSMapGet(paths, (void*)aPath);
if (pathInfo != nil)
{
unsigned count = [pathInfo->observations count];
pathInfo->allOptions = 0;
while (count-- > 0)
{
GSKVOObservation *o;
o = [pathInfo->observations objectAtIndex: count];
if (o->observer == anObserver || o->observer == nil)
{
[pathInfo->observations removeObjectAtIndex: count];
if ([pathInfo->observations count] == 0)
{
NSMapRemove(paths, (void*)aPath);
}
}
else
{
pathInfo->allOptions |= o->options;
}
}
}
[iLock unlock];
}
This method is mainly used to remove the observer of the corresponding keyPath. The implementation of the method is very simple. It can be queried and removed according to the parameters passed in anObserver
and aPath
in the data structure introduced earlier.
Summarize
- It is mainly used
isa-swizzling
to modify the class information of the observer, and the hooksetter
methodsetter
sends a message to all observers when the method is called - It can be seen from the source code above that the references to the observer and the observed are not retain, so the observer must be removed before the object is released.
- The sending of the message is mainly triggered by
[self willChangeValueForKey: key]
,[self didChangeValueForKey: key]
and must appear in pairs.automaticallyNotifiesObserversForKey
The method is used to control whether to add the above two methods. The default return value is YES. If it returns NO, it will not be added automatically, that is to say, the call of the setter and KVC modifications will not trigger notifications + (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key
This method is used to set dependencies. Sometimes it is required that the value of a certain property changes with the change of other properties of the same object. By registering such a dependency in the class in advance, even if the attribute value changes indirectly, a notification message will be sent, and the internal implementation of all the key collections that are rewritten by the observer class to return and the key depends on is also relatively simple
. All dependencies are stored in the globaldependentKeyTable
, and then hook all dependentkey
methodssetter
, when called[self willChangeValueForKey: key]
,[self didChangeValueForKey: key]
all dependencies will be found, and then a message will be sent- KVC has been used many times inside KVO
-
- rewrite
setValue:forKey
- rewrite
-
- Using
valueForKey --- valueForKeyPath
get attribute value, especially when using dot syntax, can onlyvalueForKeyPath
get deep attribute value.
So KVO is implemented based on KVC.
- Using