1. Introduction of KVO
KVO: key-value-observing is a mechanism that allows you to notify other objects of specified property changes. Key-value observations provide a mechanism for notifying objects of specific property changes of other objects. It is particularly useful for communication between the model layer and the controller layer in an application. Controller objects typically observe the properties of model objects, and view objects observe the properties of model objects through controllers. In addition, however, model objects may observe other model objects (often used to determine when a dependency value changes) or even themselves (again used to determine when a dependency value changes). You can look at properties, including simple properties, one-to-one relationships, and multi-type relationships. Observers exposed to many relationships are informed of the types of changes made and which objects are involved in the changes. Because KVO is not open source, please refer to the official documentation
2. Common API usage
To take a simple example, we have a Person class that has an Account class with attributes like balance in it. We ask the account to add an observer, Person, to tell the Person when the money in the account changes, and remove the observer when it is no longer needed.
2.1 registered
[self.account addObserver:self forKeyPath:@"balance" options:NSKeyValueObservingOptionNew context:NULL];
Copy the code
Let person look at the balance of the account, where options
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
NSKeyValueObservingOptionNew = 0x01.// When the observed property is sent to change, we return its latest change
NSKeyValueObservingOptionOld = 0x02.// When the observed property changes, we return the value before it changed
NSKeyValueObservingOptionInitial API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0=))0x04.// Using it in one-to-one situations immediately sends a notification to the observer; When this option is with - addObserver: toObjectsAtIndexes: forKeyPath: options: context when used together, add to the observer each index send notification object.
NSKeyValueObservingOptionPrior API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0=))0x08 // Send separate notifications to observers before and after each change
}
Copy the code
The context pointer in the message contains arbitrary data that will be returned to the observer in the corresponding change notification. You might specify NULL and rely entirely on the key path string to determine the source of the change notification, but this approach can cause problems for objects whose superclass is also observing the same key path for different reasons. Context is a nullable void * type. We can pass NULL, which represents a unique identifier, so we can find it directly. For example, an observer is observing multiple objects but they all call the same method.
static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;
Copy the code
2.2 notice
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"% @",change);
}
Copy the code
In order to receive from the Account change notice, the Person realizes the observeValueForKeyPath: ofObject: change: context: method, this is all observers need to be. The Account sends this message to Person when one of the registered key paths changes. The Person can then take appropriate action based on the change notification.
2.3 remove
[self.account removeObserver:self forKeyPath:@"balance"];
Copy the code
In the end, when it no longer needs to notice, at least before it was released from distribution, the Person instance must by sending messages to the Account removeObserver: forKeyPath to cancel the registration.
2.4 Manual change notification
NSObject provides a basic implementation for automatic notification of key value changes. Automatic key-value change notification notifies the observer of changes made with key-value matching accessors and key-value encoding methods. For example: setter methods, KVC. In some cases, you may want to control the notification process, for example, by minimizing the triggering of notifications that are not necessary for application-specific reasons, or by grouping some changes into a single notification. Manual change notifications provide the means to do this
// Automatic switch
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"balance"]) {
automatic = NO;// Specify keypath
}
else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
// Implements the instance accessor method for manual notification
- (void)setBalance:(double)theBalance {
[self willChangeValueForKey:@"balance"];// Will change
_balance = theBalance;
[self didChangeValueForKey:@"balance"];
}
Copy the code
2.5 One-to-one Relationship
In many cases, the value of an attribute depends on the value of one or more other attributes in another object. If the value of an attribute changes, the value of the derived attribute should also be marked as changed. How you ensure that key-value observation notifications are published for these dependent properties depends on the cardinality of the relationship. For example, a person’s full name depends on first and last names. The method for returning the full name can be written as follows:
- (NSString *)fullName {
return [NSString stringWithFormat:@"% @ % @",firstName, lastName];
}
Copy the code
The application observing the fullName attribute must be notified when the firstName or lastName attributes change, because they affect the value of the attribute. One solution is to cover keyPathsForValuesAffectingValueForKey: specify a person’s fullName attribute depends on the lastName and firstName attribute
+ (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
Normally you should call super and return a collection containing any members that are created as a result of doing so (so as not to interfere with the overwriting of this method in the superclass). You can also by implementing the following naming convention keyPathsForValuesAffecting < Key > class methods to achieve the same results, including the < Key > is dependent on the value of the attribute (capitalize the first letter) name, for example fullName:
+ (NSSet *)keyPathsForValuesAffectingFullName {
return [NSSet setWithObjects:@"lastName"The @"firstName", nil];
}
Copy the code
2.6 One-to-many Relationship
The above method does not support multiple relationships. For example, Person has multiple accounts, and we hope to immediately tell Person how much my totalSalary is when salary is paid to each account.
@interface KBPerson : NSObject
@property (nonatomic, assign) NSNumber *totalSalary;
@property (nonatomic, strong) NSArray *accounts;
@end
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([keyPath isEqualToString:@"salary"]) {
[self updateTotalSalary];
}
else{}} - (void)updateTotalSalary {
[self setTotalSalary:[self valueForKeyPath:@"[email protected]"]];
}
// Implement it
KBAccount *account1 = [[KBAccount alloc]init];
account1.salary = 12;1 / / account
KBAccount *account2 = [[KBAccount alloc]init];
account2.salary = 1;2 / / account
self.person.accounts = @[account1,account2];
[account1 addObserver:self forKeyPath:@"fullName" options:NSKeyValueObservingOptionNew context:fullNameContext];
[account1 addObserver:self.person forKeyPath:@"salary" options:NSKeyValueObservingOptionNew context:NULL];// Tell person the payday has been paid
[account2 addObserver:self.person forKeyPath:@"salary" options:NSKeyValueObservingOptionNew context:NULL];
[self.person addObserver:self forKeyPath:@"totalSalary" options:NSKeyValueObservingOptionNew context:totalSalaryContext];// Tell the current self, total Salary to change
// Simulate changes- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
KBAccount *account1 = self.person.accounts[0];
NSInteger salary = arc4random()%100;
KBAccount *account2 = self.person.accounts[1];
NSInteger salary2 = arc4random()%100;
NSLog(@"Account 1 is paid: %ld",salary);
account1.salary = salary;
NSLog(@"Account 2 is paid :%ld",salary2 );
account2.salary = salary2;
}
// Listen for changes
/ / change
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if (context == totalSalaryContext) {
NSLog(@"Total assets in personal account: %@",[change objectForKey:NSKeyValueChangeNewKey]);
}else if(context == fullNameContext){
NSLog(@"Changed: %@",[change objectForKey:NSKeyValueChangeNewKey]); }}Copy the code
2.6 Mutable array observation
When our observer’s properties are mutable arrays, adding them directly does not call setter methods and does not trigger kVO notification callbacks
self.person.dateArray = [NSMutableArray arrayWithCapacity:1];
[self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];
[self.person.dateArray addObject:@"1"];/ / not to take effect
Copy the code
The official recommendation is that we use mutableArrayValueForKey, which triggers the kvo callback via KVC.
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person.nick = [NSString stringWithFormat:@"% @ +",self.person.nick];
// self.person.writtenData += 10;
// self.person.totalData += 1;
// KVC collection array
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
}
Copy the code
One of thekind
saidThe type of key value change
, is an enumeration, there are four main types
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1./ / set value
NSKeyValueChangeInsertion = 2./ / insert
NSKeyValueChangeRemoval = 3./ / remove
NSKeyValueChangeReplacement = 4./ / replace
};
Copy the code
3. Underlying principles
View official Documents
Kvo uses a technique calledisa-swizzing
“Technology to achieve.
isa
The pointer, as the name implies, points to the one that maintains the dispatch tableThe class of the object
. This dispatch table essentially containsA method that points to a class implementation
And other data.- Property of the observed object when the observer registers its properties
isa
Pointer to theHas been modified
To point toThe middle class
, instead of the real class. As a result,isa
The value of a pointer does not necessarily reflect the actual class of the instance. - You shouldn’t
isa
Pointer to determineThe members of the class
. Instead, you should useClass method to determine the class of an object instance
.
3.1 KVO triggering mechanism
We looked at the documentation earlier to say that KVO is triggered by setter methods. Let’s verify that
self.student = [[KBStudent alloc]init];
[self.student addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
[self.student addObserver:self forKeyPath:@"nickName"options:NSKeyValueObservingOptionNew context:NULL]; - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.student.name = @"jack";
self.student->nickName = @"Smith";
}
Copy the code
Member variables passTranslation memory
Unable to triggerkvo
The callback,setter
There is a callback. We use thekvo
And implementsetter
methods
- (void)setNickName:(NSString*)nickName
{
self->nickName = nickName;
}
Copy the code
Conclusion:KVO
The trigger is monitoredsetter
Method, only implementedsetter
Method to trigger.
3.2 the middle class
The official documentation states that KVO is implemented by swapping isa for instance objects, which generates an intermediate class. Let’s print out the before and after changes
#pragma mark - Traverses classes and subclasses - (void)printClasses:(Class)cls{
// Total number of registered classes
int count = objc_getClassList(NULL, 0);
// Create an array containing the given object
NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
// Get all registered classes
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
printKBStudent
And subclasses of it, added one moreNSKVONotifying_KBStudent
And this is the middle class, which is the middle classKBStudent
The subclass. Let’s check LLDB
Add our instance object beforeisa
Point to the0x000000010e1551e8
KBStudent
Class, added to point toNSKVONotifying_KBStudent
In the class.
3.3 What’s in the middle class?
Does it continue the methods, attributes, and member variables of the parent class?
#pragma mark -ivar-property - (void)printClassAllMethod:(Class)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);
}
// Prints member variables- (void)printClassAllIvars:(Class)cls{
unsigned int count = 0;
Ivar *ivars = class_copyIvarList(cls, &count);
for (int i = 0; i<count; i++) {
Ivar ivar = ivars[i];
NSString *ivarName = [NSString stringWithFormat:@"%s", ivar_getName(ivar)];
NSLog(@"% @",ivarName);
}
free(ivars);
}
// Print attributes- (void)printClassAllPropertys:(Class)cls{
unsigned int count = 0;
objc_property_t *propertys = class_copyPropertyList(cls, &count);
for (int i = 0; i<count; i++) {
objc_property_t property = propertys[i];
NSString *propertyName = [NSString stringWithFormat:@"%s", property_getName(property)];
NSLog(@"% @",propertyName);
}
free(propertys);
}
Copy the code
Prints the result, not inheriting the parent class exactly. It’s understandable, actually, that we areKBStudent
Add observation main observationkeypath
isname
That’s all we need to worry about in the middle classsetName
The implementation of.Class (get the current class)
,Dealloc (Destroy)
._isKVOA (check whether kVO)
Is to rewriteNSObject
.
Why rewrite and not inherit?
1. We can see that the addresses of the printing methods are not the same.
2. The subclass inherits the parent class method itself does not rewrite, the subclass method list is not, call the time to look up the parent class chain.
3.4 What does the intermediate class setter method do?
We’re changing the name externally, which is calling the setter for name, because the isa swap is actually calling the setter for the middle class, but we’re also changing the name property for KBStudent, otherwise we’re going to have a problem, the middle class is pretty housekeeping, Take care of hind still need to tell host.
self.student.name = @"jack";
/ / to enter- (void)setName:(NSString *)name
{
_name = name;
NSLog(@"%s",__func__);
}
Copy the code
It can be concluded that:
- When the outside calls
setter
Method, the actual entrySetter methods for intermediate classes
To implement. - The middle class
setter
The method will go firstnotice
The outsideThe observer
The value of your observation is going to change. - Sends a message to the parent class
Call the setter of the parent class
Methods, practical change.
3.5 Is the middle class destroyed when the observer dealloc?
We remove it in our dealloc controller, and hit a breakpoint.
The isa of the removeObserver instance object still points to the intermediate class NSKVONotifying_KBStudent. Removed:
We’re going back to what we had beforeisa
Point, pointThis class
.
- Why do we need instance objects to be destroyed
remove
reductionisa
To prevent wild Pointers. For example, our student isa simple interest. If we do not restore isa, the next access will crash.
So let’s remove and go back to the previous page and print out the middle class and see if it exists
I didn’t destroy it, which is understandable, but it’s kind of complicated to create this class, so destroy it. Users constantly coming in and out of a page, constantly adding and removing, can consume a lot of memory,Waste of memory
.
4. To summarize
kvo
It’s like we’re monitoring a value of interest in real time, based on that valueSetter method listening
When called to tell the observer that the value is about to change, remove the observation when not in use.- Kvo internal implementation is through a
The middle class
The setter for the middle class is basicallyNotifies the observer of a value change
.Call the setter of the parent class
Method, actually change. This design is equivalent to an intermediate class acting as a butler, as long as the external instructions, the specific implementation of the butler,The coupling is reduced
. - kvo
remove
mainlyReduction of the isa
, restore the isa of the instance object from the intermediate class toThis class
The middle class,Don't destroy
To facilitate the next association.