KVO profile
Apple official documentation KVO introduction
Key-value observing (KVO) is a mechanism that allows an object to be notified when the specified properties of other objects change.
KVO details
Options
Options affects the contents of the change dictionary provided in the notification and how the notification is generated.
NSKeyValueObservingOptionNew
: gets the changed valueNSKeyValueObservingOptionOld
: gets the value before the changeNSKeyValueObservingOptionInitial
: Observe the initial value (register the method to handle notifications immediately called)NSKeyValueObservingOptionPrior
: called once before and after a value change
Context
When the addObserver: forKeyPath: options: context: method are introduced to NULL context, in observeValueForKeyPath: ofObject: The change:context: method can handle callbacks using object and KeyPath. If you pass in a unique identifier context, you can use the context directly to distinguish between callbacks.
Remove observer
When the current page release need to remove the observer, or when the observed object is a singleton, page (observers) is released, the singleton exists, the registration is still effective, last time the page was again registered as observers, would be considered to have two observers, the attributes change, trigger notifications, but the first observer has been released, This causes wild pointer access.
Manual/automatic
It is enabled automatically by defaultKVO
When you want to manually controlKVO
When, except above to returnNO
And also do some processing on the observed properties, like in my case, the observed propertiesage
For example:
KVO App (Download progress)
- Define an observed property
downloadProgress
, and two other attributes (totalData
,writtenData
) is used to calculate progress
@interface DownLoader : NSObject
@property (nonatomic.copy) NSString *downloadProgress;
@property (nonatomic.assign) double writtenData;
@property (nonatomic.assign) double totalData;
@end
Copy the code
2. Implement the getter method of downloadProgress
- (NSString *)downloadProgress{
return [[NSString alloc] initWithFormat:@"%f".1.0f*self.writtenData/self.totalData];
}
Copy the code
3. Configure monitoring totalData and writtenData, and obtain downloadProgress by calculation. That is, downloadProgress changes depend on totalData, writtenData.
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"downloadProgress"]) {
NSArray *affectingKeys = @[@"totalData".@"writtenData"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
Copy the code
Use 4.
Look at mutable arrays
usemutableArrayValueForKey
Gets an array and performs array manipulation.
KVO underlying principles
KVO registered
That is, instance objectsp
registeredKVO
, a subclass is generatedNSKVONotifying_Person
, named asNSKVONotifying_<className>
.
KVO subclass
NSKVONotifying_Person
What is being implemented?Override the following methods:
SetAge:
:class
Will:KVO
Generated subclassesisa
A pointer to the current class is verified as follows:
dealloc
: called when destroying._isKVOA
: An identifier indicating that the current class isKVO
Generated subclasses
Explore a class method hereSetAge:
The call.Pictured above,[Person setAge:]
These methods are also called before the call:
[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:]
[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:]
_NSSetLongLongValueAndNotify
Copy the code
In other words, the setAge method of the dynamically generated subclass of KVO will call the above three methods internally, and then call the setAge method of the parent class again.
Call stack (); call ();
NSKeyValueDidChange
NSKeyValueNotifyObserver
[ViewController observeValueForKeyPath:ofObject:change:context:]
Copy the code
Finally, the sequence of setAge listener calls is as follows:
1.[NSKVONotifying_Person setAge:]
2._NSSetLongLongValueAndNotify
3.[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:]
4.[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:]
5.[Person setAge:]
6.NSKeyValueDidChange
7.NSKeyValueNotifyObserver
8.[ViewController observeValueForKeyPath:ofObject:change:context:]
Copy the code
NSKeyValueDidChange calls after the call, send a notification call _observeValueForKeyPath: ofObject: changeKind: oldValue: newValue: indexes: context: complete notification processing.
KVO removed
When removing theKVO
When listening, the class becomes original, that is, when registered, the instance object’sisa
Will point to a subclass of the current classNSKVONotifying_<className>
When you remove the wiretap,isa
Let’s go back to the previous class.
Custom KVO
#import <Foundation/Foundation.h>
#import "MWKVOInfo.h"
NS_ASSUME_NONNULL_BEGIN
@interface NSObject (KVO)
- (void)mw_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(MWKeyValueObservingOptions)options context:(nullable void *)context;
- (void)mw_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
- (void)mw_observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey.id> *)change context:(nullable void *)context;
@end
NS_ASSUME_NONNULL_END
Copy the code
#import "NSObject+KVO.h"
#import <objc/message.h>
static NSString *const kMWKVOPrefix = @"MWKVONotifying_";
static NSString *const kMWKVOAssiociateKey = @"kMWKVO_AssiociateKey";
@implementation NSObject (KVO)
- (void)mw_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(MWKeyValueObservingOptions)options context:(nullable void *)context
{
//1. Do not observe member variables, only observe attributes, by whether there is a setter method
[self judgeSetterMethodFromKeyPath:keyPath];
//2. Dynamically generate subclasses
Class newClass = [self createChildClassWithKeyPath:keyPath];
//3. Isa points to the newly generated subclass
object_setClass(self, newClass);
//4. Save observer information
MWKVOInfo *info = [[MWKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath options:options];
NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kMWKVOAssiociateKey));
if(! observerArr) { observerArr = [NSMutableArray arrayWithCapacity:1];
[observerArr addObject:info];
objc_setAssociatedObject(self, (__bridge const void* _Nonnull)(kMWKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC); }} - (void)mw_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath
{
NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kMWKVOAssiociateKey));
if (observerArr.count<=0) {
return;
}
for (MWKVOInfo *info in observerArr) {
if ([info.keyPath isEqualToString:keyPath]) {
[observerArr removeObject:info];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kMWKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
break; }}if (observerArr.count<=0) {
// refer back to the parent class
Class superClass = [self class];
object_setClass(self, superClass); }}#pragmaMark **- Dynamically generate subclasses **
- (Class)createChildClassWithKeyPath:(NSString *)keyPath
{
NSString *oldClassName = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@ % @ % @ "",kMWKVOPrefix,oldClassName];
Class newClass = NSClassFromString(newClassName);
// Prevent repeated creation of new classes
if (newClass) return newClass;
/** * If memory is not present, create generate * parameter 1: parent class * parameter 2: name of the new class * parameter 3: extra space created by the new class */
// 2.1: Application class
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
// 2.2: Registration class
objc_registerClassPair(newClass);
// 2.3.1: Add class: the class refers to the class of the observed object
SEL classSEL = NSSelectorFromString(@"class");
Method classMethod = class_getInstanceMethod([self class], classSEL);
const char *classTypes = method_getTypeEncoding(classMethod);
class_addMethod(newClass, classSEL, (IMP)mw_class, classTypes);
// 2.3.2: Add setters
SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod([self class], setterSEL);
const char *setterTypes = method_getTypeEncoding(setterMethod);
class_addMethod(newClass, setterSEL, (IMP)mw_setter, setterTypes);
return newClass;
}
Class mw_class(id self,SEL _cmd){
return class_getSuperclass(object_getClass(self));
}
static void mw_setter(id self,SEL _cmd,id newValue){
// 4: message forwarding: forward to the parent class
// Change the value of the parent class - can be cast
NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
id oldValue = [self valueForKey:keyPath];
void (*mw_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
// void /* struct objc_super *super, SEL op, ... * /
struct objc_super superStruct = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self)),};//objc_msgSendSuper(&superStruct,_cmd,newValue)
mw_msgSendSuper(&superStruct,_cmd,newValue);
// 1: Get the observer
NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kMWKVOAssiociateKey));
for (MWKVOInfo *info in observerArr) {
if ([info.keyPath isEqualToString:keyPath]) {
dispatch_async(dispatch_get_global_queue(0.0), ^ {NSMutableDictionary<NSKeyValueChangeKey.id> *change = [NSMutableDictionary dictionaryWithCapacity:1];
// New and old values are processed
if (info.options & MWKeyValueObservingOptionNew) {
[change setObject:newValue forKey:NSKeyValueChangeNewKey];
}
if (info.options & MWKeyValueObservingOptionOld) {
[change setObject:@ "" forKey:NSKeyValueChangeOldKey];
if (oldValue) {
[change setObject:oldValue forKey:NSKeyValueChangeOldKey]; }}// 2: The message is sent to the observer
SEL observerSEL = @selector(mw_observeValueForKeyPath:ofObject:change:context:); objc_msgSend(info.observer,observerSEL,keyPath,self,change,NULL); }); }}} - (void)mw_observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey.id> *)change context:(nullable void *)context
{
}
#pragmaMark **- Verifies the existence of setter methods **
- (void)judgeSetterMethodFromKeyPath:(NSString *)keyPath{
Class superClass = object_getClass(self);
SEL setterSeletor = NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod(superClass, setterSeletor);
if(! setterMethod) {@throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@" Old iron has no setter for current %@",keyPath] userInfo:nil]; }}#pragmaMark **- Gets the name of the set method from the get method key ===>>> setKey:**
static NSString *setterForGetter(NSString *getter) {if (getter.length <= 0) { return nil; }NSString *firstString = [[getter substringToIndex:1] uppercaseString];
NSString *leaveString = [getter substringFromIndex:1];
return [NSString stringWithFormat:@"set%@%@:",firstString,leaveString];
}
#pragmaMark **- Gets the name set of the getter method from the set method<Key>:===> key**
static NSString *getterForSetter(NSString *setter) {if (setter.length <= 0| |! [setter hasPrefix:@"set") | |! [setter hasSuffix:@ ","]) { return nil; }NSRange range = NSMakeRange(3.setter.length4 -);
NSString *getter = [setter substringWithRange:range];
NSString *firstString = [[getter substringToIndex:1] lowercaseString];
return [getter stringByReplacingCharactersInRange:NSMakeRange(0.1) withString:firstString];
}
@end
Copy the code