1. KVO detail analysis
The context of use
Go to the Official Apple documentation to see the definition of context:
Context
The context pointer in theaddObserver:forKeyPath:options:context:
message contains arbitrary data that will be passed back to the observer in the corresponding change notifications. You may specifyNULL
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.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.
The address of a uniquely named static variable within your class makes a good context. Contexts chosen in a similar manner in the super- or subclass will be unlikely to overlap. You may choose a single context for the entire class and rely on the key path string in the notification message to determine what changed. Alternatively, you may create a distinct context for each observed key path, which bypasses the need for string comparisons entirely, resulting in more efficient notification parsing. Listing 1 shows example contexts for the
balance
andinterestRate
properties chosen this way.
Registration method addObserver: forKeyPath: options: context: in the context can pass in any data, and can receive the data in the monitoring method.
-
Context functions: tag-distinguish, which can determine the properties of the observed object more accurately. Context is used for inheritance and multiple listening. It can also be used to pass values.
KVO
There is only one listening callback methodobserveValueForKeyPath:ofObject:change:context:
, which we can usually specify in the registration methodcontext
forNULL
And pass in the listening methodobject
andkeyPath
To determine the triggerKVO
The source of the.- But if there is inheritance, such as the Person class and its two subclasses Teacher and Student, the Person, Teacher, and Student instance objects all observe the balance property of the Account object. Question:
- When balance changes, who should handle it?
- If it’s all handled by Person, how do you determine if it’s your own transaction or a subclass’s transaction in the person class’s listener method?
- By using
context
You can solve this problem nicely in the registration methodcontext
Set a unique value, and then set the values in the listener methodcontext
The value can be checked.
-
Apple’s recommended usage: Use context to precisely determine the properties of the observed object, and use the address of a uniquely named static variable as the context’s value. You can set a context for the entire class, and then use object and keyPath in the listener methods to determine the properties to be viewed, so that the presence of inheritance can be determined by context; You can also set a different context for each property of the observed object, so that you can use the context to accurately determine the properties of the observed object.
static void *PersonAccountBalanceContext = &PersonAccountBalanceContext; static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext; Copy the code
- (void)registerAsObserverForAccount:(Account*)account { [account addObserver:self forKeyPath:@"balance" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:PersonAccountBalanceContext]; [account addObserver:self forKeyPath:@"interestRate" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:PersonAccountInterestRateContext]; } Copy the code
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (context == PersonAccountBalanceContext) { // Do something with the balance… } else if (context == PersonAccountInterestRateContext) { // Do something with the interest rate… } else { // Any unrecognized context must belong to super [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } } Copy the code
-
Context advantages: Less nesting, high performance, more security, and strong scalability.
-
Context note:
- If an object is passed, a strong reference to it must be held before the observation is removed, or accessed in the listener method
context
Could lead toCrash
; - airborne
NULL
You shouldn’tnil
.
- If an object is passed, a strong reference to it must be held before the observation is removed, or accessed in the listener method
Remove observer
For the observer, many people on the Internet say that iOS9 will not be removed, is that right? If we go to apple’s official documentation, we can find the following:
An observer does not automatically remove itself when deallocated. The observed object continues to send notifications, oblivious to the state of the observer. However, a change notification, like any other message, sent to a released object, triggers a memory access exception. You therefore ensure that observers remove themselves before disappearing from memory.
- The short answer is: when
The observer
It doesn’t actively remove itself when it’s released.To observe the object
Notifications will continue to be sent, and messages will be sent to memory that has been freed, and exceptions will be thrownCrash
. soThe observer
To remove themselves before release, use the following example to illustrate.
Create the SSLDetailViewController class:
@interface SSLDetailViewController () @property (nonatomic, strong) SSLStudent *student; @implementation SSLDetailViewController // student is the property of the previous page - (instancetype)initWithStudent:(SSLStudent *)student { if (self = [super init]) { self.student = student; } return self; } - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor orangeColor]; // weak observer [self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL]; } - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ self.student.name = @"hello word"; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{ NSLog(@"SSLDetailViewController :%@",change); } - (void)dealloc {} @endCopy the code
student
The value is passed from the property of the previous page as an object to observe.SSLDetailViewController
Is the observer,dealloc
There is no operation in.touchesBegan
changename
Set out,KVO
.
Push to the SSLDetailViewController page, and then the ‘touchesBegan’ page displays the result:
2021-07-28 17:38:06.665226+0800 KVO [34046:504715] SSLDetailViewController :{kind = 1; new = "hello word"; }Copy the code
Pop leaves the page and pushes the SSLDetailViewController page again. The student is still passing in values from the previous page.
- So follow apple’s official recommendation: During observer initialization (
init
orviewDidLoad
Register as an observer during release (dealloc
Call the remove method, which ensures that they are paired and is an ideal way to use them.
Automatic triggering of KVO
Can be observed in the class of the object in the rewrite + (BOOL) automaticallyNotifiesObserversForKey: (nsstrings *) key methods to control the KVO trigger automatically.
If we only allow outsiders to observe the NAME property of SSLStudent, we can do the following in the SSLStudent class. This allows the outside world to observe only the name property, and even if the outside world registers listening for other properties of the SSLStudent object, KVO is not triggered when the property changes.
/ / the return value represents allow + (BOOL) are not allowed to trigger the KVO automaticallyNotifiesObserversForKey: (nsstrings *) key {BOOL automatic = NO. if ([key isEqualToString:@"name"]) { automatic = YES; } else { automatic = [super automaticallyNotifiesObserversForKey:key]; } return automatic; }Copy the code
Manual trigger of KVO
To minimize unnecessary notification-triggering operations, or to call a listening method for a property change when multiple changes are present.
KVO can be triggered manually by manually calling willChangeValueForKey: and didChangeValueForKey: before and after assigning a value to a member variable, as shown in the following example:
+ (BOOL)automaticallyNotifiesObserversOfName
{
return NO;
}
Copy the code
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[self.student willChangeValueForKey:@"age"];
self.student->_age = 18;
[self.student didChangeValueForKey:@"age"];
}
Copy the code
NSKeyValueObservingOptionPrior
(trigger method before and after value change respectively, that is, there are two triggers in one change)willChangeValueForKey:
anddidChangeValueForKey:
Was carried out at the time.- If the registration method
options
The incomingNSKeyValueObservingOptionPrior
, then you can do this by just callingwillChangeValueForKey:
To trigger the one before the changeKVO
Can be used to do something about a property value before it is about to change.
One-to-many relationship
In some cases, a change in one property depends on a change in one or more other properties, that is, when other properties change, the property will also change.
For example, if we want to do KVO listening on the downloadProgress property in the Download class, the change of the property depends on the change of the writtenData and totalData properties. The observer listens for downloadProgress and should also be notified when the values of the writtenData and totalData properties change.
Override the following method to indicate that the downloadProgress property depends on writtenData and totalData:
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key { NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key]; if ([key isEqualToString:@"downloadProgress"]) { NSArray *affectingKeys = @[@"writtenData",@"totalData"]; keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys]; } return keyPaths; } - (NSString *)downloadProgress{ if (self.writtenData == 0) { self.writtenData = 10; } if (self.totalData == 0) { self.totalData = 100; } return [[nsstrings alloc] initWithFormat: @ "% f", 1.0 f * self writtenData/self totalData]; }Copy the code
Call code in SSLDetailViewController:
- (void)viewDidLoad {
[super viewDidLoad];
[self.student addObserver:self forKeyPath:@"downloadProgress" options:(NSKeyValueObservingOptionNew) context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person.writtenData += 10;
self.person.totalData += 1;
}
Copy the code
Observations of mutable arrays
KVO can listen for changes in individual properties as well as collection objects. When monitoring the change of the collection object, you need to obtain the proxy object through methods such as mutableArrayValueForKey: of KVC, and use the proxy object to operate. When the internal object of the proxy object changes, KVO’s monitoring method will be triggered. The collection object contains NSArray and NSSet. (Note: KVO is not triggered if you make operational changes directly to a collection object.)
Sample code:
- (void)viewDidLoad { [super viewDidLoad]; [self.student addObserver:self forKeyPath:@"dataArray" options:(NSKeyValueObservingOptionNew) context:NULL]; } - (void)touch began :(NSSet< touch *> *)touches withEvent:(UIEvent *)event{// KVC set array [[self.student mutableArrayValueForKey:@"dateArray"] addObject:@"1"]; [[self.student mutableArrayValueForKey:@"dateArray"] removeObject:@"1"]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{ NSLog(@"%@",change); }Copy the code
View the print result:
2021-07-29 14:44:31.606480+0800 001-- KVO[15464:280688] {indexes = "<_NSCachedIndexSet: 0x600000b68f20>[number of indexes: 1 (in 1 ranges), indexes: (0)]"; kind = 2; new = ( 1 ); } 2021-07-29 14:44:31.606802+0800 001-- KVO[15464:280688] {indexes = "<_NSCachedIndexSet: 0x600000b68f20>[number of indexes: 1 (in 1 ranges), indexes: (0)]"; kind = 3; }Copy the code
- about
kind
Value:typedef NS_ENUM(NSUInteger, NSKeyValueChange) { NSKeyValueChangeSetting = 1, NSKeyValueChangeInsertion = 2, NSKeyValueChangeRemoval = 3, NSKeyValueChangeReplacement = 4, }; Copy the code
Two, KVO principle analysis
KVO implementation official documentation
Let’s take a look at the Official Apple documentation to see what it says about KVO implementation:
Key-Value Observing Implementation Details
Automatic key-value observing is implemented using a technique called 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.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.
You should never rely on the
isa
pointer to determine class membership. Instead, you should use theclass
method to determine the class of an object instance.
- Mentioned here
KVO
The implementation of theisa-swizzling
Technology,isa
It points to a new class, which you’ll explore with examples.
KVO dynamically generates subclasses
Get self.person class name from object_getClassName before KVO call:
- At this moment
self.person
Is the name of the classSSLPerson
No problem.
Get the self.person class name from object_getClassName after KVO:
- At this moment
self.person
The class name of theNSKVONotifying_SSLPerson
, so it andSSLPerson
What is the relationship?
The test code is created as follows, printing the subclasses of SSLPerson before the KVO call and the subclasses of SSLPerson and NSKVONotifying_SSLPerson after the KVO call
- (void)viewDidLoad { [super viewDidLoad]; self.person = [[SSLPerson alloc] init]; [self printClasses:[SSLPerson class]]; [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL]; [self printClasses:[SSLPerson class]]; [self printClasses:objc_getClass("NSKVONotifying_SSLPerson")]; Int count = objc_getClassList(NULL, 0); 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
View the print result:
[18966:358659] classes = (SSLPerson, SSLStudent) 2021-07-29 16:10:06.847060+0800 002-- Classes = (SSLPerson, "NSKVONotifying_SSLPerson", SSLStudent) 2021-07-29 16:10:06.849750+0800 002-- Classes = ("NSKVONotifying_SSLPerson")Copy the code
- You can see the newly created
NSKVONotifying_SSLPerson
isSSLPerson
And has no subclasses of its own.
Next, let’s look at what happens when removeObserver is called. Breakpoint to the dealloc of the ViewController and print the result:
self.person
theisa
It’s pointing backSSLPerson
.NSKVONotifying_SSLPerson
It didn’t destroy it. It becameSSLStuent
The subclass.
What are the methods for dynamically generated subclasses of KVO
1. View all methods
Create code like this to print all the methods of the NSKVONotifying_SSLPerson class:
- (void)viewDidLoad { [super viewDidLoad]; self.person = [[SSLPerson alloc] init]; [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL]; [self printClassAllMethod:objc_getClass("NSKVONotifying_SSLPerson")]; } -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); }Copy the code
View the print result:
setNickName:-0x7fff207bf03f
class-0x7fff207bdb49
dealloc-0x7fff207bd8f7
_isKVOA-0x7fff207bd8ef
Copy the code
- All of these ways that you can print it, all of them
NSKVONotifying_SSLPerson
Rewrite. _isKVOA
Is to determine whether the current class isKVO
Created.dealloc
Of the instance variable inisa
Repoint back to the originalSSLPerson
Class.
2. class
Methods to rewrite
Breakpoint debugging:
- Rewrite the
class
Method, returnsNSKVONotifying_SSLPerson
The parent classSSLPerson
.
3. setter
Methods to rewrite
Look at the sample code:
- (void)viewDidLoad { [super viewDidLoad]; self.person = [[SSLPerson alloc] init]; [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL]; [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL]; } - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ self.person.nickName = @"SSL"; self.person->name = @"Spring"; } // KVO callback - (void) observeforkeypath :(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{ NSLog(@"%@",change); }Copy the code
Click on the screen to view the print result:
2021-07-29 17:49:08.827957+0800 002-- KVO principle exploration [22666:436552] {kind = 1; new = SSL; }Copy the code
- You can see the member variables
name
It’s not triggered,nickName
Property is triggered, indicating insetNickName
MethodKVO
Related operations of.
In the following figure, when self.person’s ISA is referred back to SSLPerson, SSL can be printed successfully, indicating that setNickName is called for the implementation of the parent class.
3. Customize KVO
Customize basic KVO functions
Create NSObject+SSLKVO class to emulate KVO.
NSObject + SSLKVO. H:
@interface NSObject (SSLKVO)
- (void)ssl_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)ssl_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
@end
Copy the code
NSObject + SSLKVO. M:
static NSString *const kSSLKVOPrefix = @"SSLKVONotifying_"; static NSString *const kSSLKVOAssiociateKey = @"kSSLKVO_AssiociateKey"; @implementation NSObject (SSLKVO) - (void)ssl_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath Options: (NSKeyValueObservingOptions) options context: (nullable void *) context {/ / 1: to verify whether there is a setter method: Don't let the instance in [the self judgeSetterMethodFromKeyPath: keyPath]; / / 2: the dynamically generated Class subclass newClass = [self createChildClassWithKeyPath: keyPath]; SSLKVONotifying_SSLPerson object_setClass(self, newClass); SSLKVONotifying_SSLPerson object_setClass(self, newClass); / / 4: Save observer objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kSSLKVOAssiociateKey), Observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (void)ssl_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{// refer to the parent Class superClass = [self class]; object_setClass(self, superClass); } # pragma mark - verify whether there is a setter method - (void) judgeSetterMethodFromKeyPath keyPath: (nsstrings *) {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]; }} # pragma mark - dynamic subclassing - (Class) createChildClassWithKeyPath keyPath: (nsstrings *) {nsstrings * oldClassName = NSStringFromClass([self class]); NSString *newClassName = [NSString stringWithFormat:@"%@%@",kSSLKVOPrefix,oldClassName]; Class newClass = NSClassFromString(newClassName); If (newClass) return newClass; /** * If memory does not exist, create generate * parameter 1: parent class * parameter 2: name of the new class * parameter 3: additional space created by the new class */ / 2.1: NewClass = objc_allocateClassPair([self class], newClassName.utf8String, 0); // 2.2: Register class objc_registerClassPair(newClass); Sslpersonsel classSEL = NSSelectorFromString(@"class"); Method classMethod = class_getInstanceMethod([self class], classSEL); const char *classTypes = method_getTypeEncoding(classMethod); class_addMethod(newClass, classSEL, (IMP)ssl_class, classTypes); / / 2.3.2: Add setter [SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath)); Method setterMethod = class_getInstanceMethod([self class], setterSEL); const char *setterTypes = method_getTypeEncoding(setterMethod); class_addMethod(newClass, setterSEL, (IMP) ssl_set_types, setterTypes); Add deallocSEL deallocSEL = NSSelectorFromString(@"dealloc"); Method deallocMethod = class_getInstanceMethod([self) class], deallocSEL); const char *deallocTypes = method_getTypeEncoding(deallocMethod); class_addMethod(newClass, deallocSEL, (IMP)ssl_dealloc, deallocTypes); return newClass; } static void ssl_dealloc(id self,SEL _cmd){ Class superClass = [self class]; object_setClass(self, SuperClass);} static void ssl_setter(id self,SEL _cmd,id newValue){NSLog(@" come :%@",newValue); Void (*ssl_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) ssl_msgSendSuper(&superstruct,_cmd,newValue) Let our observer call // - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context // 1: Observer = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kSSLKVOAssiociateKey)); Message is sent to the observer SEL observerSEL = @ the selector (observeValueForKeyPath: ofObject: change: context:); nsstrings * keyPath = getterForSetter(NSStringFromSelector(_cmd)); objc_msgSend(observer,observerSEL,keyPath,self,@{keyPath:newValue},NULL); } Class ssl_class(id self,SEL _cmd){return class_getSuperclass(object_getClass(self));} #pragma mark - Obtains 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];} #pragma mark - Obtains the name of a getter from a set method set<Key>:===> Key static NSString *getterForSetter(NSString *setter){ if (setter.length <= 0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) { return nil;} NSRange range = NSMakeRange(3, setter.length-4); NSString *getter = [setter substringWithRange:range]; NSString *firstString = [[getter substringToIndex:1] lowercaseString]; return [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString]; } @endCopy the code
Custom KVO progression
The above code can already achieve the basic functions of KVO, but can not listen to multiple attributes, next related optimization.
Create an SSLKVOInfo class to hold observer information, add multiple instances of SSLKVOInfo to the array, and store them in self via the associated object.
SSLKVOInfo class:
typedef NS_OPTIONS(NSUInteger, LGKeyValueObservingOptions) {
LGKeyValueObservingOptionNew = 0x01,
LGKeyValueObservingOptionOld = 0x02,
};
@interface SSLKVOInfo : NSObject
@property (nonatomic, weak) NSObject *observer;
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, assign) LGKeyValueObservingOptions options;
- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(LGKeyValueObservingOptions)options;
@end
@implementation SSLKVOInfo
- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(LGKeyValueObservingOptions)options{
self = [super init];
if (self) {
self.observer = observer;
self.keyPath = keyPath;
self.options = options;
}
return self;
}
@end
Copy the code
Optimization methods in NSObject+ SSLKvo. m
- (void)ssl_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(LGKeyValueObservingOptions)options Context: (nullable void *) context {/ / 1: to verify whether there is a setter method: don't let the instance in [the self judgeSetterMethodFromKeyPath: keyPath]; / / 2: the dynamically generated Class subclass newClass = [self createChildClassWithKeyPath: keyPath]; SSLKVONotifying_SSLPerson object_setClass(self, newClass); SSLKVONotifying_SSLPerson object_setClass(self, newClass); SSLKVOInfo *info = [[SSLKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath options:options]; NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kSSLKVOAssiociateKey)); if (! observerArr) { observerArr = [NSMutableArray arrayWithCapacity:1]; [observerArr addObject:info]; objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kSSLKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } } - (void)ssl_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{ NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kSSLKVOAssiociateKey)); if (observerArr.count<=0) { return; } for (SSLKVOInfo *info in observerArr) { if ([info.keyPath isEqualToString:keyPath]) { [observerArr removeObject:info]; objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kSSLKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC); break; }} if (observerarr. count<=0) {// Call the parent Class superClass = [self Class]; object_setClass(self, superClass); }} static void ssl_setter(id self,SEL _cmd,id newValue){NSLog(@" come :%@",newValue); NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd)); NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd)); id oldValue = [self valueForKey:keyPath]; void (*ssl_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) ssl_msgSendSuper(&superStruct,_cmd,newValue); / / 1: Get observer NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kSSLKVOAssiociateKey)); for (SSLKVOInfo *info in observerArr) { if ([info.keyPath isEqualToString:keyPath]) { dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSMutableDictionary<NSKeyValueChangeKey,id> *change = [NSMutableDictionary dictionaryWithCapacity:1]; / / to deal with the old and new values if (info. The options & LGKeyValueObservingOptionNew) {[change setObject: newValue forKey:NSKeyValueChangeNewKey]; } if (info.options & LGKeyValueObservingOptionOld) { [change setObject:@"" forKey:NSKeyValueChangeOldKey]; if (oldValue) { [change setObject:oldValue forKey:NSKeyValueChangeOldKey]; }} / / 2: a message sent to the observer SEL observerSEL = @ the selector (ssl_observeValueForKeyPath: ofObject: change: context:); objc_msgSend(info.observer,observerSEL,keyPath,self,change,NULL); }); }}}Copy the code
Custom KVO function
Through ssl_observeValueForKeyPath: ofObject: change: context: this call is not very convenient, we introduce the functional thought, through the block to realize the monitor callback, look at the next class.
SSLKVOInfo class:
@interface SSLKVOInfo : NSObject
@property (nonatomic, weak) NSObject *observer;
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, copy) SSLKVOBlock handleBlock;
@end
@implementation SSLKVOInfo
- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(SSLKVOBlock)block{
if (self=[super init]) {
_observer = observer;
_keyPath = keyPath;
_handleBlock = block;
}
return self;
}
@end
Copy the code
NSObject + SSLKVO. H:
typedef void(^SSLKVOBlock)(id observer,NSString *keyPath,id oldValue,id newValue);
@interface NSObject (SSLKVO)
- (void)ssl_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(SSLKVOBlock)block;
- (void)ssl_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
@end
Copy the code
NSObject + SSLKVO. M:
static NSString *const kSSLKVOPrefix = @"SSLKVONotifying_";
static NSString *const kSSLKVOAssiociateKey = @"kSSLKVO_AssiociateKey";
@implementation NSObject (SSLKVO)
- (void)ssl_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(SSLKVOBlock)block{
// 1: 验证是否存在setter方法 : 不让实例进来
[self judgeSetterMethodFromKeyPath:keyPath];
// 2: 动态生成子类
Class newClass = [self createChildClassWithKeyPath:keyPath];
// 3: isa的指向 : SSLKVONotifying_LGPerson
object_setClass(self, newClass);
// 4: 保存信息
LGInfo *info = [[LGInfo alloc] initWitObserver:observer forKeyPath:keyPath handleBlock:block];
NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kSSLKVOAssiociateKey));
if (!mArray) {
mArray = [NSMutableArray arrayWithCapacity:1];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kSSLKVOAssiociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[mArray addObject:info];
}
- (void)ssl_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{
NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kSSLKVOAssiociateKey));
if (observerArr.count<=0) {
return;
}
for (LGInfo *info in observerArr) {
if ([info.keyPath isEqualToString:keyPath]) {
[observerArr removeObject:info];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kSSLKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
break;
}
}
if (observerArr.count<=0) {
// 指回给父类
Class superClass = [self class];
object_setClass(self, superClass);
}
}
#pragma mark - 验证是否存在setter方法
- (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:@"老铁没有当前%@的setter",keyPath] userInfo:nil];
}
}
#pragma mark -
- (Class)createChildClassWithKeyPath:(NSString *)keyPath{
NSString *oldClassName = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@"%@%@",kSSLKVOPrefix,oldClassName];
Class newClass = NSClassFromString(newClassName);
// 防止重复创建生成新类
if (newClass) return newClass;
/**
* 如果内存不存在,创建生成
* 参数一: 父类
* 参数二: 新类的名字
* 参数三: 新类的开辟的额外空间
*/
// 2.1 : 申请类
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
// 2.2 : 注册类
objc_registerClassPair(newClass);
// 2.3.1 : 添加class : class的指向是LGPerson
SEL classSEL = NSSelectorFromString(@"class");
Method classMethod = class_getInstanceMethod([self class], classSEL);
const char *classTypes = method_getTypeEncoding(classMethod);
class_addMethod(newClass, classSEL, (IMP)ssl_class, classTypes);
// 2.3.2 : 添加setter
SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod([self class], setterSEL);
const char *setterTypes = method_getTypeEncoding(setterMethod);
class_addMethod(newClass, setterSEL, (IMP)ssl_setter, setterTypes);
// 2.3.3 : 添加dealloc
SEL deallocSEL = NSSelectorFromString(@"dealloc");
Method deallocMethod = class_getInstanceMethod([self class], deallocSEL);
const char *deallocTypes = method_getTypeEncoding(deallocMethod);
class_addMethod(newClass, deallocSEL, (IMP)ssl_dealloc, deallocTypes);
return newClass;
}
static void ssl_dealloc(id self,SEL _cmd){
Class superClass = [self class];
object_setClass(self, superClass);
}
static void ssl_setter(id self,SEL _cmd,id newValue){
NSLog(@"来了:%@",newValue);
NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
id oldValue = [self valueForKey:keyPath];
// 4: 消息转发 : 转发给父类
// 改变父类的值 --- 可以强制类型转换
void (*ssl_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)
ssl_msgSendSuper(&superStruct,_cmd,newValue);
// 5: 信息数据回调
NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kSSLKVOAssiociateKey));
for (LGInfo *info in mArray) {
if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) {
info.handleBlock(info.observer, keyPath, oldValue, newValue);
}
}
}
Class ssl_class(id self,SEL _cmd){
return class_getSuperclass(object_getClass(self));
}
#pragma mark - 从get方法获取set方法的名称 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];
}
#pragma mark - 从set方法获取getter方法的名称 set<Key>:===> key
static NSString *getterForSetter(NSString *setter){
if (setter.length <= 0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) { return nil;}
NSRange range = NSMakeRange(3, setter.length-4);
NSString *getter = [setter substringWithRange:range];
NSString *firstString = [[getter substringToIndex:1] lowercaseString];
return [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
}
@end
Copy the code
FBKVOController exploration
FBKVOController is a Facebook open source framework based on the system KVO implementation. Support for Objective-C and Swift. GitHub:github.com/facebook/KV… .
The advantages of FBKVOController
- The observer is automatically removed.
- Functional programming, you can achieve a line of code system
KVO
The three steps of. - implementation
KVO
As with the code context where the event occurs, there is no need to pass parameters across methods. - increased
block
andSEL
Custom operation pairsNSKeyValueObserving
Processing support for callbacks. - each
keyPath
Will correspond to ablock
orSEL
, do not use if judgmentkeyPath
. - Multiple properties of an object can be monitored at the same time, simple writing.
- Thread safety.
Example of FBKVOController
@interface ViewController () @property (nonatomic, strong) FBKVOController *kvoCtrl; @property (nonatomic, strong) SSLPerson *person; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.person = [[SSLPerson alloc] init]; self.person.name = @"ssl"; self.person.age = 18; self.person.mArray = [NSMutableArray arrayWithObject:@"1"]; [self.kvoCtrl observe:self.person keyPath:@"age" options:0 action:@selector(ssl_observerAge)]; [self.kvoCtrl observe:self.person keyPath:@"name" options:(NSKeyValueObservingOptionNew) block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) { NSLog(@"****name:%@****",change); }]; [self.kvoCtrl observe:self.person keyPath:@"mArray" options:(NSKeyValueObservingOptionNew) block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) { NSLog(@"****mArray:%@****",change); }]; } - (void)ssl_observerAge{NSLog(@" change grade "); } - (void)touch began :(NSSet< touch *> *)touches withEvent:(UIEvent *)event{NSLog(@" touch screen "); self.person.name = [NSString stringWithFormat:@"%@+",self.person]; self.person.age ++; [[self.person mutableArrayValueForKey:@"mArray"] addObject:self.person.name]; } #pragma mark - lazy - (FBKVOController *)kvoCtrl{ if (! _kvoCtrl) { _kvoCtrl = [FBKVOController controllerWithObserver:self]; } return _kvoCtrl; } @endCopy the code
FBKVOController principle
FBKVOController uses the mediator pattern by creating an instance of FBKVOController that does all the listening, callbacks, and releases.
1. How does FBKVOController listen
To observe: keyPath: options: block: methods:
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block { NSAssert(0 ! = keyPath.length && NULL ! = block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPath, block); if (nil == object || 0 == keyPath.length || NULL == block) { return; } // create info _FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block]; // observe object with info [self _observe:object info:info]; }Copy the code
Enter _observe:info: method:
- (void)_observe:(id)object info:(_FBKVOInfo *)info { // lock pthread_mutex_lock(&_lock); NSMutableSet *infos = [_objectInfosMap objectForKey:object]; // check for info existence _FBKVOInfo *existingInfo = [infos member:info]; if (nil ! = existingInfo) { // observation info already exists; do not observe it again // unlock and return pthread_mutex_unlock(&_lock); return; } // lazilly create set of infos if (nil == infos) { infos = [NSMutableSet set]; [_objectInfosMap setObject:infos forKey:object]; } // add info and oberve [infos addObject:info]; // unlock prior to callout pthread_mutex_unlock(&_lock); [[_FBKVOSharedController sharedController] observe:object info:info]; }Copy the code
_objectInfosMap
Type:{ NSMapTable<id, NSMutableSet<_FBKVOInfo *> *> *_objectInfosMap; pthread_mutex_t _lock; } Copy the code
_FBKVOSharedController
The class is a singleton that stores related information.
Enter observe:info: Observe:
- (void)observe:(id)object info:(nullable _FBKVOInfo *)info { if (nil == info) { return; } // register info pthread_mutex_lock(&_mutex); [_infos addObject:info]; pthread_mutex_unlock(&_mutex); // add observer [object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info]; if (info->_state == _FBKVOInfoStateInitial) { info->_state = _FBKVOInfoStateObserving; } else if (info->_state == _FBKVOInfoStateNotObserving) { // this could happen when `NSKeyValueObservingOptionInitial` is one of the NSKeyValueObservingOptions, // and the observer is unregistered within the callback block. // at this time the object has been registered as an observer (in Foundation KVO), // so we can safely unobserve it. [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info]; }}Copy the code
- Here,
object
isself.person
It was passed on instead ofVC
Because the callback implementation here usesblock
I don’t need to call back toVC
. [_infos addObject:info];
Related information is added to the singleton.[object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];
Here it is called againKVO
The listening operation of
2. FBKVOController
How is it automaticremove
the
When VC releases _kvoCtrl as a property will also be released, it will call its dealloc method, automatic remove is also implemented here.
Enter FBKVOController’s dealloc:
- (void) 'dealloc' : {[unobserveAll]; pthread_mutex_destroy(&_lock); } - (void)unobserveAll { [self _unobserveAll]; }Copy the code
Enter the _unobserveAll:
- (void)_unobserveAll { // lock pthread_mutex_lock(&_lock); NSMapTable *objectInfoMaps = [_objectInfosMap copy]; // clear table and map [_objectInfosMap removeAllObjects]; // unlock pthread_mutex_unlock(&_lock); _FBKVOSharedController *shareController = [_FBKVOSharedController sharedController]; for (id object in objectInfoMaps) { // unobserve each registered object and infos NSSet *infos = [objectInfoMaps objectForKey:object]; [shareController unobserve:object infos:infos]; }}Copy the code
- get
_FBKVOSharedController
Singleton, callunobserve:infos:
.
Enter the unobserve: infos: :
- (void)unobserve:(id)object infos:(nullable NSSet<_FBKVOInfo *> *)infos { if (0 == infos.count) { return; } // unregister info pthread_mutex_lock(&_mutex); for (_FBKVOInfo *info in infos) { [_infos removeObject:info]; } pthread_mutex_unlock(&_mutex); // remove observer for (_FBKVOInfo *info in infos) { if (info->_state == _FBKVOInfoStateObserving) { [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info]; } info->_state = _FBKVOInfoStateNotObserving; }}Copy the code
- Remove related information:
for (_FBKVOInfo *info in infos) { [_infos removeObject:info]; } Copy the code
- for
removeObserver
Operation:[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info]; Copy the code