Source address: KVOController , summary
- What did FBKVOController do
- FBKVOController use pose
- FBKVOController source code analysis
- Summary of FBKVOController design ideas
- Other gains of FBKVOController
What did FBKVOController do?
In short, Facebook's open source code is very few, with only two classes and one category. It mainly encapsulates the KVO mechanism we often use. The most striking feature is that it provides a block callback for us to process, so as to avoid the scattering of KVO related code. The following method is no longer needed:
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
Use posture
Using the open source framework, we can implement KVO in this way. The second method can be implemented in one line of code:
#import "ViewController.h" #import "FBKVOController.h" #import "NSObject+FBKVOController.h" @interface KVOModel : NSObject @property (nonatomic, copy) NSString *name; @property (nonatomic, assign) NSUInteger age; @end @implementation KVOModel @end NS_ASSUME_NONNULL_BEGIN @interface ViewController () @property (nonatomic, strong) KVOModel *model; @property (nonatomic, strong) FBKVOController *kvoController; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //Create the observed model class KVOModel *model = [[KVOModel alloc] init]; //Initializes and sets the value of the member variable of the model model.name = @"wo"; model.age = 5; self.model = model; //The first method: create an FBKVOController object, which is strongly referenced by the VC. Otherwise, it will be destroyed if it is out of the current scope FBKVOController *kvoController = [[FBKVOController alloc] initWithObserver:self]; _kvoController = kvoController; //Add observation [kvoController observe:model keyPath:@"name" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSKeyValueChangeKey,id> * _Nonnull change) { NSLog(@"My old name was:%@", change[NSKeyValueChangeOldKey]); NSLog(@"My new name is:%@", change[NSKeyValueChangeNewKey]); }]; //The second method: without actively creating FBKVOController objects, self.KVOController directly lazy loads and creates FBKVOController objects //You can directly perform KVO on multiple member variables of an object //------Really realize one line of code to get KVO------ [self.KVOController observe:model keyPaths:@[@"name", @"age"] options: NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew block:^(id observer, id object, NSDictionary *change) { NSString *changedKeyPath = change[FBKVONotificationKeyPathKey]; if ([changedKeyPath isEqualToString:@"name"]) { NSLog(@"Changed the name"); } else if ([changedKeyPath isEqualToString:@"age"]) { NSLog(@"Modified age"); } NSLog(@"Old values are:%@", change[NSKeyValueChangeOldKey]); NSLog(@"The new values are:%@", change[NSKeyValueChangeNewKey]); }]; //Modify the name member variable of the model model.name = @"ni"; } @end NS_ASSUME_NONNULL_END
Advantages over native API s:
- 1. KVO can be performed on multiple different member variables of model in the form of array at the same time.
- 2. Use the provided block to concentrate the KVO related code in one piece instead of scattered everywhere. Relatively clear and clear at a glance.
- 3. There is no need to cancel the observation of the object in the dealloc method. When the FBKVOController object dealloc, the observation will be automatically cancelled.
Source code analysis
This set of source code mainly includes four files: FBKVOController.h, FBKVOController.m, NSObject+FBKVOController.h and NSObject+FBKVOController.m.
Among them, the classification of NSObject+FBKVOController is relatively simple. The main thing it does is through objc_setAssociatedObject (associated object) creates and associates an object of FBKVOController to NSObject in the form of lazy loading.
Next, I will focus on today's protagonist FBKVOController class. Its file also contains two other classes_ FBKVOInfo,_ FBKVOSharedController . It will be introduced below.
Let's first look at the initialization function specified by FBKVOController:
- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved { self = [super init]; if (nil != self) { //Generally, the observer holds the FBKVOController. To avoid circular reference, the_ The memory management semantics of observer is weak reference _observer = observer; //Define the memory management policy of the NSMapTable key. By default, the passed in parameter retainObserved = YES NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality; //To create an nsmaptable, the key is of id type and the value is nsmutableset<_ Fbkvoinfo * > type _objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0]; //Initialize mutex to avoid data competition among multiple threads pthread_mutex_init(&_lock, NULL); } return self; }
In the above initialization code, the comments are clearly written. The only stranger is NSMapTable. In short, it is similar to NSDictionary. The difference is that NSMapTable can independently control the memory management strategy of key / value. The memory policy of NSDictionary is fixed to copy. When the key is object, the cost of copy may be large! Therefore, only the relatively flexible NSMapTable can be used here.
Perform KVO related method code parsing
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block { //When the keyPath string length is 0 or the block is empty, an assertion will be generated and the program will crash NSAssert(0 != keyPath.length && NULL != block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPath, block); //If the observed object is nil, it will also be returned directly if (nil == object || 0 == keyPath.length || NULL == block) { return; } // create info _FBKVOInfo _FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block]; // observe object with info [self _observe:object info:info]; }
In the above code, a previously mentioned_ FBKVOInfo class, which stores information including FBKVOController, keypath, options and block.
Follow the last sentence of the previous code [self _observe:object info:info];
- (void)_observe:(id)object info:(_FBKVOInfo *)info { // lock mutex locking pthread_mutex_lock(&_lock); //Remember the NSMapTable created when initializing FBKVOController? //The structure takes the observed object as the key. Unlike the commonly used NSDictionary, NSString is used as the key NSMutableSet *infos = [_objectInfosMap objectForKey:object]; // check for info existence // Must override_ FBKVOInfo hash and isEqual methods, so that the member method of NSSet can be used. _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 there is no relevant information about this object (observed), create NSMutableSet and add it to NSMapTable // lazilly create set of infos if (nil == infos) { infos = [NSMutableSet set]; [_objectInfosMap setObject:infos forKey:object]; } // add info and oberve -- NSMutableSet [infos addObject:info]; // unlock prior to callout pthread_mutex_unlock(&_lock); //What is sharedController for? All observation information is uniformly handed over to a single example [[_FBKVOSharedController sharedController] observe:object info:info]; }
Summarize the data structure in the previous paragraph. FBKVOController has the member variable NSMapTable, which takes the observed object as the key and NSMutableSet as the value. Different info is stored in NSMutableSet. The relationship diagram is as follows:
Trace this code
[[_FBKVOSharedController sharedController] observe:object info:info];
_ FBKVOSharedController is a single instance that will exist throughout the app life cycle. Its responsibilities are to receive and forward KVO notifications. Therefore, all KVO notifications in the app are completed by this singleton.
- (void)observe:(id)object info:(nullable _FBKVOInfo *)info { if (nil == info) { return; } // register info add info to NSHashTable //Note: in_ The NSMutableSet in FBKVOController class has strongly referenced info //Here, the NSHashTable is used for weak reference to info. When info dealloc, it will be deleted from the container at the same time pthread_mutex_lock(&_mutex); [_infos addObject:info]; pthread_mutex_unlock(&_mutex); //_ FBKVOSharedController is the actual observer! It will be forwarded later, //context is void * typeless pointer and info pointer! [object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info]; //If the state is the original state, it will be changed to the observed state, indicating that it is in the observed state 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]; } }
In the above code, I want to talk about the following code separately. The context parameter uses the pointer of (void *)info, which can ensure the uniqueness of context.
Receive KVO notice and handle it accordingly
- (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 // Use context to find info, where void * is converted to id type variable (_bridge id) 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]; //Merge the dictionary and make a new copy, //The information includes: 1. Which value has been changed, mChange 2. The original change dictionary [mChange addEntriesFromDictionary:change]; changeWithKeyPath = [mChange copy]; } info->_block(observer, object, changeWithKeyPath); } else if (info->_action) { //Ignore warning! #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" [observer performSelector:info->_action withObject:change withObject:object]; #pragma clang diagnostic pop } else { //Call the observer's native function by default!! [observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context]; } } } } }
Summary of design ideas
- 1. Fbkvocontroller holds the NSMapTable and gets the corresponding NSMutableSet with object as the key. The NSMutableSet stores different_ FBKVOInfo. The main function of this data structure is to prevent developers from adding the same KVO repeatedly. When it is checked that the same_ Fbkvoinfo object, the following code will not be executed.
- 2 _FBKVOSharedController holds NSHashTable. NSHashTable holds different values in the form of weak references_ FBKVOInfo. The KVO code is actually executed here_ Fbkvoinfo has an important member variable_ FBKVOInfoState. Add or delete kvos according to the enumeration values (_FBKVOInfoStateInitial, _FBKVOInfoStateObserving, _FBKVOInfoStateNotObserving).
Harvest (after reading through and studying the source code)
- 1. Learning of nsset / nshashtable, NSDictionary/ NSMapTable
NSSet is a collection class that filters out duplicate objects. NSHashTable is an upgraded container of NSSet and has only a variable version. It allows the holding relationship of weak references to objects added to the container. When an object in NSHashTable is destroyed, the object will also be removed from the container.
NSMapTable is similar to NSDictionary. The only difference is that it has multiple functions: NSPointerFunctionsOptions of key and value can be set! The key policy of NSDictionary is fixed to copy. Considering the cost, a simple number or string is generally used as the key. But what if you encounter an application scenario that requires an object as a key? NSMapTable can come in handy! Memory management policies for key and value can be defined through nsfunctionsponter, which can be divided into strong,weak and copy.