FBKVOController source
FBKVOController
advantages
- Avoid repeated add, repeated remove mismatch problem
- Support Block,SEL, write code more convenient and fast
- The observer is released early, causing flashback problems
disadvantages
- Specification requirements need to be made in the project, otherwise there will be two ways: KVO and FBKVOController
- Bring extra memory consumption, NSMapTable*objectInfosMap, _FBKVOSharedController *sharedController, these consumption is acceptable, better than the online flash back, of course, can also Hook system method to solve the flash back problem.
structure
- NSObject+FBKVOController
- _FBKVOInfo
- FBKVOController
- _FBKVOSharedController
Logical diagram
NSObject+FBKVOController
- This is a class of NSObject, and any object that inherits from NSObject can use it.
- call
self.KVOController
orself.KVOControllerNonRetaining
Will generate aFBKVOController
Instance objects. The difference between instance objects isFBKVOController
The inside of theNSMapTable*objectInfosMap
At initializationkeyOptions
The different. If YES, the agent will hold objects internally, equivalent to Strong; if NO, equivalent to Weak. - Will generate part 2
self.KVOController
Object, which is saved by association operations.
@property (nonatomic, strong) FBKVOController *KVOController;
@property (nonatomic, strong) FBKVOController *KVOControllerNonRetaining;
Copy the code
FBKVOController * controller = [FBKVOController controllerWithObserver:self retainObserved:YES/NO];
objc_setAssociatedObject(self, Key, KVOController, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
Copy the code
_FBKVOInfo
Wrap the parameters for FBKVOController *KVOController to call the observe method, including the following:
- __weak FBKVOController *_controller;
- _keyPath
- _options
- _FBKVOInfoState _state, default is _FBKVOInfoStateInitial, When in the state = _FBKVOInfoStateObserving, remove when the observer state = _FBKVOInfoStateNotObserving
- _block,_action, void *_context, either all empty, or only one of them has a value.
@interface _FBKVOInfo : NSObject
{
@public
__weak FBKVOController *_controller;
NSString *_keyPath;
NSKeyValueObservingOptions _options;
SEL _action;
void *_context;
FBKVONotificationBlock _block;
_FBKVOInfoState _state;
}
Copy the code
@end
_FBKVOInfo initialization
- (instancetype)initWithController:(FBKVOController *)controller keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(nullable FBKVONotificationBlock)block action:(nullable SEL)action context:(nullable void *)context { self = [super init]; if (nil ! = self) { _controller = controller; _block = [block copy]; _keyPath = [keyPath copy]; _options = options; _action = action; _context = context; } return self; }Copy the code
Determine if the two _FBKVOInfo are equal, overridehash
.isEqual:(id)object
methods
- (NSUInteger)hash { return [_keyPath hash]; } - (BOOL)isEqual:(id)object { if (nil == object) { return NO; } if (self == object) { return YES; } if (! [object isKindOfClass:[self class]]) { return NO; } return [_keyPath isEqualToString:((_FBKVOInfo *)object)->_keyPath]; }Copy the code
FBKVOController
The Class structure
// Observers can sometimes be holders of FBKVOController * Object, as weak does not cause strong references. __weak id observer; // Key is the observed address, and value is a set of _FBKVOInfo, where isEqual is overridden. NSMapTable<id, NSMutableSet<_FBKVOInfo *> *> *_objectInfosMap; Pthread_mutex_t _lock;Copy the code
Initialize the
- _observer assignment
- _objectInfosMap initialization, according to the value of retainObserved, decides whether the operation is Strong or weak when storing.
- Pthread_mutex_t _lock is initialized
- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved
{
_observer = observer;
NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
_objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];
pthread_mutex_init(&_lock, NULL);
}
Copy the code
The method call
- Parameter verification, invalid return, and then the parameter wrapped into a
_FBKVOInfo *info
Instance object that will be generated_FBKVOInfo *info
Pass to step 2.
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
{
if (nil == object || 0 == keyPath.length || NULL == block) return;
_FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];
[self _observe:object info:info];
}
Copy the code
- Check whether the object is registered, and if so
NSMutableSet *infos = [_objectInfosMap objectForKey:object];
Will not be empty, not registered will create a new one and store it in_objectInfosMap
In the. Key is the memory address of object - Check whether the new keyPath already exists and return directly to avoid repeated addition. [This is the key step]
- Pass Object and info to the singleton
_FBKVOSharedController
- (void)_observe:(id)object info:(_FBKVOInfo *)info { // lock pthread_mutex_lock(&_lock); NSMutableSet *infos = NSMutableSet *infos; The _observe method NSMutableSet *infos = [_objectInfosMap objectForKey:object] has been executed. [infos member:info], if info is stored, return nil, if not, return nil _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; If (nil == infos) {infos = [NSMutableSet set]; [_objectInfosMap setObject:infos forKey:object]; } // Put the newly registered info into NSMutableSet infos [infos addObject:info]; // unlock prior to callout pthread_mutex_unlock(&_lock); // Pass the registered object info to _FBKVOSharedController [[_FBKVOSharedController sharedController] observe:object info:info]; }Copy the code
- Remove the observation of the single property keyPath in object
- First check whether the object has added an attribute observation
- Then check whether the keyPath to be removed exists. If it does, remove keyPath from infOS. After removing keyPath, infOS is empty, indicating that the object has no properties.
- Pass data to a singleton
_FBKVOSharedController
- (void)_unobserve:(id)object info:(_FBKVOInfo *)info { // lock pthread_mutex_lock(&_lock); NSMutableSet *infos = [_objectInfosMap objectForKey:object]; NSMutableSet *infos = [_objectInfosMap objectForKey:object]; RegisteredInfo _FBKVOInfo *registeredInfo = [infos member:info]; registeredInfo = [infos member:info]; if (nil ! = registeredInfo) {/ / will keyPath lift [infos removeObject: registeredInfo]; // remove no longer used infos if (0 == infos.count) { Remove the object corresponding to a collection of [_objectInfosMap removeObjectForKey: object]; } } // unlock pthread_mutex_unlock(&_lock); // unobserve [[_FBKVOSharedController sharedController] unobserve:object info:registeredInfo]; }Copy the code
_FBKVOSharedController
- Singleton design pattern
- Implementation of KVO – related methods
- Methods of listening and removing properties are provided externally, notifying the observer of receiving the observed property change method.
- Thread safety
Class structure
{
NSHashTable<_FBKVOInfo *> *_infos;
pthread_mutex_t _mutex;
}
Copy the code
NSHashTable *infos NSHashTable *infos = [[NSHashTable alloc] initWithOptions:]; pthread_mutex_init(&_mutex, NULL); return self; }Copy the code
Core method
- Stores the info registered to listen into the _infOS collection
- Add a listen to the property keypath of obejCT and pass info back as context, which will be used when property changes are received.
- Change the info – > _state state for _FBKVOInfoStateNotObserving when removing the observers to monitor directly
- (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); // The singlet _FBKVOSharedController adds a view of the object's property keyPath, where we pass info to the context [object addObserver:self] forKeyPath:info->_keyPath options:info->_options context:(void *)info]; If (info->_state == _FBKVOInfoStateInitial) {info->_state = _FBKVOInfoStateObserving; } else if (info - > _state = = _FBKVOInfoStateNotObserving) {/ / if the state is not in the observation, [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info]; }}Copy the code
- According to the info, the corresponding property observer, and change the info – > _state status to _FBKVOInfoStateNotObserving
- (void)unobserve:(id)object info:(nullable _FBKVOInfo *)info { if (nil == info) { return; } // unregister info pthread_mutex_lock(&_mutex); [_infos removeObject:info]; pthread_mutex_unlock(&_mutex); If (info->_state == _FBKVOInfoStateObserving) {[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info]; } info->_state = _FBKVOInfoStateNotObserving; }Copy the code
- Notify the observer of the singleton _FBKVOSharedController when the properties of the observed object change
- _FBKVOInfo *info = _FBKVOInfo *info
- According to _FBKVOInfo *info, get controller, Observer, etc
- Execute a block, Sel, or tell the observer that the property information has changed. Note that the observer can be implemented directly
observeValueForKeyPath:ofObject:change:context
The difference is that the change message is received by _FBKVOSharedController and then passed to the observer.
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context { NSAssert(context, @"missing context keyPath:%@ object:%@ change:%@", keyPath, object, change); _FBKVOInfo *info; { // lookup context in registered infos, taking out a strong reference only if it exists pthread_mutex_lock(&_mutex); info = [_infos member:(__bridge id)context]; pthread_mutex_unlock(&_mutex); } if (nil ! = info) { // take strong reference to controller FBKVOController *controller = info->_controller; if (nil ! = controller) { // take strong reference to observer id observer = controller.observer; if (nil ! = observer) { // dispatch custom block or action, fall back to default action if (info->_block) { NSDictionary<NSKeyValueChangeKey, id> *changeWithKeyPath = change; // add the keyPath to the change dictionary for clarity when mulitple keyPaths are being observed if (keyPath) { NSMutableDictionary<NSString *, id> *mChange = [NSMutableDictionary dictionaryWithObject:keyPath forKey:FBKVONotificationKeyPathKey]; [mChange addEntriesFromDictionary:change]; changeWithKeyPath = [mChange copy]; } info->_block(observer, object, changeWithKeyPath); } else if (info->_action) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" [observer performSelector:info->_action withObject:change withObject:object]; #pragma clang diagnostic pop } else { [observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context]; } } } } }Copy the code
Making a link
Github.com/facebookarc…