Important: In order to understand key-value observing, you must first understand key-value coding
To understand KVO, you must first understand KVC, because key-value observation is based on key-value coding. Audience gentlemen can refer to the author of the article iOS advanced road (13) KVC.
I. Definition of KVO
Key-value observing
is a mechanism that allows objects to be notified of changes to specified properties of other objects.
KVO (key-value observing) is an informal protocol that allows an object to be notified when a specified property of another object changes. IOS developers can use KVO to detect and respond quickly to changes in object properties, which can be a huge help in developing highly interactive, responsive applications and bidirectional binding of views and models.
KVO’s primary benefit is that you don’t have to implement your own scheme to send notifications every time a property 1. Its defined infrastructure has framework-level support that makes it easy to adopt — typically you do not have to add any code to your project. In addition, the infrastructure is already full-featured, which makes it easy to support multiple observers for a single property, as well as dependent values.
The main benefit of KVO is that you don’t have to implement your own scheme to send notifications every time a property changes. Much of this process is built in, automated, and transparent. This makes it easy to adopt, and often you don’t have to add any code to your project. In addition, KVO supports adding multiple observers and dependent values for a single attribute.
Registering KVO — Registering for key-value Observing
Registering as an Observer — Registering as an Observer
- (void)addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(void *)context;
Copy the code
observer
: Registers KVO notification objects. The observer must implementkey-value observing
Methods –observeValueForKeyPath:ofObject:change:context:
.keyPath
: of the properties of the observedkeypath
, the value cannot be nil relative to the receiver.options
Representative:NSKeyValueObservingOptions
Which specifies what is contained in the observation notificationcontext
In:observeValueForKeyPath:ofObject:change:context:
To pass toobserver
Context of a parameter
2.1.1 Better keyPath
Passing strings as keypath is worse than using attributes directly, because any typos or misspellers are not detected by the compiler and will not work. A clever solution is to use NSStringFromSelector and an @selector literal:
NSStringFromSelector(@selector(isFinished))
Copy the code
Because @Selector checks all available selectors in the target, this doesn’t prevent all errors, but it can be used to catch most changes.
2.1.2 Better Context
There’s a nice note in the Apple documentation about context.
The context pointer in the addObserver:forKeyPath:options:context: message contains arbitrary data that will be passed back to the observer in the corresponding change notifications. You may specify NULL and rely entirely on the key path string to determine the origin of a change notification, but this approach may cause problems for an object whose superclass is also observing the same key path for different reasons.
The context contains arbitrary data that will be passed back to the observer in the corresponding change notification. You can specify NULL and rely entirely on keyPath to determine the source of the change notification. But this approach can be problematic: especially when dealing with subclasses that inherit from the same parent class and have the same keypath.
A safer and more extensible approach is to use the context to ensure notifications you receive are destined for your observer and not a superclass.
A safer and more extensible approach is to use the context to ensure that received notifications are sent to the observer and not to the superclass. How to set up a good content? Apple’s official documentation also provides recommended methods.
// a Person instance registers itself as an observer forA static variable holds its own pointer. That means it has nothing of its own. static void *PersonAccountBalanceContext = &PersonAccountBalanceContext; static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext; - (void)registerAsObserverForAccount:(Account*)account { [account addObserver:selfforKeyPath:@"balance"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:PersonAccountBalanceContext];
[account addObserver:self
forKeyPath:@"interestRate"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:PersonAccountInterestRateContext];
}
Copy the code
Receiving Notification of a Change
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if(the context = = PersonAccountBalanceContext) {/ / Do something with the balance... }else if(the context = = PersonAccountInterestRateContext) {/ / Do something with the what... }else{ // Any unrecognized context must belong to super [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; }}Copy the code
2.3 Removing an Object as an Observer
Apple documentation recommends adding observers in init or viewDidLoad, and removing observers in Dealloc to make sure the Add and remove are paired.
- (void)unregisterAsObserverForAccount:(Account*)account {
[account removeObserver:self
forKeyPath:@"balance"
context:PersonAccountBalanceContext];
[account removeObserver:self
forKeyPath:@"interestRate"
context:PersonAccountInterestRateContext];
}
Copy the code
Better to remove
Call – removeObserver: forKeyPath: context: when, when the object has not been registered for the observer (because it has registered the solution or start without registration), an exception is thrown. Interestingly, there is no built-in way to check whether an object is registered. The Apple documentation recommends using @try / @catch to remove an observer.
- (void)dealloc {
@try {
// 3. unsubscribe
[_account removeObserver:self
forKeyPath:NSStringFromSelector(@selector(contentSize))
context:ContentSizeContext];
}
@catch (NSException *exception) {
}
}
Copy the code
3. Monitoring of collection attributes
- Collection attributes (NSArray and NSSet) trigger KVO only when assigned. Changing elements in collection attributes does not trigger KVO (such as adding, deleting, or modifying elements).
OC from shallow to deep series of KVC (2) : collection proxy object
- An ordered set
- There is no need to implement the following additional methods:
- Indirect operation: When the variable set object operation is carried out, the set object can be obtained through key or keyPath, and then the add or remove operation will trigger the KVO message notification. This approach is most commonly used in real-world development.
// key method - (NSMutableArray *)mutableArrayValueForKey:(NSString *)key; - (NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key; - (NSMutableSet *)mutableSetValueForKey:(NSString *)key; / / keyPath methods: - (NSMutableArray *) mutableArrayValueForKeyPath (keyPath nsstrings *); - (NSMutableOrderedSet *)mutableOrderedSetValueForKeyPath:(NSString *)keyPath; - (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath;Copy the code
Example:
KVO Compliance — KVO Compliance
To use KVO, first you must ensure that the observed object, the Account in this case, is KVO compliant. Typically, if your objects inherit from NSObject and you create properties in the usual way, your objects and their properties will automatically be KVO Compliant. It is also possible to implement compliance manually. KVO Compliance describes the difference between automatic and manual key-value observing, and how to implement both.
To use KVO, you must first ensure that the object being observed conforms to KVO. In general, if your object inherits from NSObject, and you create properties in the usual way, your object and its properties will automatically be KVO compatible. Of course, compliance can also be implemented manually. KVO Compliance talks about the difference between automatic and manual key-value observation, and how to implement both.
By facsimile automaticallyNotifiesObserversForKey: return values, select automatic exit KVO. At the same time, the class method can also achieve the purpose of controlling specific properties.
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"balance"]) {
automatic = NO;
} else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
Copy the code
4.1 Automatic KVO — Automatic Change Notification
// Call the accessor method.
[account setName:@"Savings"];
// Use setValue:forKey:.
[account setValue:@"Savings" forKey:@"name"];
// Use a key path, where 'account' is a kvc-compliant property of 'document'.
[document setValue:@"Savings" forKeyPath:@"account.name"];
// Use mutableArrayValueForKey: to retrieve a relationship proxy object.
Transaction *newTransaction = <#Create a new transaction for the account#>;
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];
Copy the code
4.2 Manual KVO — Manual Change Notification
Manual KVO helps us combine changes to multiple attribute values into one, so that there is one at a time for callbacks, and also minimizes notification for application-specific reasons.
- To implement manual observer notification, call before changing the value
willChangeValueForKey
, after changing the valuedidChangeValueForKey
- (void)setBalance:(double)theBalance {
[self willChangeValueForKey:@"balance"];
_balance = theBalance;
[self didChangeValueForKey:@"balance"];
}
Copy the code
- For best performance, minimize sending unnecessary notifications by checking to see if values have changed
- (void)setBalance:(double)theBalance {
if(theBalance ! = _balance) { [self willChangeValueForKey:@"balance"];
_balance = theBalance;
[self didChangeValueForKey:@"balance"]; }}Copy the code
- If an operation causes multiple keys to change, this should be the case
- (void)setBalance:(double)theBalance {
[self willChangeValueForKey:@"balance"];
[self willChangeValueForKey:@"itemChanged"];
_balance = theBalance;
_itemChanged = _itemChanged+1;
[self didChangeValueForKey:@"itemChanged"];
[self didChangeValueForKey:@"balance"];
}
Copy the code
- For ordered one-to-many relationship properties, you must specify not only the changed key, but also the type of change and the index of the object involved. The type of change is
NSKeyValueChange
(NSKeyValueChangeInsertion
.NSKeyValueChangeRemoval
或NSKeyValueChangeReplacement
) for the index of the affected objectNSIndexSet
object
- (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes {
[self willChange:NSKeyValueChangeRemoval
valuesAtIndexes:indexes forKey:@"transactions"];
// Remove the transaction objects at the specified indexes.
[self didChange:NSKeyValueChangeRemoval
valuesAtIndexes:indexes forKey:@"transactions"];
}
Copy the code
Registering Dependent Keys — business Dependent Keys
There are properties whose values depend on the values of one or more other objects, and dependent properties need to be notified when a dependent property value changes.
5.1 the To – One Relationships
To automatically trigger a to-one relationship, there are two ways:
- rewrite
keyPathsForValuesAffectingValueForKey:
methods - Define the name as
keyPathsForValuesAffecting<Key>
Methods.
For example, if a person’s fullName fullName is made up of firstName and lastName, an application observing fullName should also be notified when firstName or lastName changes.
- (NSString *)fullName
{
return [NSString stringWithFormat:@"% @ % @",firstName, lastName];
}
Copy the code
- Method one: Rewrite
keyPathsForValuesAffectingValueForKey:
Method to indicate that the fullname attribute depends on firstName and lastName:
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"fullName"]) {
NSArray *affectingKeys = @[@"lastName"The @"firstName"];
keyPaths = [keyPaths setByAddingObjectsFromArray: affectingKeys];
}
return keyPaths;
}
Copy the code
This is equivalent to adding two new keys to the keypath that affects the fullName value: lastName and firstName, which is easy to understand.
It is worth noting: need keyPathsForValuesAffectingValueForKey messages sent to the parent class, so as not to interfere with this method in the parent class to rewrite
- Method 2: implement a follow the naming convention for keyPathsForValuesAffecting class methods, is dependent on other value of the attribute name (capitalize the first letter)
+ (NSSet *)keyPathsForValuesAffectingFullName
{
return [NSSet setWithObjects:@"lastName"The @"firstName", nil];
}
Copy the code
If keyPathsForValuesAffectingFullName more reasonable, in the classification, used for classification is not allowed in the overloaded methods, so keyPathsForValuesAffectingValueForKey method certainly can’t be used in the classification.
5.2 the To – many Relationships
KeyPathsForValuesAffectingValueForKey: method does not support includes keypath to – many relationships.
For example, there is a Department class that has a to-many relationship to the Employee class, which has the salary attribute. You want the Department class to have a totalSalary attribute to calculate the salaries of all employees, so the Department’s totalSalary depends on the salary attribute of all employees. You can’t achieve keyPathsForValuesAffectingTotalSalary method and return employees. Salary.
You can use KVO to use parent (such as Department) as an observer of all children (such as Employee) related attributes. You must also add or remove the parent as an observer of the child when you add or remove a child to or from the parent. In observeValueForKeyPath: ofObject: change: context: method we can change to be a dependency to update the dependencies of value:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context == totalSalaryContext) {
[self updateTotalSalary];
}
else
// deal with other observations and/or invoke super...
}
- (void)updateTotalSalary {
[self setTotalSalary:[self valueForKeyPath:@"[email protected]"]];
}
- (void)setTotalSalary:(NSNumber *)newTotalSalary {
if(totalSalary ! = newTotalSalary) { [self willChangeValueForKey:@"totalSalary"];
_totalSalary = newTotalSalary;
[self didChangeValueForKey:@"totalSalary"];
}
}
- (NSNumber *)totalSalary {
return _totalSalary;
}
Copy the code
Register the Department instance object as an observer, and then observe the object as the totalSalary property, but the setter method for the totalSalary property is manually called in the notification callback, And the value passed in is the sum of all the sum values in the set of employees by means of KVC’s set operator. Then in setter methods for the totalSalary property, the willChangeValueForKey: and didChangeValueForKey: methods are called accordingly.
The principle of KVO
Take a look at the official documentation first
Automatic key-value observing is implemented using a technique called isa-swizzling.
- The implementation of automatic key observation is based on
isa-swizzling
.
The isa pointer, as the name suggests, points to the object’s class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.
- As the name implies, the ISA pointer points to the class that maintains the object of the allocation table, which essentially contains Pointers to the methods that the class implements, as well as other data
When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.
- When an observer registers an observation of an object’s property key value, the value of the object being observed
isa
What the pointer points to has changed, pointing to an intermediate class instead of the real class. This has also led toisa
Pointers do not necessarily point to the real class to which the instance belongs.
You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.
- You should never rely on it
isa
Pointer to determine class membership. Instead, you should useclass
Method to determine the class to which an object instance belongs.
6.1 Dynamically Generating an Intermediate subclass: NSKVONotifying_XXX
According to the official website document, we preliminarilyjudge that there will be a middle class generated in KVO, which will change the isa pointer of the object. Test it out:
- Before adding the observer: Class object is AKPerson, instance object
self.person
Isa pointingAKPerson class
- After adding the observer: class object is AKPerson, instance object
self.person
Isa pointingNSKVONotifying_AKPerson class
Conclusion 1: After adding observers, the system dynamically generates intermediate classes
NSKVONotifying_AKPerson
, instance objectself.person
Isa by pointingThe original AKPerson class
, changed to point to an intermediate classNSKVONotifying_AKPerson
.
So how does this middle class relate to the original class? Test them by going through their classes and subclasses
#pragma mark - Walks through classes and subclasses
- (void)printClasses:(Class) CLS {// register the total number of Classes int count = objc_getClassList(NULL, 0); // Create an array containing the given object NSMutableArray *mArray = [NSMutableArray arrayWithObject: CLS]; Class* classes = (Class*)malloc(sizeof(Class)*count); objc_getClassList(classes, count);for (int i = 0; i<count; i++) {
if (cls == class_getSuperclass(classes[i])) {
[mArray addObject:classes[i]];
}
}
free(classes);
NSLog(@"classes = %@", mArray);
}
Copy the code
Conclusion 2: After the observer is added, the intermediate class NSKVONotifying_AKPerson is the original class
AKPerson
The subclass.
6.2 Dynamic subclasses observe setter methods
Property = member variable + getter + setter. Obviously, there are only two ways to change the value of a property: setter and member variable assignment.
AKPerson = ‘name’ and ‘nickName’; AKPerson = ‘nickName’;
@interface AKPerson : NSObject{
@public
NSString *name;
}
@property (nonatomic, copy) NSString *nickName;
@end
Copy the code
- Method setter for the nickName property, which triggers the KVO callback function
- The assignment of the member variable name does not trigger the KVO callback
Conclusion 3: Dynamic subclasses observe setter methods
6.3 Dynamic Internal Policies of Subclasses
6.3.1 What methods do dynamic subclasses override?
So we can go through the list of methods of the original class and the intermediate class and say, what methods did the intermediate class overwrite?
Pragma mark - pragma mark -ivar-property
- (void)printClassAllMethod:(Class)cls{
NSLog(@"* * * * * % @ * * * * *", cls);
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i<count; i++) {
Method method = methodList[i];
SEL sel = method_getName(method);
IMP imp = class_getMethodImplementation(cls, sel);
NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
}
free(methodList);
}
Copy the code
AKPerson class
The imp implementation address has not changed.NSKVONotifying_AKPerson middle class
Rewrite theThe parent class AKPerson
的Dealloc method
NSKVONotifying_AKPerson middle class
Rewrite theThe base class NSObject
的A class method
和_isKVOA method
_isKVOA
Method: Make a sign- According to the above speculation, the intermediate class is overwritten
A class method
The original class is still returned. The purpose is to hide the existence of the intermediate class and let the caller call itA class method
The results are consistent
6.3.2 Where do dynamic subclasses go after observation is removed?
- After removing the observer, the isa of the original class is removed by
NSKVONotifying_AKPerson middle class
Which canAKPerson class
- After removing the observer, the dynamic subclass remains.
6.4 KVO debugging
With the observationInfo command, you can view all observation information about an observed object in the LLDB.
7. Defects of KVO
KVO is a schema provided to us by Cocoa for subscribing to changes to properties of other objects. KVO is strong, yes. Knowing its internal implementation might help you use it better, or debug it more easily if it goes wrong. But the API provided by the official KVO doesn’t work. Even in our daily development, we rarely use the official KVO API unless we absolutely have to.
In The key-value Observing Done Right article of Soroush Khanlou, KVO is criticized all over the body. Such as:
KVO all comes through one method
: only through rewrite – observeValueForKeyPath: ofObject: change: context: method to get a notice, want to provide custom selector, want to preach a block, no way!KVO is string-ly typed
The keyPath passed is a string, and any typos or misspellings will not be detected by the compiler and will not work properly.KVO requires you to handle superclasses yourself
: KVO requires us to deal with the superclass problem ourselves. In particular, it deals with subclasses that inherit from the same parent class and have the same keypath.KVO can crash when deregistering
: KVO requires manual removal, which can cause a crash, and KVO does not have a built-in way to check whether objects are registered or destroyed. For example, what happens if a superclass also observes the same argument on the same object? It is removed twice, the second time causing a crash.
If you want to be perfect to solve these problems, it is recommended that the reading under the powerful custom facebookarchive/KVOController KVO framework
Eight. Summary
In Objective-C and Cocoa, there are many ways for events to communicate with each other, and each has varying degrees of form and coupling
NSNotification
:
- Provides a central hub where any part of the application can notify or be notified of changes to other parts of the application. The only thing you need to know is what you’re looking for, mainly by the name of the notice.
- UIApplicationDidReceiveMemoryWarningNotification is sent application, for example, a signal out of memory.
- The recipient thread of the notification depends on the sender of the notification (the child thread sends the notification and receives it in the same child thread, in which case updating the UI requires cutting back to the thread).
Key-Value Observing
:
- Allows ad-hoc event introspection by listening for a specific keypath change between specific objects.
- For example, a ProgressView can look at the network request’s numberOfBytesRead to update its own Progress property.
Delegate
- Event-passing is a popular design pattern for passing events by defining a series of methods to be passed to a specified processing object.
- For example, UIScrollView sends scrollViewDidScroll: to its proxy every time its scroll offset changes
Callbacks
- Whether it’s something like the completionBlock in NSOperation (which fires when isFinished==YES) or a function pointer in C, pass a function hook
- For example: NetworkReachabilitySetCallback (3).
Finally, the flowchart of KVO is attached:
The resources
Key-value Observing Programming Guide-Apple Official document
IOS low-level exploration KVO – Leejunhui_ios
KVC and KVO in Objective-C
Key-Value Observing – NSHipster